Compare commits

..

1637 Commits

Author SHA1 Message Date
Girish Ramakrishnan
1d3c27ec30 Add stopped state
(cherry picked from commit b14828e8e1)
2020-10-06 13:03:43 -07:00
Johannes Zellner
b6d3c18944 Immediately indicate if remote ssh is now enabled
(cherry picked from commit 4274b8f459)
2020-10-06 13:03:37 -07:00
Johannes Zellner
31282af701 Add enableSshSupport option to support tickets
(cherry picked from commit 41e8bcd02f)
2020-10-06 13:03:31 -07:00
Girish Ramakrishnan
45c8b83d54 robots: ensure trailing and leading whitespaces are preserved
(cherry picked from commit 2fe86f9b8a)
2020-10-05 21:31:44 -07:00
Girish Ramakrishnan
29d23c9dcc Revert "unhide the volume UI"
This reverts commit 68573ceb18.
2020-10-05 12:47:18 -07:00
Johannes Zellner
589f19f370 Make app bug report the default, this is what mostly happens 2020-10-05 15:10:23 +02:00
Girish Ramakrishnan
8e20db664f Pre-select app domain by default in redirection dropdown 2020-10-04 16:39:59 -07:00
Girish Ramakrishnan
fdcd457ce1 Add link to forum 2020-09-30 09:41:20 -07:00
Girish Ramakrishnan
95516a2383 Update readme 2020-09-30 09:40:18 -07:00
Girish Ramakrishnan
ba92b1e667 Keep things alphabetical 2020-09-29 14:52:11 -07:00
Johannes Zellner
f3a159823a Mention why an app update cannot be applied and provide shortcut to start the app if stopped 2020-09-29 17:32:25 +02:00
Johannes Zellner
8388491e58 Remove version from footer 2020-09-29 16:41:00 +02:00
Johannes Zellner
e87d206dda Show Cloudron version in settings -> updates 2020-09-29 16:38:31 +02:00
Johannes Zellner
db4c8d92da Make the autoupdate disabled text explicit 2020-09-29 16:34:49 +02:00
Johannes Zellner
daab4a95c2 Move services menu entry up 2020-09-29 15:24:24 +02:00
Girish Ramakrishnan
22b8b9b9bd lint 2020-09-28 16:10:55 -07:00
Johannes Zellner
c87f3a8cb4 Give services panel a separate top-level view 2020-09-28 15:16:02 +02:00
Johannes Zellner
72118a0b66 Add app state filter 2020-09-26 17:50:23 +02:00
Girish Ramakrishnan
68573ceb18 unhide the volume UI 2020-09-24 14:38:34 -07:00
Johannes Zellner
510b88cd68 Make the splash default more fun, needs a minigame 2020-09-24 13:25:34 +02:00
Girish Ramakrishnan
490720e6a7 Add a splash page
part of cloudron/box#739
2020-09-23 22:14:29 -07:00
Girish Ramakrishnan
990f75dddc import: when importing filesystem backups, the input box is a path 2020-09-21 21:58:11 -07:00
Girish Ramakrishnan
a3c6b82283 Fix exception when getStatus errored 2020-09-21 21:48:22 -07:00
Girish Ramakrishnan
f5e0ff51f2 preserve sorting order when doing async queries 2020-09-16 16:03:14 -07:00
Girish Ramakrishnan
f114a629f3 stash the length separately 2020-09-14 12:12:39 -07:00
Girish Ramakrishnan
5fca372ddf blocklist is now a text file in the backend 2020-09-14 12:00:47 -07:00
Johannes Zellner
d9d1f13bf9 Ensure blocked ips are deduped and empty strings removed 2020-09-12 20:53:12 +02:00
Johannes Zellner
63b212bea5 Fix blocklist error form state handling 2020-09-12 19:13:10 +02:00
Girish Ramakrishnan
5a1e09936f Change step size to 1MB 2020-09-11 09:48:22 -07:00
Girish Ramakrishnan
e21a504c35 we upload 3 parts in parallel 2020-09-10 08:30:59 -07:00
Johannes Zellner
3ba6c387e9 Update dependencies and xtermjs 2020-09-10 15:37:05 +02:00
Johannes Zellner
2c7238b2c9 Make logviewer timestamp sticky 2020-09-10 15:31:19 +02:00
Girish Ramakrishnan
92b9fc02fa Fix memory slider 2020-09-10 00:07:12 -07:00
Girish Ramakrishnan
576281990b Link to our docs and not external 2020-09-09 23:19:37 -07:00
Girish Ramakrishnan
6b7570df4e just call it re-configured 2020-09-09 22:36:57 -07:00
Girish Ramakrishnan
b141db4776 mail location audit log 2020-09-09 22:31:57 -07:00
Girish Ramakrishnan
4cffcfff03 mail: move config eventlogs to box code 2020-09-09 22:24:38 -07:00
Girish Ramakrishnan
59ea292263 only reconfigure email apps when mail server relocated 2020-09-09 21:44:14 -07:00
Girish Ramakrishnan
e0ca52b1da Disable changing location when task is active 2020-09-09 21:44:11 -07:00
Girish Ramakrishnan
0c9ea1e0f0 blocklist is only for owner 2020-09-09 20:28:26 -07:00
Girish Ramakrishnan
c02cf0f5dc Fix doc links 2020-09-09 10:14:35 -07:00
Girish Ramakrishnan
d0e2df5166 re-configure mail apps on mail fqdn change 2020-09-08 19:34:27 -07:00
Girish Ramakrishnan
b9cda71413 adminFqdn -> mailFqdn 2020-09-08 15:18:46 -07:00
Johannes Zellner
e008e44566 No need to mention logs in logviewer 2020-09-06 10:16:59 +02:00
Johannes Zellner
c100539736 button group in logviewer looks wrong 2020-09-06 10:16:18 +02:00
Johannes Zellner
32aa3febf9 Do not linebreak loglines 2020-09-06 10:15:41 +02:00
Girish Ramakrishnan
1249b3b3e8 Put save and close together 2020-09-05 23:04:51 -07:00
Girish Ramakrishnan
18ba66afcc add linode singapore region 2020-09-02 19:35:02 -07:00
Girish Ramakrishnan
1000d88508 ovh: add sydney region 2020-09-02 19:30:55 -07:00
Girish Ramakrishnan
e13cb1debd Fix placeholder text 2020-09-02 14:27:09 -07:00
Johannes Zellner
2c3c8f8c4a Show graph labels based on locale 2020-09-02 18:53:46 +02:00
Johannes Zellner
b81196fa87 Update Chart.js to v2.9.3 2020-09-02 17:57:01 +02:00
Johannes Zellner
c7291af970 Instead of random string for app icon invalidation use app version
This still leaves a potential issue, where an app gets updated using the
cli while not bumping the version and changing the icon, but maybe we
can ignore that for now in favor of the browser cache for 99% of the
cases
2020-09-02 15:14:46 +02:00
Johannes Zellner
92c3237552 Ensure mail location progress starts at 0 2020-09-02 14:12:38 +02:00
Girish Ramakrishnan
848e446b93 Explain what domain is 2020-09-01 21:49:38 -07:00
Johannes Zellner
2f96f565eb Use TASK_TYPES in backup view 2020-09-01 16:36:07 +02:00
Johannes Zellner
8fa58eb108 Show mail domain change task progress 2020-09-01 16:31:23 +02:00
Johannes Zellner
31947127d9 Add TASK_TYPE definitions to client.js 2020-09-01 16:31:09 +02:00
Johannes Zellner
2c7cfa1a93 Also add Filemanager button to logviewer 2020-09-01 15:18:42 +02:00
Johannes Zellner
b856c4f995 Indent logviewer with 2 spaces since html 2020-09-01 15:17:33 +02:00
Johannes Zellner
497be710a7 Only provide save for filemanager editor 2020-09-01 12:39:02 +02:00
Girish Ramakrishnan
d7287b5c3c require owner for firewall config 2020-08-31 22:55:30 -07:00
Girish Ramakrishnan
854010b823 warn user about block list 2020-08-31 22:12:33 -07:00
Girish Ramakrishnan
39f7a5be70 Add firewall UI 2020-08-31 21:45:56 -07:00
Girish Ramakrishnan
dbc53b8d09 Remove blocked IPs
This will be implemented in the firewall level for now
2020-08-31 17:57:45 -07:00
Girish Ramakrishnan
c4fe362a08 Typo 2020-08-31 15:28:28 -07:00
Girish Ramakrishnan
f55ec5de9b Add note on backups in initial screen 2020-08-31 11:37:29 -07:00
Johannes Zellner
b2279c9acc Make texteditor fullscreen and add saving without closing action 2020-08-31 17:46:26 +02:00
Girish Ramakrishnan
b420d054ae show ldap login into in all the post install dialogs 2020-08-28 17:08:18 -07:00
Girish Ramakrishnan
566f0f7783 mail: add banner ui
part of cloudron/box#341
2020-08-24 10:36:24 -07:00
Girish Ramakrishnan
ae24c1d968 Move the refresh functions 2020-08-22 19:34:06 -07:00
Girish Ramakrishnan
8ca344e3bf Various text changes 2020-08-22 16:43:17 -07:00
Girish Ramakrishnan
0458d2cb90 Fix mail eventlog to show the new events 2020-08-22 13:08:05 -07:00
Girish Ramakrishnan
7c2322e6e0 Fixup spam configuration UI 2020-08-22 13:01:25 -07:00
Girish Ramakrishnan
08abe4bff2 Add note on restart 2020-08-20 23:28:43 -07:00
Girish Ramakrishnan
eb69c365fc Fix mail server location UI 2020-08-20 23:23:43 -07:00
Girish Ramakrishnan
f6fef21bf7 Fixup route for setting max email size 2020-08-20 22:28:58 -07:00
Girish Ramakrishnan
4a1f8457cf Switch to the merged automatic update route 2020-08-19 22:30:48 -07:00
Girish Ramakrishnan
5eb5b952d5 Further clarification 2020-08-19 15:01:39 -07:00
Girish Ramakrishnan
8a375c6363 Add note on part size concurrency 2020-08-19 14:59:27 -07:00
Girish Ramakrishnan
ac23b610bc Add upload part size slider 2020-08-19 14:56:41 -07:00
Johannes Zellner
5f8b141f62 Add stub mail settings section 2020-08-17 22:38:11 -07:00
Girish Ramakrishnan
517db50712 Hide concurrency settings for non-s3/gcs backup storage 2020-08-15 23:09:21 -07:00
Girish Ramakrishnan
6310a431dd caas: remove hyphenatedSubdomains support
this is not used since ages now
2020-08-15 18:35:51 -07:00
Girish Ramakrishnan
9996e9a6d7 Add help text for ptr record 2020-08-14 10:24:21 -07:00
Girish Ramakrishnan
ddc211a8ea Looks for search string in app title as well 2020-08-14 09:39:39 -07:00
Girish Ramakrishnan
32f4f88b88 help text everywhere 2020-08-11 17:03:49 -07:00
Girish Ramakrishnan
45b3062ac6 Always init the concurrency values 2020-08-11 16:51:02 -07:00
Girish Ramakrishnan
03296b3195 Fix padding of sliders 2020-08-11 12:07:57 -07:00
Girish Ramakrishnan
97df39a16f add advanced section to tune backup settings 2020-08-11 09:16:09 -07:00
Girish Ramakrishnan
59cd6f6e93 Fix groups code to use listing API
the listing API now returns the members
2020-08-10 13:59:46 -07:00
Girish Ramakrishnan
d4312507e2 Simplify wording 2020-08-10 13:11:10 -07:00
Girish Ramakrishnan
76950bdada remove superfluous wording 2020-08-10 13:08:53 -07:00
Girish Ramakrishnan
01b7bc96fa Fix layout of backup retention dialog 2020-08-10 13:03:59 -07:00
Girish Ramakrishnan
efde15b848 backup: remind to not overlap with update schedule 2020-08-10 12:58:19 -07:00
Girish Ramakrishnan
941e0ba6c8 This deletes filters as well 2020-08-10 12:19:20 -07:00
Girish Ramakrishnan
3b818855dc Fix broken help link 2020-08-10 12:02:53 -07:00
Girish Ramakrishnan
f73c8b00d4 global replace 2020-08-08 22:02:31 -07:00
Girish Ramakrishnan
08f116486a update showndown to 1.9.1
this has openLinksInNewWindow which is smart enough that internal links
open in same tab and external links open in new tab
2020-08-08 21:58:44 -07:00
Girish Ramakrishnan
f6f5ae8578 show users name 2020-08-08 19:15:21 -07:00
Girish Ramakrishnan
d82dde4b7f Add some $variables to the post install message
This way the post install messages can be better.

Removed it as a filter because I cannot figure how to pass args to it
2020-08-08 19:12:16 -07:00
Girish Ramakrishnan
91d4d95cb4 linter says the escapes are not needed 2020-08-08 17:58:48 -07:00
Girish Ramakrishnan
b9973d69c3 SSO_MARKER is now standardized 2020-08-08 17:58:04 -07:00
Girish Ramakrishnan
8c8e363abc atleast is not a real word
thanks @rob
2020-08-05 10:12:13 -07:00
Girish Ramakrishnan
aa240e8ee3 Remove "old" 2020-08-03 21:00:20 -07:00
Johannes Zellner
cdaf9e1876 Fix typo for notification bell icon change 2020-07-31 09:25:40 +02:00
Johannes Zellner
1c8352ec56 Fontawesome SVG fonts don't play well with angular1
Essentially SVG fonts use JS to inject elements.
This does not work with ng-show/hide
2020-07-31 09:17:55 +02:00
Johannes Zellner
43ef7f088d Improve multiselect padding when item is not selected 2020-07-31 09:17:34 +02:00
Girish Ramakrishnan
28b4f66f86 wording 2020-07-30 11:48:40 -07:00
Johannes Zellner
4fb94ea162 Update xtermjs to latest v4.8.1 2020-07-30 14:39:30 +02:00
Johannes Zellner
d24340f221 Update to fontawesome 5.14.0 and use svg fonts 2020-07-30 13:20:28 +02:00
Johannes Zellner
482cd123c0 Make notification a separate navbar item 2020-07-30 13:19:03 +02:00
Girish Ramakrishnan
ab3abe7e5e Add a way to disable auto updates 2020-07-29 20:14:30 -07:00
Girish Ramakrishnan
31fbffb435 better wording 2020-07-29 16:51:12 -07:00
Girish Ramakrishnan
9a7f8bd861 Allow days/hours to be selected for auto update schedule 2020-07-29 16:10:29 -07:00
Girish Ramakrishnan
29c20cfcc4 rename variable 2020-07-29 15:24:10 -07:00
Girish Ramakrishnan
b5c25bcaaa Fix typo in pattern 2020-07-29 12:01:00 -07:00
Girish Ramakrishnan
8abe0a174a Handle case where all days are to be selected 2020-07-29 09:27:55 -07:00
Girish Ramakrishnan
692abcd6de show backup days and hours 2020-07-28 23:09:25 -07:00
Johannes Zellner
03bdcc786e Show critical backup config warning directly in backup ui instead of notification 2020-07-28 18:08:57 +02:00
Johannes Zellner
6df2985e2a Remove wrong hand css class 2020-07-28 12:24:21 +02:00
Girish Ramakrishnan
05de8b54ec Add option to delete mails
Part of cloudron/box#720
2020-07-27 22:55:25 -07:00
Dustin Dauncey
c0dad4f5a0 Update system.html 2020-07-27 22:29:22 -07:00
Dustin Dauncey
7ad425e399 Update system.html with a more accurate message on when to use the reboot function. 2020-07-27 22:29:22 -07:00
Johannes Zellner
836a3784cb Add missing mimer dependeny files 2020-07-27 11:48:00 +02:00
Girish Ramakrishnan
06d4aec850 Fix various links in README 2020-07-24 15:05:49 -07:00
Johannes Zellner
614674563a Show folders first 2020-07-23 15:01:50 +02:00
Johannes Zellner
349633c8da Better icon handling in filemanager 2020-07-23 12:22:00 +02:00
Johannes Zellner
7d4f617757 Support moving files across folders when renaming 2020-07-23 12:07:24 +02:00
Girish Ramakrishnan
e82f17ab06 Show any settings save error 2020-07-22 18:09:44 -07:00
Johannes Zellner
cb14592705 Make filemanager reload pickup the directory from hash 2020-07-22 21:41:32 +02:00
Johannes Zellner
77300d6858 Add burger button to also open context-menu 2020-07-22 21:38:39 +02:00
Johannes Zellner
38682e48d4 Open different mimetypes differently in filemanager 2020-07-21 16:27:51 +02:00
Johannes Zellner
1e5d28e2a2 Fix nav-bar in dark mode 2020-07-21 10:18:50 +02:00
Johannes Zellner
ad86b4b1eb Fix mail domain listing in dark mode 2020-07-21 09:26:44 +02:00
Johannes Zellner
99927df991 Also ensure the admin link is not active if it shouldn't 2020-07-20 23:41:00 +02:00
Johannes Zellner
6661f21e2f Handle symlinks in filemanager 2020-07-18 19:26:18 +02:00
Girish Ramakrishnan
4ef963fe54 Don't let the user bypass 2FA by removing the 'setup2FA' in the url 2020-07-17 14:46:58 -07:00
Girish Ramakrishnan
c87ddd5116 Use prettyByteSize instead of prettyDiskSize
this prevents 'not available yet' string for 0 size files
2020-07-17 14:25:00 -07:00
Girish Ramakrishnan
4f4df7d9fe appstore: fix ordering of apps
all apps: alphabetical
popular: based on ranking instead of installCount
New Apps: based on time only
Category: tag and then ranking
2020-07-17 14:22:47 -07:00
Girish Ramakrishnan
0043b3690a Make directory config UI consistent 2020-07-17 10:17:46 -07:00
Girish Ramakrishnan
be6c34386d Always show the catch-all description text 2020-07-17 09:51:37 -07:00
Girish Ramakrishnan
a8e9a71489 Add missing break 2020-07-17 09:32:32 -07:00
Girish Ramakrishnan
90f42fe6cd Fixup text in postinstall and info dialog 2020-07-16 15:43:30 -07:00
Johannes Zellner
6dd414fe7e Add mailbox restriction code 2020-07-16 18:51:29 +02:00
Johannes Zellner
4cb5e66ccb Make catchall premium 2020-07-16 18:14:42 +02:00
Girish Ramakrishnan
1fd4d772e4 Fixup mailbox count 2020-07-15 15:47:58 -07:00
Girish Ramakrishnan
3abdbdc7c9 Add info on what cleanup backups does 2020-07-15 15:10:44 -07:00
Johannes Zellner
6d6fba873f Use browser history to allow navigation 2020-07-15 14:48:29 +02:00
Johannes Zellner
6aa8602b96 Replace action buttons with context menu 2020-07-14 23:49:14 +02:00
Johannes Zellner
240272f7ce Cleanup console.log()s 2020-07-14 19:26:24 +02:00
Johannes Zellner
3d17a33c43 Do not rely on angular trying to parse everything in the response for filemanager GET 2020-07-14 17:17:43 +02:00
Johannes Zellner
6956cfa32d Try to figure out the file language for monaco 2020-07-14 16:41:40 +02:00
Johannes Zellner
3a54e662c2 Give monaco some time to settle the ui and add tooltips 2020-07-14 16:28:46 +02:00
Johannes Zellner
823cfca3c3 Support download links 2020-07-14 16:28:18 +02:00
Johannes Zellner
9da2484bab Chrome does not have dragexit event :-/ 2020-07-14 15:32:31 +02:00
Johannes Zellner
0b50d62ef3 Fix filedrop highlight for current folder 2020-07-14 15:28:08 +02:00
Johannes Zellner
343e8e90ba Scroll file lists inside the card 2020-07-14 14:03:40 +02:00
Johannes Zellner
02dcb013ef Clear drop target highlight 2020-07-14 00:53:11 +02:00
Girish Ramakrishnan
e77d3f4fcc import and restore: add b2 provider
part of cloudron/box#508
2020-07-13 15:36:44 -07:00
Girish Ramakrishnan
7aff747b1c backups: add b2 provider
part of cloudron/box#508
2020-07-13 14:58:14 -07:00
Johannes Zellner
e97f3032cc Make monaco work 2020-07-13 23:35:49 +02:00
Johannes Zellner
ebabe29d8e Add monaco-editor resources 2020-07-13 18:58:22 +02:00
Johannes Zellner
b690c9bc95 Add filemanager chown dialog 2020-07-13 18:30:29 +02:00
Johannes Zellner
fd3034bacc Make normal files downloadable 2020-07-13 17:48:53 +02:00
Johannes Zellner
3bcef3d9c3 Fix file rename focus and initial selection 2020-07-13 17:05:01 +02:00
Johannes Zellner
da54699815 Add drag'n'drop to filemanager 2020-07-13 15:41:10 +02:00
Johannes Zellner
6b64dd52b9 Remove console.log()s 2020-07-13 12:59:50 +02:00
Girish Ramakrishnan
fb07dc2294 implement mandatory 2fa
part of #716
2020-07-10 13:10:07 -07:00
Johannes Zellner
779c3ba75b Add upload progress dialog 2020-07-10 19:15:33 +02:00
Johannes Zellner
4564e501d3 Add basic upload progress bar 2020-07-10 19:15:33 +02:00
Girish Ramakrishnan
d271d2db57 Allow users to change avatar even if profile is locked 2020-07-10 09:45:02 -07:00
Johannes Zellner
46ed0ab49e For now just add a filemanager button in console section 2020-07-10 16:19:01 +02:00
Johannes Zellner
35dfea03da Show hint if folder is empty 2020-07-10 16:12:34 +02:00
Johannes Zellner
ff5036a55b Handle filemanager error if app does not exist 2020-07-10 16:10:49 +02:00
Johannes Zellner
799892c220 Add footer and fixup main layout 2020-07-10 15:27:44 +02:00
Johannes Zellner
8b160cbbfd Share markdown angular filter via client.js 2020-07-10 15:20:53 +02:00
Johannes Zellner
48983879ab Improve new folder dialog 2020-07-10 15:11:09 +02:00
Johannes Zellner
2cecdd7f01 Add breadcrumb to filemanager 2020-07-10 15:01:56 +02:00
Johannes Zellner
4ebaa674c3 root is uid 0 2020-07-10 14:17:30 +02:00
Johannes Zellner
fb637f61f3 Add folder upload hooks 2020-07-10 14:06:32 +02:00
Johannes Zellner
805e07e65f entry.filePath -> entry.fileName 2020-07-10 14:06:32 +02:00
Johannes Zellner
049a488e08 Implement file upload 2020-07-10 14:06:32 +02:00
Johannes Zellner
afc90817cf Add file rename 2020-07-10 14:06:32 +02:00
Johannes Zellner
38f3e39258 Add directory creation 2020-07-10 14:06:32 +02:00
Johannes Zellner
c674d679bd Add file removal functionality 2020-07-10 14:06:32 +02:00
Johannes Zellner
7c2ab4e5bd Initial filemanager view 2020-07-10 14:06:32 +02:00
Girish Ramakrishnan
b86dff8601 Clarify what profile means 2020-07-09 21:56:01 -07:00
Girish Ramakrishnan
a725fc7a0b Add directory config ui 2020-07-09 21:51:51 -07:00
Girish Ramakrishnan
fbe3545153 disable profile editing based on directory config
part of cloudron/box#704
2020-07-09 17:18:41 -07:00
Girish Ramakrishnan
50b528260c account setup: Make fields readonly when profile is locked
part of cloudron/box#704
2020-07-09 15:36:52 -07:00
Girish Ramakrishnan
d2ece2b7f9 email is not used in setup account 2020-07-09 14:53:14 -07:00
Girish Ramakrishnan
f71e47aac7 Update license year 2020-07-09 09:02:26 -07:00
Johannes Zellner
8d9c4b0476 Fix eventlog crash 2020-07-06 14:53:52 +02:00
Johannes Zellner
ea1a62c3ef Finish initial round of dark moder overrides 2020-07-06 12:31:15 +02:00
Girish Ramakrishnan
2e5e459094 mail: add pagination to mailboxes UI 2020-07-05 18:21:52 -07:00
Girish Ramakrishnan
f51eccdef7 mail: Add pagination to lists UI 2020-07-05 11:55:17 -07:00
Girish Ramakrishnan
a9a9af9ef7 s3: add region field to import and restore UI
for s3 v4 compat providers like yandex

fixes cloudron/box#713
2020-07-05 10:58:20 -07:00
Girish Ramakrishnan
200122deee get all mailing lists in a single shot for now 2020-07-05 10:49:30 -07:00
Johannes Zellner
4170be7f34 Also add dark mode to modals and appstore view 2020-07-02 23:22:14 +02:00
Johannes Zellner
0be5a292c4 Initial css overrides for dark mode 2020-07-02 23:15:14 +02:00
Girish Ramakrishnan
4555586254 Login Page -> Admin Page 2020-07-01 17:05:15 -07:00
Girish Ramakrishnan
173531b767 Add note on updates 2020-07-01 14:29:02 -07:00
Johannes Zellner
412082d3ef Add univention external ldap provider to selection 2020-07-01 16:11:34 +02:00
Johannes Zellner
3b51b84308 Fix typo to show correct self signed cert support for external ldap 2020-07-01 14:59:50 +02:00
Johannes Zellner
d6d1ad98e4 Ensure info dialog collapse starts with the closed stated 2020-06-30 10:34:07 +02:00
Johannes Zellner
e8560e6905 Ensure we always order apps by fqdn 2020-06-30 10:26:00 +02:00
Girish Ramakrishnan
ccaabd6f06 Fix text for custom apps 2020-06-29 19:43:45 -07:00
Girish Ramakrishnan
9ba79cfb32 Fix infinite loop when postinstall has <br/> in it 2020-06-29 19:39:26 -07:00
Johannes Zellner
62e0e34e12 Do not hide info button for custom apps but show note about that in the dialog 2020-06-29 16:27:50 +02:00
Johannes Zellner
2d50ae4b00 Show self-signed error for external ldap setup 2020-06-26 15:18:25 +02:00
Johannes Zellner
11b567391c Allow self-signed cert for external ldap 2020-06-25 17:54:55 +02:00
Johannes Zellner
e50e488c8a Improve how sftp is mentioned in access controls 2020-06-25 10:36:25 +02:00
Girish Ramakrishnan
2a9d32309e Fix the app info dialog 2020-06-24 23:06:12 -07:00
Johannes Zellner
de0370011c Support old default autoupdate pattern 2020-06-24 12:21:27 +02:00
Girish Ramakrishnan
4a844e582e Fix wording of subscription dialog 2020-06-23 17:25:27 -07:00
Girish Ramakrishnan
d36aad4adc polish the wording 2020-06-23 09:49:11 -07:00
Girish Ramakrishnan
11240b6bbb improve wording a bit 2020-06-23 09:06:47 -07:00
Johannes Zellner
b52d3231e4 Add support for special app login page like wordpress has 2020-06-23 12:50:44 +02:00
Johannes Zellner
c9ba4ba50a Add nfs storage provider 2020-06-22 15:51:18 +02:00
Johannes Zellner
4db07b5254 Fix form error handling for mount points 2020-06-22 15:44:59 +02:00
Johannes Zellner
83688f9fd8 Add link to cifs/sshfs docs 2020-06-22 15:43:19 +02:00
Johannes Zellner
7a384846f8 Add MSP as purpose option 2020-06-19 22:04:41 +02:00
Girish Ramakrishnan
923f7f3aa8 Do not count stopped apps for memory use 2020-06-18 10:25:21 -07:00
Johannes Zellner
8e0cfcda88 Keep subscription setup screens consistent 2020-06-18 17:59:59 +02:00
Johannes Zellner
cd90af35a1 Fix active category selection if appstore search is empty 2020-06-18 17:13:20 +02:00
Johannes Zellner
d2ac8536b3 402 is a license error and also returned if no appstore account setup 2020-06-18 14:50:24 +02:00
Johannes Zellner
5100a28ff1 Remove unused features 2020-06-18 14:34:06 +02:00
Johannes Zellner
0830e9293d Disable domain remove button for dashboard domain 2020-06-18 14:06:34 +02:00
Johannes Zellner
4a981cd2e2 Show subscription setup for domain adding 2020-06-18 13:56:37 +02:00
Johannes Zellner
b1d956f7bf Show subscription setup dialog for more users 2020-06-18 12:57:12 +02:00
Johannes Zellner
75b2c7236a Use the same pattern for subscription setup in settings and users view 2020-06-18 12:29:25 +02:00
Johannes Zellner
c8278e7b24 Add subscription setup link to paid branding feature 2020-06-18 12:16:16 +02:00
Girish Ramakrishnan
dbf6520860 more newlines 2020-06-17 20:37:28 -07:00
Johannes Zellner
e593e48d40 Remove debug console.log 2020-06-17 13:16:27 +02:00
Johannes Zellner
39bccea953 Simplify footer branding html 2020-06-17 13:15:52 +02:00
Johannes Zellner
98f62eba9d Improve error case layout in setup 2020-06-17 12:43:04 +02:00
Girish Ramakrishnan
4e65728979 Better text 2020-06-16 12:31:29 -07:00
Johannes Zellner
b58ca1506e Add more information to dnssetup screen 2020-06-16 15:28:58 +02:00
Johannes Zellner
e0334b3ac8 Fix oversight to set correct view after admin setup 2020-06-16 13:23:32 +02:00
Johannes Zellner
0fa230527c Improve text layout in setupdns 2020-06-16 13:14:56 +02:00
Johannes Zellner
13c5085cb1 Fix scroll position in appstore when switching categories 2020-06-16 11:37:10 +02:00
Johannes Zellner
300a3919ab Fix appstore case without apps listed 2020-06-16 11:20:28 +02:00
Johannes Zellner
e65d946633 Swap logs and info button 2020-06-16 11:08:37 +02:00
Johannes Zellner
412bd1c1f4 Make nginx logs available in log viewer 2020-06-15 17:30:32 +02:00
Johannes Zellner
1d15fd3178 Allow to specify region for custom s3 v4 compat 2020-06-15 16:51:56 +02:00
Johannes Zellner
cb94737519 Fix bug where location change makes the app temporarily disappear 2020-06-14 16:31:06 +02:00
Johannes Zellner
01683e9383 Another attempt to fix app polling
Using taskId only to update app info leads to various outdated states if
an app task has finished. We need to also update once the task has
finished at least once. So instead of individual app polling, we can
simply rely on the all apps listing api, which we poll anyways and not
rely on the restricted properties in the main apps view.

The app configure will fetch the updated full properties now, not
relying on the Clients internal caching
2020-06-14 13:35:30 +02:00
Johannes Zellner
1960969325 Fix scrollbar quirk for chrome 2020-06-13 23:11:49 +02:00
Johannes Zellner
b49721f514 Fix toolbar with layout 2020-06-13 23:11:17 +02:00
Johannes Zellner
6876e82d64 Highligh currently selected category button instead of showing the title 2020-06-13 23:01:48 +02:00
Johannes Zellner
15a7beae57 Show selected category in dropdown button 2020-06-13 22:56:24 +02:00
Johannes Zellner
297a635613 'Recently updated' becomes 'popular' used to be 'featured' 2020-06-13 22:47:10 +02:00
Johannes Zellner
e0778c52e8 Fix appstore search margin on mobile 2020-06-13 22:43:35 +02:00
Johannes Zellner
e09b9964be Do not overlay the scrollbar 2020-06-13 22:39:37 +02:00
Johannes Zellner
1d27926220 Shorten the appstore search placeholder text 2020-06-13 22:22:44 +02:00
Johannes Zellner
7427d549cc Make the appstore view not so wide 2020-06-13 22:22:25 +02:00
Johannes Zellner
37aeb3f713 Fix border radius in dropdown 2020-06-13 22:17:38 +02:00
Johannes Zellner
7bf06da9f8 Don't add 1sec delay for search input focus 2020-06-13 22:16:16 +02:00
Johannes Zellner
b6157d58c8 Show if no app passwords are created 2020-06-12 15:46:34 +02:00
Johannes Zellner
4767fe5515 Fix z-index of appstore view to not overlay the menu 2020-06-12 15:44:13 +02:00
Johannes Zellner
750acdbcd7 Swap navbar icons to make sense 2020-06-12 15:41:23 +02:00
Johannes Zellner
29543fbc85 Use 'My Apps' everywhere 2020-06-12 15:34:54 +02:00
Johannes Zellner
05913d0ae0 Rename owner role to superadmin in UI bits 2020-06-12 15:20:18 +02:00
Johannes Zellner
a31617fcb0 Rework the appstore view 2020-06-12 15:02:41 +02:00
Johannes Zellner
ec71b622fc Ensure action items are aligned on mobile 2020-06-11 19:33:59 +02:00
Johannes Zellner
3dd659639d Add action item tooltips 2020-06-11 19:32:23 +02:00
Johannes Zellner
4aca2b64b9 Just show the rendered postinstall message as docs in info dialog 2020-06-11 15:32:24 +02:00
Johannes Zellner
4c2c27c686 move logs button into app grid 2020-06-11 15:24:18 +02:00
Johannes Zellner
429f45a09a add info dialog for apps 2020-06-10 18:00:50 +02:00
Johannes Zellner
886c668107 Show default memory requirement in app install 2020-06-10 11:31:29 +02:00
Johannes Zellner
c0df62cd5b Give more info in dns setup what is happening with the domain 2020-06-10 11:28:47 +02:00
Johannes Zellner
a8e6d727fa Add logic for email setup and invite setup forms 2020-06-09 15:39:27 +02:00
Johannes Zellner
ccf1c78cbb Add ability to develop the setup screen more easily 2020-06-09 15:03:35 +02:00
Johannes Zellner
4e25688dd9 We always require owner email 2020-06-09 14:58:03 +02:00
Johannes Zellner
3378bf7a1e Remove provider from setup 2020-06-09 14:53:43 +02:00
Johannes Zellner
2bbafb5604 Remove unused pre-fill logic in setup 2020-06-09 14:53:19 +02:00
Girish Ramakrishnan
1e82774460 set poll frequency same as the apps.js 2020-06-08 20:26:25 -07:00
Girish Ramakrishnan
dce865c3cb only fetch app when there is an active task
fixes cloudron/box#677
2020-06-08 18:01:19 -07:00
Girish Ramakrishnan
81bf84b50a re-use existing progress and message properties
now, when we go back to the app grid, it reflects immediately
2020-06-08 17:54:50 -07:00
Girish Ramakrishnan
94b6f5bffd Call postProcess in getApps 2020-06-08 17:20:18 -07:00
Johannes Zellner
5440a3b62b Ensure we send the info also for cifs 2020-06-08 18:00:04 +02:00
Johannes Zellner
24737382f9 Add CIFS storage backend 2020-06-08 17:52:56 +02:00
Johannes Zellner
5fa3215a4d Ensure additional group ldap settings are shown 2020-06-08 14:50:32 +02:00
Girish Ramakrishnan
105141be53 show warning for unstable updates
part of cloudron/box#698
2020-06-05 17:07:33 -07:00
Johannes Zellner
e19edcb67a Do not crash when retention policy is unknown to the dashboard code 2020-06-05 14:35:34 +02:00
Johannes Zellner
be0b61a628 Fix backup location display for sshfs 2020-06-05 13:03:16 +02:00
Johannes Zellner
8d79244068 Reorder backup provider list 2020-06-05 12:48:27 +02:00
Johannes Zellner
8ee66d3abf Add sshfs backup configuration 2020-06-05 12:47:33 +02:00
Johannes Zellner
fb94416b1b Give more information about ldap sync 2020-06-05 08:59:04 +02:00
Johannes Zellner
70a925b416 Handle ldap groups in group edit form 2020-06-05 08:18:40 +02:00
Johannes Zellner
959f245ce4 Show ldap indicator for groups 2020-06-04 14:11:05 +02:00
Johannes Zellner
b3eb650315 Allow to enable/disable group sync 2020-06-04 12:30:31 +02:00
Johannes Zellner
bdf7da6ef6 Mention that ldap sync is not automatic 2020-06-04 11:01:18 +02:00
Johannes Zellner
36d49b8217 Refresh 20 apps in parallel 2020-06-03 23:38:00 +02:00
Johannes Zellner
18ac61e8ab custom asyncForEach() is gone 2020-06-03 23:17:06 +02:00
Johannes Zellner
b524da23d5 custom asyncForEachParallel() is gone 2020-06-03 23:11:44 +02:00
Johannes Zellner
eeac846f5a custom asyncSeries() is gone 2020-06-03 23:08:05 +02:00
Johannes Zellner
0410ba51ca Add a proper async library 2020-06-03 22:59:17 +02:00
Johannes Zellner
ca3bf6fe5c Add a way to specify LDAP group related configs 2020-06-03 22:12:50 +02:00
Johannes Zellner
4353a05350 Just accept all image types for profile pictures
This works as we render things on the browser into a canvas which will
be stored as png on the server
2020-06-02 15:25:29 +02:00
Johannes Zellner
d2a3bb7339 Accept all image types for cloudron avatar 2020-06-02 15:14:36 +02:00
Johannes Zellner
589ee2d0c5 Always use binary byte units 2020-06-02 14:34:38 +02:00
Girish Ramakrishnan
2178dcc963 Handle already exists
part of cloudron/box#688
2020-05-30 13:33:17 -07:00
Girish Ramakrishnan
f18fdd4a46 Match default app auto-update pattern with box code 2020-05-30 10:47:00 -07:00
Girish Ramakrishnan
4352d9c698 Add note about triggering backup before stopping 2020-05-28 13:19:33 -07:00
Girish Ramakrishnan
494884595c do not allow backup, import, update in stopped state 2020-05-28 12:15:29 -07:00
Girish Ramakrishnan
b17db02f9d Remove duplicate taskId check 2020-05-28 11:54:56 -07:00
Girish Ramakrishnan
0f33a6b34b Fix display of non-appstore apps 2020-05-27 22:31:10 -07:00
Girish Ramakrishnan
231dfe70d0 remove broken disk graphs 2020-05-27 22:24:10 -07:00
Girish Ramakrishnan
79eecd8b3e OVH requires region to be set
https://docs.ovh.com/gb/en/public-cloud/getting_started_with_the_swift_S3_API/#configure-aws-client
2020-05-27 18:11:28 -07:00
Girish Ramakrishnan
ca09f64c12 force path style to true for minio
part of cloudron/box#680
2020-05-27 17:50:23 -07:00
Girish Ramakrishnan
dea1f01998 Put some ordering 2020-05-27 09:34:04 -07:00
Girish Ramakrishnan
8cfae92c24 Keep the app backup list concise
if you have even more than 10 apps, the dialog overflows and makes for bad
screenshots...
2020-05-27 09:21:16 -07:00
Johannes Zellner
989a5ba685 Fix docs link to resurrect uninstalled apps 2020-05-27 13:38:43 +02:00
Girish Ramakrishnan
a9e49d98fd Wait for sometime to refresh mail domains 2020-05-26 17:02:01 -07:00
Johannes Zellner
f66d4e34d6 Bring back backupId clipboard logic 2020-05-25 21:47:58 +02:00
Johannes Zellner
989820183c Remove unused clipboard function 2020-05-25 21:47:10 +02:00
Girish Ramakrishnan
53f0e6c7d3 Fix regression in import UI 2020-05-24 18:44:42 -07:00
Girish Ramakrishnan
1608faecea Make min period as 12 hours
because we only collect disk data twice a day
2020-05-23 12:40:41 -07:00
Girish Ramakrishnan
4260082726 rename variable to avoid name conflict 2020-05-22 14:52:23 -07:00
Girish Ramakrishnan
ca573dec91 hide the ruler 2020-05-22 12:05:23 -07:00
Johannes Zellner
3e252e1fd8 app disk usage is only collected twice a day 2020-05-22 19:48:18 +02:00
Johannes Zellner
7adc153e57 Also add swap to apps memory graph 2020-05-22 18:04:33 +02:00
Johannes Zellner
ae105d9f83 Fixup app disk usage graphs 2020-05-22 17:16:37 +02:00
Johannes Zellner
87c895bd76 Show graph minutes, since we now have a smaller resolution 2020-05-22 16:33:27 +02:00
Johannes Zellner
034b2b2ddd Add backup details dialog
When a backup contains many apps, displaying them in the main backup
list is confusing and hides most apps
2020-05-22 13:48:29 +02:00
Girish Ramakrishnan
fb5a789f55 1 monthly does not make much sense 2020-05-21 14:09:06 -07:00
Johannes Zellner
2b36a2f8cb Fix busy state for automatic updates and backups 2020-05-20 12:16:35 +02:00
Johannes Zellner
d2a81ce907 Move retention and backup schedule interval into a separate settings dialog 2020-05-19 16:13:20 +02:00
Johannes Zellner
1f0b0d7bd1 Add all wasabi regions 2020-05-19 14:52:42 +02:00
Girish Ramakrishnan
735527a0f0 rework the updates ui to show the app id
browser are hiding the URL bar and it's becoming harder to get to that id
2020-05-18 14:57:24 -07:00
Girish Ramakrishnan
4dc034dd5e better redis label 2020-05-18 14:57:13 -07:00
Girish Ramakrishnan
4bfe4079cc Show enabled/disabled with appropriate class 2020-05-17 09:11:19 -07:00
Girish Ramakrishnan
66eff3a020 Add save/restore backup config to app view 2020-05-16 11:19:47 -07:00
Girish Ramakrishnan
401c561238 Fix spacing in restore view 2020-05-16 10:32:33 -07:00
Girish Ramakrishnan
606fe87ca0 backups: show the app info in contents 2020-05-16 09:47:00 -07:00
Girish Ramakrishnan
f4775cc17c eventlog: handle update error 2020-05-15 21:37:27 -07:00
Girish Ramakrishnan
a2e941970a Show endpoint 2020-05-15 16:05:30 -07:00
Girish Ramakrishnan
c2ed909818 fixes to backups view 2020-05-15 12:48:54 -07:00
Johannes Zellner
c38c440e63 Do not throw exception if no app graph data is yet available 2020-05-15 11:49:02 +02:00
Johannes Zellner
29b0785594 Fix cleanup backup button style
Sorry for messing with the btn style names, this grew
2020-05-15 11:41:55 +02:00
Johannes Zellner
e15dcd41db Fix documentation links in restore view 2020-05-15 11:40:41 +02:00
Girish Ramakrishnan
6528461873 Add backup listing UI 2020-05-14 22:42:41 -07:00
Girish Ramakrishnan
a8f5b5d4e4 Change label to MB 2020-05-14 21:56:22 -07:00
Girish Ramakrishnan
be489744c9 Add some retention policies 2020-05-14 21:36:22 -07:00
Girish Ramakrishnan
cd0b7ed3d2 rename to keepWithinSecs 2020-05-14 16:45:52 -07:00
Girish Ramakrishnan
3ebc5c6b9d retentionSecs is now retentionPolicy 2020-05-14 16:41:55 -07:00
Girish Ramakrishnan
66ada600b7 rename retentionSecs to retentionPolicy 2020-05-14 16:27:06 -07:00
Girish Ramakrishnan
4871d5df9d hide the binds ui for this release 2020-05-14 16:09:59 -07:00
Johannes Zellner
7088e6682b Add button to upload and pre-fill backup config 2020-05-15 00:32:49 +02:00
Johannes Zellner
babe0adffb Remove secret values and add encryption flag in restore config json 2020-05-14 23:19:17 +02:00
Johannes Zellner
8f0a76ecef Make SECRET_PLACEHOLDER available globally 2020-05-14 23:04:19 +02:00
Girish Ramakrishnan
23607c303c typo 2020-05-13 22:37:11 -07:00
Girish Ramakrishnan
884b7062c9 rename version to packageVersion 2020-05-13 21:54:52 -07:00
Johannes Zellner
07650d424a Show a bit more explanation on the system memory graph 2020-05-14 00:01:42 +02:00
Johannes Zellner
218ec9c678 Plot app memory against the apps memory limit 2020-05-13 23:38:32 +02:00
Johannes Zellner
8b7c3308b3 Remove noisy dots in graphs 2020-05-13 23:35:14 +02:00
Johannes Zellner
ca9528fa4e Show app memory in system view 2020-05-13 23:34:14 +02:00
Johannes Zellner
aef625ba31 Make it explicit what the graphs show in which units 2020-05-13 23:11:30 +02:00
Johannes Zellner
e5c8f2caec Fixup disk graph summary 2020-05-13 22:53:51 +02:00
Johannes Zellner
5c06305f85 Remove unnecessary graphs header and dim top actions 2020-05-13 21:44:26 +02:00
Johannes Zellner
428893d5c5 Actually use cpu values in system graphs 2020-05-13 21:41:16 +02:00
Johannes Zellner
fc7277a542 Only show redis services line if we even have a redis 2020-05-13 20:45:03 +02:00
Johannes Zellner
c8c6b15285 Rework the system view layout 2020-05-13 20:41:56 +02:00
Girish Ramakrishnan
0a987bdec9 show lock for encrypted backups 2020-05-13 00:03:54 -07:00
Girish Ramakrishnan
ecc4fee84e restore UI fixes for encrypted backups 2020-05-12 22:30:43 -07:00
Girish Ramakrishnan
4802ecfc29 Move the download config down 2020-05-12 21:59:01 -07:00
Girish Ramakrishnan
436f415d9f Add space in save button 2020-05-12 21:43:35 -07:00
Girish Ramakrishnan
164480834a do not reset password and other settings on provider change 2020-05-12 21:41:35 -07:00
Girish Ramakrishnan
68642e056c Show error message in app view 2020-05-12 21:34:44 -07:00
Girish Ramakrishnan
9033c6e1d4 user param is not needed 2020-05-12 21:30:57 -07:00
Girish Ramakrishnan
89fc6feb5f password is not stored 2020-05-12 21:30:52 -07:00
Johannes Zellner
80dc9568ce Remove graphs page 2020-05-13 01:15:04 +02:00
Johannes Zellner
5774a7893f Improve app graph layout 2020-05-13 01:12:13 +02:00
Johannes Zellner
abd9ea9ec5 Add memory and disk graphs to app view 2020-05-13 00:42:34 +02:00
Girish Ramakrishnan
8799882f09 show warning only if location or format changed 2020-05-12 14:59:06 -07:00
Girish Ramakrishnan
f85a4878de rename backup key to password 2020-05-12 14:53:37 -07:00
Johannes Zellner
ae87213105 Show hint about required subscription for app update 2020-05-11 23:14:19 +02:00
Johannes Zellner
33bd86a2c7 Show larger app configure icon on mobile 2020-05-11 12:01:01 +02:00
Girish Ramakrishnan
2092ae22dc redis: show app fqdn instead of location 2020-05-07 09:31:31 -07:00
Johannes Zellner
aa9317069a Group redis services and have them collapsed 2020-05-05 16:36:52 +02:00
Girish Ramakrishnan
a31ea92649 Add a UI for binds 2020-05-02 11:07:36 -07:00
Girish Ramakrishnan
b8f18bdec2 Add OVH Object Storage regions 2020-04-29 13:13:01 -07:00
Johannes Zellner
704977d5f6 Avoid some flickering of apps ui while loading 2020-04-28 15:52:04 +02:00
Johannes Zellner
0757c20d59 Show inline text with backup config downlaod link to avoid prominent button 2020-04-21 11:26:52 +02:00
Girish Ramakrishnan
fa08847d6d Query aliases for each mailbox 2020-04-20 19:18:11 -07:00
Girish Ramakrishnan
f91f08628a Better variable name 2020-04-20 16:35:52 -07:00
Girish Ramakrishnan
9ebf6b06dd mail: implement aliases across domains
Part of #577
2020-04-20 16:07:23 -07:00
Johannes Zellner
357d5e46a3 Add backup config download button 2020-04-20 18:21:35 +02:00
Girish Ramakrishnan
c0f5526801 Simple add a used label for disks that contains nothing we monitor 2020-04-18 23:02:16 -07:00
Girish Ramakrishnan
861204e442 sort returns compare value and not bool 2020-04-18 22:56:12 -07:00
Girish Ramakrishnan
eb90b614ea disks: busy flag 2020-04-18 22:48:09 -07:00
Girish Ramakrishnan
d087ed2349 graph query exceeeds param limit
node.js has some built-in http header limit. when this gets exceeded,
it terminates the connection and all the queued queries fail as well
2020-04-18 21:25:10 -07:00
Girish Ramakrishnan
6ee7e75465 do not popup error dialogs 2020-04-18 18:09:54 -07:00
Girish Ramakrishnan
c2b80d7aba show icon for list 2020-04-18 17:39:13 -07:00
Girish Ramakrishnan
a95e8633cd mail list: add members only checkbox 2020-04-17 17:55:07 -07:00
Johannes Zellner
e3adbbe000 Only show subscription setup dialog when coming from free 2020-04-14 18:38:59 +02:00
Johannes Zellner
eef360673b Also hide the app header bits to avoid empty ui fragments while loading 2020-04-12 13:20:01 +02:00
Girish Ramakrishnan
36e298c758 check for updates wants more space 2020-04-11 17:46:19 -07:00
Girish Ramakrishnan
275157f27b Show logs link when updater has error 2020-04-11 17:44:04 -07:00
Girish Ramakrishnan
e776deaa3f Add note on Ext4/NFS mounts only 2020-04-09 15:49:47 -07:00
Johannes Zellner
4fc8e9b45e Ensure disable state for all form elements in backup import 2020-04-09 13:15:26 +02:00
Johannes Zellner
fe41eec7c5 Fix spacing on import button in app view 2020-04-09 13:13:14 +02:00
Johannes Zellner
d1d1d22734 Ensure we only show the tabs and content when app is loaded 2020-04-08 12:56:57 +02:00
Girish Ramakrishnan
da8b76957a sort disk contents by usage 2020-04-03 10:41:04 -07:00
Girish Ramakrishnan
305f9fd1cf show apps with automatic backups disabled 2020-04-03 10:36:51 -07:00
Girish Ramakrishnan
cd2a94ddb8 typo in variable name 2020-04-03 09:56:38 -07:00
Johannes Zellner
a2df4db504 Parse task creationTime also as utc 2020-04-02 12:19:42 +02:00
Girish Ramakrishnan
b7740a4758 do not count reserved as used 2020-04-01 22:15:03 -07:00
Girish Ramakrishnan
62c24de5c4 don't say ubuntu
https://forum.cloudron.io/topic/2228/what-type-area-of-data-makes-up-other-in-disk-usage/4
2020-04-01 18:39:05 -07:00
Girish Ramakrishnan
5ed3e67b76 graphs: ubuntu is only on the root mount point 2020-04-01 16:56:56 -07:00
Girish Ramakrishnan
c7f2314a15 add note that memory is 1024 based 2020-04-01 16:42:20 -07:00
Girish Ramakrishnan
420c7ebd67 Fixup mail sizes to be 1000 and not 1024 2020-04-01 16:29:10 -07:00
Girish Ramakrishnan
b93b1a6eec Fix prettyDiskSize to use 1000 instead of 1024 2020-04-01 16:26:47 -07:00
Girish Ramakrishnan
7d52be6e99 system: setError is not defined 2020-03-31 18:47:19 -07:00
Girish Ramakrishnan
9b1f0e394a set busy to false on error 2020-03-31 17:45:34 -07:00
Girish Ramakrishnan
1b0cb5d455 remove API calls to add/remove mail domain separately
part of cloudron/box#669
2020-03-31 10:59:01 -07:00
Girish Ramakrishnan
9b79d59d93 Add API token note 2020-03-30 22:37:25 -07:00
Girish Ramakrishnan
3e12316ea1 better wording from rob 2020-03-30 22:34:47 -07:00
Johannes Zellner
1b38c0111f Add turn to logviewer 2020-03-30 18:43:43 +02:00
Girish Ramakrishnan
5542393eb5 branding: fix login page title 2020-03-28 22:59:07 -07:00
Girish Ramakrishnan
ad48bc0ee8 mail: refresh in the background 2020-03-28 17:48:11 -07:00
Girish Ramakrishnan
ba0e5d0b59 query 1000 aliases and mailboxes
we don't handle pagination yet. it's not needed
2020-03-28 17:35:53 -07:00
Girish Ramakrishnan
1c5ff88e3c Use space instead of command for tag-input
this makes sure that email aliases wrap. if we used comma, it does not wrap
2020-03-28 16:46:19 -07:00
Girish Ramakrishnan
bf7d4a550e ftp apps can be set a per-app password
this is useful for use in ftp clients
2020-03-26 21:50:44 -07:00
Girish Ramakrishnan
324bc763fc mail eventlog is owner only 2020-03-26 18:56:32 -07:00
Girish Ramakrishnan
f9fb2ca3a1 Fixup users filter 2020-03-26 18:32:49 -07:00
Girish Ramakrishnan
b5eac7c91b email: add type filter to eventlog 2020-03-25 22:07:01 -07:00
Johannes Zellner
3c858ca0fd Only show the progress bar when task is actually active 2020-03-26 00:22:46 +01:00
Johannes Zellner
da9d634b83 Remove already hidden task stop button 2020-03-26 00:19:50 +01:00
Johannes Zellner
128704400f Hook up task cancel action 2020-03-26 00:19:06 +01:00
Johannes Zellner
a3594322bd Show task cancel button after 5min 2020-03-26 00:16:23 +01:00
Girish Ramakrishnan
fe4b3d5f1d branding: use separate css 2020-03-25 08:56:56 -07:00
Johannes Zellner
da08da2b54 Use footer info from settings to show empty on default 2020-03-25 07:00:53 +01:00
Johannes Zellner
5deb5f79bd Ensure textareas don't overflow horizontally on resize 2020-03-25 06:58:38 +01:00
Johannes Zellner
9f0d694f0a Prevent angular crash when adding already existing tag 2020-03-25 06:51:42 +01:00
Johannes Zellner
4153fb7d1e Use theme for tag-input tags 2020-03-25 06:51:42 +01:00
Johannes Zellner
6994ec0f03 Allow to click anywhere in tag-input for focus 2020-03-25 06:51:42 +01:00
Johannes Zellner
e1af60cfa9 Fix tag-input with flex layout to better overflow 2020-03-25 06:51:42 +01:00
Johannes Zellner
7bcec61e6d Make tag-input support dirty handling on tag deletion 2020-03-25 06:51:42 +01:00
Girish Ramakrishnan
dde287f05d avatar size is 128px 2020-03-24 13:04:12 -07:00
Girish Ramakrishnan
27fc37e55c descriptive mail eventlog 2020-03-20 13:05:58 -07:00
Girish Ramakrishnan
ad901760f6 move footer to separate section 2020-03-19 23:28:22 -07:00
Girish Ramakrishnan
973029865e Branding UI changes 2020-03-19 22:59:30 -07:00
Girish Ramakrishnan
52e4fedd16 fieldset must be inside form 2020-03-19 19:26:19 -07:00
Girish Ramakrishnan
b81ba49370 CPU shares is a percent 2020-03-19 17:15:08 -07:00
Girish Ramakrishnan
39a0f93f69 add cpuShares 2020-03-19 17:11:51 -07:00
Girish Ramakrishnan
53cb83eacc eventlog: add start/stop/restart logs 2020-03-19 17:05:50 -07:00
Girish Ramakrishnan
b307d278b0 mailboxName should have lower priority than location change 2020-03-19 16:48:46 -07:00
Girish Ramakrishnan
14348eba38 Move name and logo into branding page 2020-03-18 22:11:33 -07:00
Girish Ramakrishnan
cead5b74ae if ldap is noop, show a message 2020-03-18 21:44:55 -07:00
Girish Ramakrishnan
2e2a945f7c add custom apps link 2020-03-18 21:25:19 -07:00
Girish Ramakrishnan
0e3ae2b450 add new branding view 2020-03-18 17:53:50 -07:00
Girish Ramakrishnan
19e2df65ca backups: hide configure button for non-owners 2020-03-18 17:24:20 -07:00
Girish Ramakrishnan
565d715a66 remove extra break 2020-03-18 13:43:15 -07:00
Girish Ramakrishnan
abe6f55aa6 gcdns: fix add/save 2020-03-17 22:51:47 -07:00
Girish Ramakrishnan
c278d0c5d4 bring back reboot button 2020-03-17 22:26:01 -07:00
Girish Ramakrishnan
a7e2c74158 more linode warnings 2020-03-13 12:05:47 -07:00
Girish Ramakrishnan
d84900d601 linode: dns frontend 2020-03-13 11:32:30 -07:00
Girish Ramakrishnan
fdda28d67f lint 2020-03-12 17:07:17 -07:00
Johannes Zellner
e00dccaa7c Set autofocus in setupdns view 2020-03-09 16:08:55 -07:00
Johannes Zellner
08c1a33362 Ensure tabindex works better in setupdns 2020-03-09 16:07:10 -07:00
Johannes Zellner
31e3c8da30 Update feature test comments 2020-03-09 14:19:12 -07:00
Girish Ramakrishnan
62a6095ed7 remove obsolete mail events 2020-03-09 13:49:22 -07:00
Johannes Zellner
4c2c0e2b95 Fix login footer 2020-03-09 13:32:49 -07:00
Johannes Zellner
d36e4937d4 Mark backup folder in backup config as required 2020-03-09 13:28:06 -07:00
Johannes Zellner
fea48e8220 Do not show noop provider for external ldap 2020-03-09 13:04:44 -07:00
Girish Ramakrishnan
637a59136b email size can be 0 unlike disk size 2020-03-07 16:09:44 -08:00
Johannes Zellner
385d275f59 Another attempt on fixing the user edit with roles business 2020-03-07 14:05:58 -08:00
Johannes Zellner
e240ac1fa5 Disable subscription UI in demo mode 2020-03-07 10:00:32 -08:00
Johannes Zellner
4f020c1ec7 Do not allow the admin to edit the owner 2020-03-07 02:25:57 -08:00
Johannes Zellner
237decb81e Dont not show all those mail events 2020-03-07 02:21:21 -08:00
Johannes Zellner
1c98cba36d Show busy state on webtoken logout 2020-03-07 01:59:38 -08:00
Girish Ramakrishnan
21fb815adc inform user that it takes a while 2020-03-07 00:15:19 -08:00
Girish Ramakrishnan
6b729bd9b5 typo 2020-03-07 00:07:36 -08:00
Girish Ramakrishnan
a7ee869c8e Remove the source from eventlog 2020-03-06 23:11:26 -08:00
Girish Ramakrishnan
bbbe3cc92f spam-selectdb is only new users now 2020-03-06 22:56:00 -08:00
Girish Ramakrishnan
c1770c8d90 spam filter seeding begin/end 2020-03-06 22:44:30 -08:00
Girish Ramakrishnan
71b7e68937 5 rows 2020-03-06 20:43:57 -08:00
Girish Ramakrishnan
dbca88829a Just put a hyphen 2020-03-06 20:38:48 -08:00
Girish Ramakrishnan
ebd365a156 Alt message for spam-selectdb when users is empty 2020-03-06 19:34:51 -08:00
Johannes Zellner
698a20396c Always open markdown links in new tab 2020-03-06 19:11:39 -08:00
Johannes Zellner
ffc2507362 Ensure we trim the footer content 2020-03-06 18:21:34 -08:00
Johannes Zellner
038d6fe2c3 For immediate update of the footer and cloudron name we have to rely on config here 2020-03-06 17:57:38 -08:00
Johannes Zellner
34c8baa744 Pick public info from status object 2020-03-06 17:57:38 -08:00
Girish Ramakrishnan
52a8081d0f no padding for footer links 2020-03-06 17:28:14 -08:00
Johannes Zellner
23813aa346 Make admins got to app configure when app task is active 2020-03-06 16:41:17 -08:00
Johannes Zellner
7d034a4b0b Show apptask progress label directly 2020-03-06 16:25:21 -08:00
Johannes Zellner
7e41f2ef35 Further improve email eventlog 2020-03-06 14:42:08 -08:00
Johannes Zellner
f49dd31804 Reduce columns in mail event log 2020-03-06 13:35:34 -08:00
Johannes Zellner
cf9e116388 Pipe backend error message on user deletion 2020-03-06 13:15:57 -08:00
Johannes Zellner
2a8d6f37c4 Disable roles the current user cannot assign 2020-03-06 12:33:36 -08:00
Johannes Zellner
d5930fd859 Show error on user deletion if not allowed 2020-03-06 12:23:50 -08:00
Johannes Zellner
f1e0167e1b Only fetch subscription for owners 2020-03-06 11:42:27 -08:00
Johannes Zellner
2d74c62054 Hide subscription status display for non-owners 2020-03-06 11:40:30 -08:00
Johannes Zellner
9249f28e68 Replace default logos with our own icon in colors 2020-03-06 11:21:32 -08:00
Girish Ramakrishnan
1273dbde76 Better icon for owner 2020-03-06 11:14:25 -08:00
Girish Ramakrishnan
966960c64b cpu share: add step and better ticks 2020-03-06 10:40:21 -08:00
Johannes Zellner
19e2919d5b Add reconnect handler and make reboot state better reflected in the notfications 2020-03-06 02:38:21 -08:00
Girish Ramakrishnan
1555b143a9 strip .tar.gz/.tar.gz.enc ending for all backup ids 2020-03-06 02:31:26 -08:00
Girish Ramakrishnan
f1c2679137 typo 2020-03-06 02:09:44 -08:00
Girish Ramakrishnan
49e9bd3ca6 Add linode to restore UI 2020-03-06 01:47:07 -08:00
Johannes Zellner
6d9fe0410d Use branded footer in login and account setup pages 2020-03-06 01:44:08 -08:00
Girish Ramakrishnan
11419365ca Move help text after footer heading 2020-03-06 01:18:51 -08:00
Johannes Zellner
a9767ac29a Make use of new support feature flag 2020-03-06 01:08:55 -08:00
Johannes Zellner
efbf78ed00 Improve notification dismiss action button 2020-03-05 21:14:19 -08:00
Johannes Zellner
1f0965fdf6 Implement specific reboot notification action 2020-03-05 21:14:19 -08:00
Johannes Zellner
0ea2f48d94 Avoid tooltip flickering on mouse events 2020-03-05 21:14:19 -08:00
Johannes Zellner
547b351f40 Move setup subscription button in app install dialog to the right 2020-03-05 21:14:19 -08:00
Girish Ramakrishnan
c9f0166c3d Other -> Ubuntu 2020-03-05 21:05:31 -08:00
Johannes Zellner
0de106f23d Disable app access submit when no group or user is selected 2020-03-05 20:15:17 -08:00
Girish Ramakrishnan
854281417d various app password ui fixes 2020-03-05 20:11:07 -08:00
Johannes Zellner
4d35fde8ba remove extra br for normal users in profile page 2020-03-05 20:10:01 -08:00
Johannes Zellner
754c9eff9e Rename system to system info 2020-03-05 18:28:11 -08:00
Johannes Zellner
aca5a876d8 Move disk usage to system 2020-03-05 18:28:11 -08:00
Girish Ramakrishnan
7a817319ba Better text for password 2020-03-05 17:52:28 -08:00
Johannes Zellner
f01ed81472 Remove feature to show old notifications 2020-03-05 17:31:03 -08:00
Johannes Zellner
942c755a5b Some better avatar overlay 2020-03-05 17:28:25 -08:00
Johannes Zellner
392da50f2c Show cloudron name in account setup view 2020-03-05 17:08:10 -08:00
Johannes Zellner
dffaaf067d Show cloudron name in login view 2020-03-05 17:03:32 -08:00
Girish Ramakrishnan
d663930d66 Fix casing of password changed 2020-03-05 17:02:52 -08:00
Girish Ramakrishnan
9291b6a489 Fixup text 2020-03-05 16:54:39 -08:00
Johannes Zellner
53abc1171e Remove toplevel doc links 2020-03-05 16:31:41 -08:00
Johannes Zellner
7f95e11af1 Do not send active or role attributes for own user while edit 2020-03-05 16:23:27 -08:00
Johannes Zellner
256676cb9d Allow to refresh users without showing busy state 2020-03-05 16:14:03 -08:00
Johannes Zellner
6180c0dc69 Add help button for user roles 2020-03-05 16:05:11 -08:00
Johannes Zellner
e44ae4a0a0 Swap role and group in user add dialog 2020-03-05 16:04:05 -08:00
Johannes Zellner
8221e6a148 User permissions property is gone 2020-03-05 15:57:42 -08:00
Girish Ramakrishnan
6603c48fd9 Fixup text 2020-03-05 15:49:43 -08:00
Girish Ramakrishnan
3665d7cab7 Add linode to import UI 2020-03-05 11:24:42 -08:00
Johannes Zellner
27da68dc4b Fix gcdns provider configuration 2020-03-04 18:53:53 -08:00
Johannes Zellner
5695d555e5 Prevent eventlog filter from crashing 2020-03-04 15:07:35 -08:00
Johannes Zellner
08a6ad8bd3 Fix long-term hover bug in cog icon 2020-03-04 14:33:33 -08:00
Johannes Zellner
66a95fb130 Eventlog is available to all again
This is not actually the same as a business type audit log
2020-03-04 14:17:31 -08:00
Johannes Zellner
6eca1dfb83 Revert "Rename eventlog to audit trail"
This reverts commit fef854580d.
2020-03-04 14:15:32 -08:00
Girish Ramakrishnan
fb5e2ef671 Throw error object and not a string 2020-03-03 11:07:56 -08:00
Girish Ramakrishnan
3cb15f0097 linode: add frankfurt 2020-03-02 20:03:02 -08:00
Johannes Zellner
7367932f2c Reword tooltip when adding users is blocked 2020-02-28 20:51:56 +01:00
Johannes Zellner
fef854580d Rename eventlog to audit trail 2020-02-28 19:43:09 +01:00
Johannes Zellner
102a0a40a6 Make all only in paid version displays the same 2020-02-28 19:27:19 +01:00
Girish Ramakrishnan
0515b650ca mail from validation: add busy indicator 2020-02-27 10:47:14 -08:00
Girish Ramakrishnan
3c7e28c768 mail relay: wait for mail container to restart 2020-02-27 10:42:33 -08:00
Girish Ramakrishnan
e528cf5692 Fix crash in statusOk computation 2020-02-27 10:37:21 -08:00
Girish Ramakrishnan
dfe2eee0b9 Fix crash when no mailbox usage present 2020-02-27 10:11:00 -08:00
Girish Ramakrishnan
60f42e342b better message 2020-02-27 10:08:41 -08:00
Johannes Zellner
958f738820 Fix app mailbox form state 2020-02-27 16:04:11 +01:00
Johannes Zellner
2d2989a425 Fix add user button state 2020-02-27 15:10:56 +01:00
Johannes Zellner
3c3370b929 Check mail state depending on relay used or not 2020-02-27 13:27:45 +01:00
Girish Ramakrishnan
90b22196b1 add linode objectstorage backend 2020-02-26 10:12:26 -08:00
Johannes Zellner
e7b9c2d294 Disable role selection for Cloudrons with user restriction 2020-02-26 16:48:51 +01:00
Johannes Zellner
a3830d23e8 Move Email menu entry back to old place 2020-02-26 15:03:17 +01:00
Johannes Zellner
d83eb32b6e Remove code for now dead spaces feature 2020-02-26 13:51:00 +01:00
Girish Ramakrishnan
303c55dbba Show busy indicator in reboot button 2020-02-25 14:58:29 -08:00
Girish Ramakrishnan
afde058a85 capitalize 2020-02-24 11:58:25 -08:00
Johannes Zellner
14397cab96 Hide external ldap view for non-admins 2020-02-24 18:01:42 +01:00
Johannes Zellner
a78eec79a8 Fix users view for user manager role 2020-02-24 17:49:36 +01:00
Johannes Zellner
7ce4effc2d Remove all .admin usage 2020-02-24 17:29:20 +01:00
Johannes Zellner
2674160acc Fix error form state when adding a new domain 2020-02-24 17:29:10 +01:00
Johannes Zellner
f1c951c997 User creation and edit apis don't take admin flag anymore 2020-02-24 17:15:50 +01:00
Johannes Zellner
a17d810fea Only auto-login the owner for subscription setup 2020-02-24 15:10:21 +01:00
Johannes Zellner
4b1cb76eaf Remove unused angular filter for oauth clients 2020-02-24 14:36:52 +01:00
Johannes Zellner
6b89b2be5e Remove tokens view for good 2020-02-24 14:36:35 +01:00
Johannes Zellner
92bfda9028 Remove admin usage in main view 2020-02-24 14:34:12 +01:00
Johannes Zellner
728d50461f Fix app password usage for non-admins 2020-02-24 13:29:47 +01:00
Johannes Zellner
3f92204de5 Fix indentation 2020-02-24 13:23:47 +01:00
Johannes Zellner
0e6c9177f0 Migrate most of .admin usage 2020-02-24 12:56:13 +01:00
Johannes Zellner
3c0e674ee5 Fixup user list icons and set default add to 'user' 2020-02-24 12:22:07 +01:00
Johannes Zellner
15c9052912 Move role selector up 2020-02-22 17:51:30 +01:00
Girish Ramakrishnan
7061880104 derive admin flag from roles 2020-02-21 16:55:39 -08:00
Johannes Zellner
6f12cde2e8 Add user role dropdown
Also pass `role` instead of `permissions`
Once done ng-disable/ng-hide based on userInfo.role
2020-02-21 21:15:54 +01:00
Johannes Zellner
52d454276d Use new subscription setup flow for app install dialog 2020-02-21 14:07:46 +01:00
Johannes Zellner
81fb4ab435 Add appstore accessToken to subscription setup links 2020-02-21 13:08:20 +01:00
Girish Ramakrishnan
af8bb1f0e8 Add email view summary 2020-02-20 12:36:05 -08:00
Girish Ramakrishnan
5fd575a217 display usage info 2020-02-20 12:36:05 -08:00
Johannes Zellner
1ef5fd1a0f Only show support ticket and remote support UI for paid plans 2020-02-19 14:19:46 +01:00
Girish Ramakrishnan
932de7dba7 mail: display source 2020-02-18 21:31:35 -08:00
Girish Ramakrishnan
84310336bd Add search param 2020-02-18 09:46:52 -08:00
Johannes Zellner
aba233c74a Add permissions UI 2020-02-17 14:05:26 +01:00
Johannes Zellner
016e2b375d Add commented test block for features 2020-02-14 20:45:23 +01:00
Johannes Zellner
732b1ae0de Improve business plan placeholders 2020-02-14 20:45:23 +01:00
Girish Ramakrishnan
ae0c0f957e Show 20 per page by default 2020-02-14 09:04:57 -08:00
Johannes Zellner
4283046e76 Add footer branding configuration 2020-02-14 15:34:44 +01:00
Johannes Zellner
0a126a15ba Add branding section with footer configuration 2020-02-14 15:22:16 +01:00
Johannes Zellner
21e7190b72 Remove subscription setup modal, instead open cloudron.io directly 2020-02-14 14:16:04 +01:00
Girish Ramakrishnan
9fcd049bdc domain.locked is gone 2020-02-13 21:15:09 -08:00
Girish Ramakrishnan
bc31ea5eb7 mail: move the next/prev page buttons to header
aligns with other ui like activity and mailbox
2020-02-13 09:12:59 -08:00
Johannes Zellner
35dd92f54e If maxUserCount is not set allow all 2020-02-13 17:09:42 +01:00
Johannes Zellner
0a29f92384 Use features from config object 2020-02-13 16:34:37 +01:00
Johannes Zellner
a13414ddb9 Add feature object and show/hide elements accordingly 2020-02-13 15:30:31 +01:00
Johannes Zellner
3dd0566f48 fix empty eventlog label display 2020-02-13 13:03:57 +01:00
Johannes Zellner
8b3bc28120 add eventlog refresh button 2020-02-13 12:01:47 +01:00
Johannes Zellner
8051d6ba48 Add pagination to eventlog 2020-02-13 12:01:47 +01:00
Girish Ramakrishnan
43b49ef4c9 cleanup mail eventlog ui 2020-02-12 23:30:15 -08:00
Johannes Zellner
8ca51f1877 Show last 5 new apps 2020-02-12 23:24:33 +01:00
Johannes Zellner
59c5f22dbd Show mail eventlog type also as icon 2020-02-12 15:52:39 +01:00
Johannes Zellner
a92dc1ad73 Make eventlog display work 2020-02-12 15:37:05 +01:00
Johannes Zellner
fd72a00cfb Fixup raw email logs button layout 2020-02-12 14:52:38 +01:00
Johannes Zellner
e9d10d6f2f Add action button to send test mail in global mail view 2020-02-12 14:51:06 +01:00
Johannes Zellner
fa630a6cb5 Show mail status per domain in overview 2020-02-12 14:10:21 +01:00
Girish Ramakrishnan
135548a03b mail: add js code to get eventlog 2020-02-11 22:07:58 -08:00
Girish Ramakrishnan
304c930f95 Fix comment 2020-02-11 21:27:16 -08:00
Johannes Zellner
48991e22b1 Move Email menu entry toplevel
Not sure about that, but lets see until we make a release
2020-02-11 21:10:36 +01:00
Johannes Zellner
132a375347 Separate emails and email view 2020-02-11 21:06:34 +01:00
Johannes Zellner
1392abe2c0 Fix password length error message 2020-02-11 16:45:20 +01:00
Johannes Zellner
fde07fda55 Passwords must be between 8 and 256 characters 2020-02-11 15:33:40 +01:00
Johannes Zellner
b9c4928949 Add missing token description 2020-02-11 09:54:57 +01:00
Girish Ramakrishnan
d2025d5ddf Show logs in browser timezone 2020-02-10 14:13:26 -08:00
Girish Ramakrishnan
113ae1cfa5 add note on utc to tz conversion 2020-02-10 13:49:59 -08:00
Girish Ramakrishnan
a0f2039fd4 eventlog: wrong tooltip 2020-02-10 13:49:59 -08:00
Johannes Zellner
8442f80641 Ensure app password and api token tables have same layout 2020-02-08 01:25:05 +01:00
Johannes Zellner
f10fafd038 Fix token expiration display 2020-02-08 01:23:29 +01:00
Johannes Zellner
c85f48a9e9 Remove console.log() 2020-02-07 22:38:51 +01:00
Girish Ramakrishnan
a1c487b29d Add note that old backups have to be cleaned up manually 2020-02-07 13:20:19 -08:00
Johannes Zellner
62562f051c Add token add api and separate api tokens from rest 2020-02-07 21:40:55 +01:00
Girish Ramakrishnan
467edb6b32 Error handing fixes for import 2020-02-07 11:23:34 -08:00
Johannes Zellner
1970641001 Implement revoke tokens 2020-02-07 20:00:10 +01:00
Girish Ramakrishnan
72b9384902 Use tooltips instead of overlays as hint when app is busy 2020-02-07 10:43:00 -08:00
Girish Ramakrishnan
8600019079 Fixes to import UI 2020-02-07 10:22:52 -08:00
Girish Ramakrishnan
49975a521b Add UI to import backup 2020-02-07 09:42:27 -08:00
Johannes Zellner
b289db6879 Actually fix deployment tarball apiOrigin 2020-02-07 18:27:39 +01:00
Johannes Zellner
8ae6bf832a List all tokens in profile 2020-02-07 17:03:14 +01:00
Johannes Zellner
3efe7eb85d Add token route wrapper 2020-02-07 16:42:35 +01:00
Johannes Zellner
46635e1992 Remove oauth client wrapper 2020-02-07 16:36:10 +01:00
Johannes Zellner
e8abe35bc5 Fix gulpfile for release tarball creation 2020-02-07 16:31:35 +01:00
Johannes Zellner
7596881464 Remove oauth from gulp and ejs templates 2020-02-07 13:41:10 +01:00
Girish Ramakrishnan
ba36f05182 Just hide the table altogether when no backup 2020-02-06 16:11:25 -08:00
Girish Ramakrishnan
be1874839e get support configuration via REST API 2020-02-05 14:15:46 -08:00
Johannes Zellner
5996ea1ba3 Remove unused code 2020-02-05 17:26:48 +01:00
Johannes Zellner
9283537efc Implement account setup view logic 2020-02-05 16:34:15 +01:00
Johannes Zellner
9dbdad324a Use invite link generated on the server to stay in sync 2020-02-05 15:53:19 +01:00
Johannes Zellner
39faf2e55c Add new account setup page 2020-02-05 15:05:34 +01:00
Johannes Zellner
19b5253708 Remove now unused login callback 2020-02-05 12:54:14 +01:00
Johannes Zellner
3f2b59e67f Ensure we have proper login app entry points 2020-02-05 11:54:31 +01:00
Girish Ramakrishnan
4bc1d5cd4a uiSpec is dead 2020-02-04 13:17:26 -08:00
Girish Ramakrishnan
689349887d custom: remove support section 2020-02-04 13:07:20 -08:00
Girish Ramakrishnan
e7a1b5d40b custom: remove subscription.configurable 2020-02-04 12:59:16 -08:00
Girish Ramakrishnan
8f997ee724 Remove usage of config.uiSpec.domains 2020-02-04 12:55:51 -08:00
Girish Ramakrishnan
99bce50b4d custom: remove backups.configurable 2020-02-04 12:49:36 -08:00
Johannes Zellner
db166c4f29 Be at least consistent with casing 2020-02-04 18:41:53 +01:00
Johannes Zellner
4a8a52a0c7 Implement setting new password 2020-02-04 18:36:24 +01:00
Johannes Zellner
0762b337b8 Fixup password reset page 2020-02-04 15:53:56 +01:00
Johannes Zellner
a50fa9bcf4 Add password reset feature in login view 2020-02-04 15:27:35 +01:00
Johannes Zellner
1b0c5f8771 Remove unnecessary login page navbar 2020-02-04 15:00:56 +01:00
Johannes Zellner
92be875a2f Use standalone login screen instead of OAuth 2020-02-04 14:46:19 +01:00
Girish Ramakrishnan
d5e4453f15 app passwords: add ui 2020-02-01 18:49:29 -08:00
Johannes Zellner
c9e43ed295 Add basic ticks to cpu share slider 2020-02-01 16:30:32 +01:00
Girish Ramakrishnan
63610f04ec Show backup disk usage 2020-01-31 14:47:04 -08:00
Girish Ramakrishnan
e0db4fce6e Better purpose 2020-01-30 21:53:43 -08:00
Johannes Zellner
1f4b6f4a42 Update purpose list 2020-01-30 18:33:44 +01:00
Johannes Zellner
300ab191fe Add cloudron purpose to appstore login/signup flow 2020-01-30 15:36:05 +01:00
Girish Ramakrishnan
0315ae511b do not allows < 2 2020-01-28 22:44:08 -08:00
Girish Ramakrishnan
e336c4405d Rename Custom data dir to Storage 2020-01-28 22:33:38 -08:00
Girish Ramakrishnan
2a4d9c0ba6 ui for cpu shares 2020-01-28 22:32:58 -08:00
Girish Ramakrishnan
4a29fa93c5 services: use memorySwap to be consistent with the app memory UI
the memory limit sliders take the total memory (memory + swap).
2020-01-28 13:38:05 -08:00
Girish Ramakrishnan
c5d14195d6 Remove broken help link and add explanation 2020-01-28 10:09:25 -08:00
Girish Ramakrishnan
09d34f5843 service: fix broken memory sliders 2020-01-28 09:37:25 -08:00
Girish Ramakrishnan
7432610629 lint 2020-01-24 17:18:00 -08:00
Girish Ramakrishnan
d50d555372 eventlog: mailbox and list update events 2020-01-24 17:18:00 -08:00
Johannes Zellner
f0859291fc Actually shorten the memory limit description 2020-01-23 08:10:33 +01:00
Johannes Zellner
1eeba899f0 Mention how memory limit is allocated inline 2020-01-23 08:08:03 +01:00
Johannes Zellner
36653c10dc Fixup profile picture selection to only allow really changed pictures to be submitted 2020-01-20 19:01:41 +01:00
Johannes Zellner
008ac68ecb Also search within app label if any 2020-01-20 14:38:41 +01:00
Johannes Zellner
36b8b0e6a1 Fix user select in group add/edit dialogs
We have to depend on all users not just the paginated ones
The selection does not need all information from the user so we are good
2020-01-09 16:21:22 +01:00
Johannes Zellner
42066e20ed Make timezone settings much beautiful 2020-01-08 21:41:06 +01:00
Johannes Zellner
57aa93eb84 Do not show UTC offset in select 2020-01-08 20:35:00 +01:00
Johannes Zellner
c6c51bd319 Generate timezones.js with gulp 2020-01-08 17:22:07 +01:00
Johannes Zellner
2fa9c89246 Show UTC time offset and use better dropdown with search 2020-01-08 13:01:31 +01:00
Girish Ramakrishnan
840b326187 Reword 2020-01-07 18:30:54 -08:00
Johannes Zellner
1fef7130fb Add time zone settings ui 2020-01-07 21:41:45 +01:00
Johannes Zellner
2328dd1d58 Ensure outdated selected app tags are cleared if not available anymore 2020-01-07 12:17:40 +01:00
Johannes Zellner
dbba99eee5 Only show app search field if more than 10 apps are installed 2020-01-07 11:13:22 +01:00
Johannes Zellner
f07e0bf967 Fix typo to actually restart an app 2020-01-06 16:38:48 +01:00
Johannes Zellner
c548f572df fixup linter errors 2020-01-06 16:38:33 +01:00
Johannes Zellner
129f67c9f8 Make app view tags and domain filter persistent
This is only stored in the browser's localStorage to survive a reload,
but is not stored on the server to be preserved across different clients
2020-01-06 16:23:33 +01:00
Johannes Zellner
bc75a8e7b8 Add search field in apps view 2020-01-06 15:27:31 +01:00
Girish Ramakrishnan
e4f16ae520 2020: happy new year 2020-01-02 16:56:42 -08:00
Girish Ramakrishnan
4df34c724e cloudflare: add token type selector 2020-01-01 16:49:17 -08:00
Girish Ramakrishnan
1960fc8606 cloudflare: send tokenType 2019-12-31 17:03:29 -08:00
Girish Ramakrishnan
aaae8a84f6 show warning when enabling email with cloudflare 2019-12-31 17:03:25 -08:00
Girish Ramakrishnan
1dca3c17a4 debug mode apps skip the health check 2019-12-24 11:08:17 -08:00
Johannes Zellner
dfd31722fc Ensure webServerOrigin is actually set 2019-12-23 17:20:28 +01:00
Girish Ramakrishnan
f5fd75f4fa Set busy indicator to false in refresh callbacks 2019-12-22 17:10:04 -08:00
Girish Ramakrishnan
24d1c2d63a Fix error state handling
Do not disable views in error state. Many actions like display work just fine.
Also, people want to restore etc but all this is disabled.
2019-12-20 19:04:51 -08:00
Girish Ramakrishnan
da191d62cc Show console view in error state for the logs
disable the terminal button accordingly
2019-12-20 15:55:57 -08:00
Girish Ramakrishnan
e8bc6e564d Rename the repair button 2019-12-20 15:38:40 -08:00
Girish Ramakrishnan
95f3158bb4 Rename paused to recovery mode 2019-12-20 11:53:06 -08:00
Girish Ramakrishnan
f7cc5be173 Wrap the tooltip 2019-12-20 11:42:41 -08:00
Girish Ramakrishnan
adc078a5cb Add a restart button in recovery section 2019-12-20 11:27:32 -08:00
Girish Ramakrishnan
3f3ec9ef9a Fix usage of config.memory 2019-12-20 10:02:01 -08:00
Girish Ramakrishnan
7d70060962 debug view is now called repair 2019-12-19 21:53:53 -08:00
Girish Ramakrishnan
4507496d3d Show endpoint for minio 2019-12-19 11:05:40 -08:00
Girish Ramakrishnan
445dcc24df Fix documentation links 2019-12-18 14:29:42 -08:00
Girish Ramakrishnan
78205c9a13 Add gravatar link 2019-12-17 15:05:01 -08:00
Girish Ramakrishnan
3a0f7e0602 Disable console view when app is in error state 2019-12-17 10:24:42 -08:00
Girish Ramakrishnan
fab23ee595 backup: compact the view 2019-12-17 10:15:38 -08:00
Girish Ramakrishnan
d285c5a679 Make the buttons primary instead of success 2019-12-17 10:03:04 -08:00
Girish Ramakrishnan
6d079b9349 hide the cancel task button
we considered putting it in the progress bar, but we don't want to
encourage it's use
2019-12-17 09:43:40 -08:00
Johannes Zellner
c291f744e7 Ensure min-height for app view tabs matches the tabs 2019-12-17 14:56:15 +01:00
Girish Ramakrishnan
f0730f595f rework the repair view 2019-12-16 19:24:32 -08:00
Girish Ramakrishnan
d175d06b35 debug is now repair 2019-12-16 17:45:40 -08:00
Girish Ramakrishnan
119969634e Fix display of start/stop button 2019-12-16 16:27:24 -08:00
Girish Ramakrishnan
b67c09a4c1 Use refreshApp in onReady 2019-12-16 16:22:29 -08:00
Girish Ramakrishnan
8a850ecc5b Remove usage of trackBackupTask 2019-12-16 16:17:54 -08:00
Girish Ramakrishnan
f752ed3927 remove superfluous trackBackuptask 2019-12-16 16:17:13 -08:00
Girish Ramakrishnan
866a7480bc make refreshApp take appId 2019-12-16 16:08:49 -08:00
Girish Ramakrishnan
55ae8404cd remove duplicate show call
this is already done in $scope.setView
2019-12-16 15:54:03 -08:00
Girish Ramakrishnan
71e9d7c4af evenlog: ssh and ticket 2019-12-16 14:12:59 -08:00
Girish Ramakrishnan
580556cab6 app: add console section 2019-12-16 13:34:21 -08:00
Girish Ramakrishnan
bcb6182be3 lint: indent 2019-12-16 12:54:24 -08:00
Girish Ramakrishnan
c865aaed6f remove unused cert code 2019-12-16 12:53:51 -08:00
Girish Ramakrishnan
37c23fa187 avatar: fix broken image for emails with no gravatar
this was because we blindly add ? to a url which already has query params
2019-12-13 13:50:50 -08:00
Johannes Zellner
2eeb99e869 Improve avatar settings dialog 2019-12-12 15:34:26 +01:00
Johannes Zellner
fd528edfed Use cloudron/memory api instead of removed config.memory 2019-12-12 12:13:06 +01:00
Girish Ramakrishnan
df6a645600 typo 2019-12-11 15:13:46 -08:00
Girish Ramakrishnan
d1515f8f64 Handle 422 for invalid provider token 2019-12-11 15:01:51 -08:00
Girish Ramakrishnan
9fb85311b9 Various spacing issues 2019-12-11 14:43:54 -08:00
Girish Ramakrishnan
aed949e221 Fix layout of instance id 2019-12-11 14:18:12 -08:00
Girish Ramakrishnan
5b29f48bfd Fix bug where providerToken is sent in config 2019-12-11 13:58:29 -08:00
Girish Ramakrishnan
5bfb48b863 ami: set default to route53 2019-12-11 12:57:46 -08:00
Girish Ramakrishnan
9536fafd53 eventlog: mail list removal typo 2019-12-11 10:00:00 -08:00
Johannes Zellner
4434d59b09 Remove dns setup Cloudron logo 2019-12-10 17:44:22 +01:00
Girish Ramakrishnan
060fe39f2e Fix repair route path 2019-12-06 11:44:33 -08:00
Girish Ramakrishnan
a5b14e8d68 Fix repair
Call appropriate routes based on the error state
2019-12-06 10:13:30 -08:00
Johannes Zellner
165ad229e2 Add support to upload custom profile avatar 2019-12-02 18:03:41 +01:00
Girish Ramakrishnan
beb3117bfc Remove dead certificate code 2019-11-23 17:50:54 -08:00
Girish Ramakrishnan
e1d462aa42 Fix taskName 2019-11-23 17:49:13 -08:00
Girish Ramakrishnan
bd15ef7768 @ sign missing for email address 2019-11-22 14:41:17 -08:00
Girish Ramakrishnan
646f669c14 Display swap 2019-11-21 14:00:37 -08:00
Johannes Zellner
682eb8d6e5 Improve external ldap auto creation text and move to bottom 2019-11-20 22:42:32 +01:00
Johannes Zellner
00b0a21c78 Add ldap user autocreate settings 2019-11-20 10:46:26 +01:00
Johannes Zellner
c7a5d295ec Add new apps category in appstore view 2019-11-18 22:43:33 +01:00
Girish Ramakrishnan
bcb055ed05 Show slash only with prefix 2019-11-18 11:04:06 -08:00
Girish Ramakrishnan
983b1e3656 index.docker.io is legacy
https://github.com/docker/distribution/blob/release/2.7/reference/normalize.go#L13
2019-11-17 11:39:42 -08:00
Girish Ramakrishnan
3c4dbe2558 Disable update view for custom apps 2019-11-16 10:53:15 -08:00
Girish Ramakrishnan
80b931ca9e email domain can be selected 2019-11-15 09:41:09 -08:00
Girish Ramakrishnan
200a234469 ldap: use separate objects for current config and user config 2019-11-14 17:30:23 -08:00
Girish Ramakrishnan
24ef877bfe website/description is optional for dev apps 2019-11-12 17:12:57 -08:00
Girish Ramakrishnan
602244b53f Must contain since it is a path 2019-11-11 16:26:01 -08:00
Girish Ramakrishnan
97b2a6eea0 typo 2019-11-11 16:06:46 -08:00
Girish Ramakrishnan
e20d09cfee Pass syinfoConfig properly 2019-11-11 15:15:50 -08:00
Girish Ramakrishnan
7e2ae8e87c add sysinfo to setup & restore 2019-11-11 12:27:44 -08:00
Girish Ramakrishnan
a868766a65 lint 2019-11-11 09:49:41 -08:00
Girish Ramakrishnan
7f1c505303 Add check for backupId to start with "box" 2019-11-11 09:46:47 -08:00
Girish Ramakrishnan
f679746e63 pass clientId instead of client object to revoke 2019-11-11 09:20:42 -08:00
Girish Ramakrishnan
40b75c6ac8 Always enable unstable app listing 2019-11-11 08:41:12 -08:00
Johannes Zellner
c5fc4db980 Align appstatus page style with other dashboard pages 2019-11-10 16:33:15 +01:00
Johannes Zellner
f6b88518a2 Add option to logout to destroy all sessions by this user 2019-11-08 21:33:29 +01:00
Girish Ramakrishnan
3fecb777e8 make token ui work again 2019-11-08 12:24:26 -08:00
Girish Ramakrishnan
3d2914da94 remove extra break 2019-11-08 11:10:26 -08:00
Johannes Zellner
52e1ce5237 Actually destroy the OAuth session on token revokation 2019-11-08 17:31:19 +01:00
Girish Ramakrishnan
b6b5875786 track current config separately 2019-11-07 23:08:42 -08:00
Girish Ramakrishnan
97782d29cc move tokens entirely into token page 2019-11-07 14:41:28 -08:00
Girish Ramakrishnan
0c5930d5cf simplify the sessions UI 2019-11-07 14:28:52 -08:00
Girish Ramakrishnan
836a3659b6 Add external ldap progress bar 2019-11-07 11:39:02 -08:00
Girish Ramakrishnan
2e6e320bd9 Make sysinfo page show detected ip 2019-11-07 10:47:40 -08:00
Girish Ramakrishnan
c26597cf02 Various network page improvements 2019-11-07 10:20:34 -08:00
Girish Ramakrishnan
9a0cc4a717 Various minor UI fixes 2019-11-07 09:40:49 -08:00
Girish Ramakrishnan
81aa94c8df chrome: groups buttons were wrapping 2019-11-07 09:40:49 -08:00
Johannes Zellner
ff30d6d23a Move sysinfo and dyndns settings to new network view 2019-11-07 15:26:18 +01:00
Johannes Zellner
30769b5992 We now always show the external ldap settings 2019-11-07 12:11:48 +01:00
Johannes Zellner
c6d2e6cda3 Hide api token ui behind 'tokens' query and ensure button sizes are consistent 2019-11-07 12:08:51 +01:00
Johannes Zellner
3a0c29988e Update package-lock file 2019-11-07 11:10:00 +01:00
Johannes Zellner
85f1c3816b Rename account to profile 2019-11-07 11:07:57 +01:00
Johannes Zellner
7040bb01f4 Ensure groups configure dialogs have all users available 2019-11-05 22:08:48 +01:00
Johannes Zellner
71f1304606 Make all multiselect search and scrollable if more than 10 items are available 2019-11-05 19:45:59 +01:00
Johannes Zellner
90d242b784 Add helper scripts to add/remove many users for testing purpose 2019-11-05 19:24:08 +01:00
Johannes Zellner
b520b6dc13 Add sysinfo configuration 2019-11-05 15:19:48 +01:00
Johannes Zellner
82b2b0b334 Simply mention available disk space in graphs 2019-11-05 12:04:19 +01:00
Girish Ramakrishnan
6463b84952 Add get/setSysinfoConfig 2019-10-31 19:33:05 -07:00
Girish Ramakrishnan
34cedbdadc Fill the usernameField 2019-10-31 11:39:42 -07:00
Girish Ramakrishnan
44cf25b447 bind dn can also be username 2019-10-30 09:36:02 -07:00
Johannes Zellner
94c7638c96 Mark external users with an icon 2019-10-29 13:04:44 +01:00
Johannes Zellner
6cf0727bd5 Ensure admin tooltip is fully visible 2019-10-29 12:58:54 +01:00
Johannes Zellner
561301bd28 Always show user actions, only disable them 2019-10-29 12:49:21 +01:00
Johannes Zellner
9039be8e39 Fix error reporting on password change 2019-10-29 12:39:39 +01:00
Johannes Zellner
c42292d546 Also ensure the progressbar is full width 2019-10-27 22:52:12 +01:00
Johannes Zellner
e21d17f6b8 Only adjust card margin on mobile 2019-10-27 22:52:12 +01:00
Girish Ramakrishnan
ca2eacdd82 the v2 is implied it seems 2019-10-27 13:13:41 -07:00
Girish Ramakrishnan
af9f2794be Remove error suggestion for now 2019-10-27 12:01:53 -07:00
Girish Ramakrishnan
ff84149623 ldap: add username field 2019-10-25 16:38:59 -07:00
Girish Ramakrishnan
99aea3ed60 ldap: move error to top 2019-10-25 16:13:52 -07:00
Girish Ramakrishnan
9528db700a ldap: add provider field 2019-10-25 15:42:51 -07:00
Girish Ramakrishnan
106187e2f4 Make ldap users have disabled input instead 2019-10-25 15:25:46 -07:00
Girish Ramakrishnan
e412aa9a3d Various fixes to ldap view
also, keep it enabled for all for now
2019-10-25 15:19:57 -07:00
Johannes Zellner
0a8fa40b6b Fixup various mobile view issues in the app configure view 2019-10-25 12:10:08 +02:00
Girish Ramakrishnan
8a84fa5cdd fix comment 2019-10-24 18:09:48 -07:00
Girish Ramakrishnan
1e8fb61abf Add warning on data loss 2019-10-24 10:07:34 -07:00
Girish Ramakrishnan
ee4e90deb5 Add modal restore dialog 2019-10-24 10:01:23 -07:00
Girish Ramakrishnan
e2124bac5a Add tooltip to show the raw time 2019-10-24 09:40:11 -07:00
Girish Ramakrishnan
c1b95547d7 This can also be a token
https://www.docker.com/blog/docker-hub-new-personal-access-tokens/
2019-10-23 06:51:12 -07:00
Girish Ramakrishnan
28025cfb44 Add email field to registry config 2019-10-23 06:48:34 -07:00
Girish Ramakrishnan
7c978f6c1c Add ui to configure registry 2019-10-22 22:42:55 -07:00
Girish Ramakrishnan
470936476e Move buttons to right 2019-10-22 10:49:57 -07:00
Girish Ramakrishnan
9c418e110f Make unstable apps a normal button 2019-10-22 10:06:32 -07:00
Johannes Zellner
499cb76492 Add app listing filter for recently updated apps 2019-10-22 17:51:09 +02:00
Johannes Zellner
bb00327e81 Ensure we don't cut off tooltips in apps view 2019-10-22 15:56:53 +02:00
Johannes Zellner
e79dec3c2b sort mailinglists by name 2019-10-22 12:52:03 +02:00
Johannes Zellner
ab23882c27 Add basic search filter for mailboxes and mailinglists 2019-10-22 12:47:32 +02:00
Johannes Zellner
a22602f6d1 Always mention how mailinglist addresses should be separated
The placeholder is not shown once anything is typed
2019-10-21 13:02:45 +02:00
Girish Ramakrishnan
c1ba1014c3 support: allow input of additional email 2019-10-18 18:23:58 -07:00
Johannes Zellner
f393f58bce Source the dns setup image from the api server 2019-10-18 21:54:59 +02:00
Johannes Zellner
b06d1fd293 Add server provider query argument to dns setup 2019-10-17 13:25:25 +02:00
Johannes Zellner
ac32e76eec Add dns setup logo 2019-10-16 14:16:54 +02:00
Girish Ramakrishnan
420fe0df0d Only required for email_error 2019-10-15 12:00:37 -07:00
Girish Ramakrishnan
b035030867 Add altEmail to support ticket (when mail is down) 2019-10-15 11:39:57 -07:00
Girish Ramakrishnan
a641fec3ae Set CSP instead of frameAncestors 2019-10-14 17:20:35 -07:00
Girish Ramakrishnan
13c3624025 Add ui for frame-ancestors 2019-10-14 16:04:41 -07:00
Girish Ramakrishnan
16728ab51c Fix wrong icons for non-admins 2019-10-11 18:35:09 -07:00
Girish Ramakrishnan
247eea1a0c Remove unused function 2019-10-11 15:24:25 -07:00
Girish Ramakrishnan
bf454816ea On error, reset the busy flag
this happens when icon is too large, for example
2019-10-11 15:18:52 -07:00
Girish Ramakrishnan
36028632ac simplify 2019-10-11 14:59:12 -07:00
Girish Ramakrishnan
0e386d33b0 Remove dead code 2019-10-11 14:53:44 -07:00
Girish Ramakrishnan
4f9d8915fb Hide progress bar for normal users
They cannot get the progress information anyway
2019-10-11 14:50:50 -07:00
Girish Ramakrishnan
0d94e4290b Remove the "v" from the version 2019-10-11 10:58:06 -07:00
Johannes Zellner
a1426bc81b Fix copy and paste error for namecheap input label 2019-10-06 16:20:40 +02:00
Johannes Zellner
d98d36d97b Disable documentation url is not set in app manifest 2019-10-06 16:20:40 +02:00
Girish Ramakrishnan
bf930d2ae0 Fix email reconfigure 2019-10-04 11:20:27 -07:00
Girish Ramakrishnan
631730bf3a repair can always be called
this is because sometimes cloudron thinks there is no error, but there is
2019-10-03 12:23:36 -07:00
Johannes Zellner
8d34a4c5a1 Ensure the notification badge has its place in the layout calculation
The float property makes it overflow on high notification numbers
2019-10-03 13:32:43 +02:00
Johannes Zellner
c8f50fc117 If app is not found while app view is visible, go back 2019-10-01 20:04:28 +02:00
Johannes Zellner
cfdb7b32fc Fix basic layout issues on small screens for app view 2019-09-30 15:19:48 +02:00
Girish Ramakrishnan
cc833f0b73 Stop any active app task on uninstall 2019-09-29 16:55:03 -07:00
Girish Ramakrishnan
47282afa22 Overwrite existing DNS records in repair 2019-09-29 16:37:38 -07:00
Girish Ramakrishnan
417640cfbe Fix hyphenation of alt domains in repair dialog 2019-09-29 16:37:35 -07:00
Johannes Zellner
8df1690d5d Fix yet another layout issue with long fqdn's 2019-09-28 15:49:35 +02:00
Girish Ramakrishnan
461d1bcd5b Add note about pause in the repair section 2019-09-27 15:25:46 -07:00
Girish Ramakrishnan
8b5e164291 Fix login eventlog 2019-09-27 15:07:37 -07:00
Girish Ramakrishnan
2644f56755 Show portBinding conflicts properly 2019-09-27 14:47:49 -07:00
Girish Ramakrishnan
437e6baeba eventlog: Add icon set/reset 2019-09-27 13:44:57 -07:00
Girish Ramakrishnan
6788d37b6f Configure label and tags only if changed 2019-09-27 13:38:30 -07:00
Girish Ramakrishnan
b90550d6ba Fix display of alt domains 2019-09-27 12:58:39 -07:00
Johannes Zellner
c068deb47e Avoid some text decoration on app title in various states 2019-09-27 19:55:45 +02:00
Johannes Zellner
2ce5b28048 Fixup postinstall message if app link is clicked 2019-09-27 19:43:03 +02:00
Johannes Zellner
57f1751309 Fix terminal restart and other state issues 2019-09-27 19:13:08 +02:00
Girish Ramakrishnan
2d8dc36f28 Elaborate what kind of backup 2019-09-27 09:34:01 -07:00
Johannes Zellner
077a717525 Move all STATEs to client.js so we can use them in other angular apps like terminal 2019-09-27 18:11:48 +02:00
Johannes Zellner
8795493462 Fixup linter issues in terminal view 2019-09-27 17:43:33 +02:00
Johannes Zellner
af532bae8f Make app main header the open app action instead of the button 2019-09-27 16:35:43 +02:00
Girish Ramakrishnan
c07b3e2d3c lint 2019-09-26 22:23:08 -07:00
Girish Ramakrishnan
6b1a9fa837 just return 'paused' 2019-09-26 22:16:37 -07:00
Girish Ramakrishnan
24888dfad5 terminal: repair -> pause 2019-09-26 22:16:37 -07:00
Girish Ramakrishnan
4d4c8638ca eventlog: Fix display of app.repair 2019-09-26 21:36:11 -07:00
Girish Ramakrishnan
9fd983abfb Make eventlog filter as functions
Some of the events requires access to domains and apps, making it hard
unsuitable for filters
2019-09-26 21:27:18 -07:00
Girish Ramakrishnan
637839ee14 remove unused filter 2019-09-26 21:10:26 -07:00
Girish Ramakrishnan
9b2578665f Add finish events to the filter 2019-09-26 20:13:24 -07:00
Girish Ramakrishnan
ee05e109c8 Add checkbox to skip backup for app update 2019-09-26 20:10:25 -07:00
Girish Ramakrishnan
a905e32cde eventlog: fix text of update events 2019-09-26 19:04:28 -07:00
Girish Ramakrishnan
fb5cec0d38 eventlog: Make next button disabled in last page 2019-09-26 18:27:00 -07:00
Girish Ramakrishnan
1a4f490fb5 Display the backupId 2019-09-26 18:23:13 -07:00
Johannes Zellner
4518c2c4c0 Use img tag instead of background-image for custom icon to avoid flickering 2019-09-26 21:12:14 +02:00
Johannes Zellner
17771ccecd Make app documentation a dropdown to show postinstall and upstream project link 2019-09-26 21:12:14 +02:00
Girish Ramakrishnan
f3c2a3c025 Remove the 'Daily' 2019-09-26 10:07:15 -07:00
Johannes Zellner
2aa919b444 Fix update error overflow in settings view 2019-09-25 11:49:09 +02:00
Johannes Zellner
408987ee30 avoid task progress flickering 2019-09-24 21:27:49 +02:00
Johannes Zellner
fe04ad9940 Show task progress as progress bar instead of indicator 2019-09-24 21:08:42 +02:00
Johannes Zellner
1b03e750a2 Remove unnecessary retry to fetch domains in apps view 2019-09-24 19:56:31 +02:00
Johannes Zellner
3ff781139e Add pre-flight and fix clone dialog 2019-09-24 18:50:52 +02:00
Johannes Zellner
2ea3ba492e Prevent angular from throwing exceptions if error is null 2019-09-24 18:46:07 +02:00
Girish Ramakrishnan
89d3228077 Do not show overwrite when creds are invalid 2019-09-24 01:03:57 -07:00
Girish Ramakrishnan
7946f5ee81 Rename func and put error below the control label 2019-09-24 00:04:31 -07:00
Girish Ramakrishnan
44f62eac9a Add error suggestion 2019-09-23 23:51:15 -07:00
Girish Ramakrishnan
47725e57b0 Fixup repair text 2019-09-23 23:28:14 -07:00
Girish Ramakrishnan
aac2aaa999 Make repair always visible 2019-09-23 22:03:54 -07:00
Girish Ramakrishnan
6ac1160bf2 Make stop/start work again 2019-09-23 15:50:41 -07:00
Johannes Zellner
70fae41042 Handle dns overwrite in appstore view 2019-09-24 00:21:12 +02:00
Johannes Zellner
07f5bfe3dc Provide appstore install overwrite checkbox 2019-09-24 00:21:12 +02:00
Girish Ramakrishnan
93a88a22b9 Move the collision list to the top 2019-09-23 15:13:50 -07:00
Girish Ramakrishnan
792faa1176 remove extra arg 2019-09-23 14:57:37 -07:00
Johannes Zellner
66900d594f Add app install dns preflight check 2019-09-23 23:47:33 +02:00
Johannes Zellner
9555f3c853 Adjust to new dns_check api 2019-09-23 23:47:12 +02:00
Johannes Zellner
9ed2fa734a Add location and domain selector to repair dialog 2019-09-23 22:45:45 +02:00
Johannes Zellner
db83508920 Remove ... from installation state label
Makes the filter not useful in sentences
2019-09-23 22:45:16 +02:00
Johannes Zellner
d72a6585d4 Handle pre-flight domain check api access issue 2019-09-23 21:38:35 +02:00
Girish Ramakrishnan
74fc8c9cf7 Add repair state 2019-09-23 12:34:47 -07:00
Johannes Zellner
f3440f3c01 Show debug view if app errors while having some other view open 2019-09-23 19:37:40 +02:00
Girish Ramakrishnan
b01799c606 Move help text down 2019-09-23 10:25:12 -07:00
Johannes Zellner
3cbb4e3f43 Handle location change api key invalid error 2019-09-23 19:23:06 +02:00
Girish Ramakrishnan
36299acbfb lint 2019-09-23 10:16:19 -07:00
Girish Ramakrishnan
acc20af2d9 Fix text 2019-09-23 09:56:31 -07:00
Girish Ramakrishnan
077ce5b521 Fixup text 2019-09-23 09:54:40 -07:00
Johannes Zellner
1efe82dda2 Remove debug code and fixup ISTATE usage 2019-09-23 16:53:48 +02:00
Johannes Zellner
f283618209 Only show error message if also set 2019-09-23 16:27:05 +02:00
Johannes Zellner
fe6baf8dba Attempt to cover most repair cases 2019-09-23 15:32:38 +02:00
Johannes Zellner
b742dc51fb Ensure we can start a stopped app 2019-09-22 12:21:39 +02:00
Johannes Zellner
c8ea649afc Display error message in debug view 2019-09-22 12:16:20 +02:00
Johannes Zellner
a27e94f694 Only allow debug and uninstall views on app error 2019-09-22 12:10:44 +02:00
Girish Ramakrishnan
25f9e7829f Fix run state handling 2019-09-22 01:02:39 -07:00
Johannes Zellner
85be7acab2 add initial repair dialog with domain/backup selection 2019-09-21 22:45:26 +02:00
Johannes Zellner
36c23227e5 Fix location form submission bug on enter 2019-09-21 11:07:20 +02:00
Johannes Zellner
0b6f68e190 Fix layout issue if app domain is too long 2019-09-21 11:02:00 +02:00
Girish Ramakrishnan
6c90fc2764 add app creation time 2019-09-20 09:20:41 -07:00
Johannes Zellner
4fd1e55ae8 Add pre-flight check for domain collision 2019-09-20 11:32:15 +02:00
Girish Ramakrishnan
9672f7e3da Remove "App" 2019-09-19 18:01:12 -07:00
Girish Ramakrishnan
50d29f8ef0 Use simple input field for custom data dir instead of checkbox (email) 2019-09-19 18:00:18 -07:00
Girish Ramakrishnan
6ffa00026e pending_configure is dead 2019-09-19 17:36:31 -07:00
Girish Ramakrishnan
c4677505ac Fix debug_mode route 2019-09-19 17:30:24 -07:00
Girish Ramakrishnan
bac6d7cf3c Revert "Add repair state"
This reverts commit 85ea91e0e3.
2019-09-19 17:15:43 -07:00
Girish Ramakrishnan
85ea91e0e3 Add repair state 2019-09-19 17:03:52 -07:00
Johannes Zellner
09b09086ce Use simple input field for custom data dir instead of checkbox 2019-09-20 01:22:10 +02:00
Johannes Zellner
8fbfa86a7f Do not randomly throw unhandled error notifications 2019-09-20 00:51:16 +02:00
Johannes Zellner
6224e942dc Track backup progress 2019-09-20 00:05:17 +02:00
Johannes Zellner
ab5edbdd41 Add postinstall message to app view 2019-09-20 00:03:58 +02:00
Girish Ramakrishnan
7825d10f18 Move the error down 2019-09-19 14:51:32 -07:00
Johannes Zellner
8c1988e480 handle reserved domain errors 2019-09-19 22:02:37 +02:00
Girish Ramakrishnan
8403b811d8 Fix font size 2019-09-19 12:50:55 -07:00
Girish Ramakrishnan
1c797505ae Fixup debug tab 2019-09-19 12:50:06 -07:00
Girish Ramakrishnan
466086b509 Fixup backup tab 2019-09-19 12:24:22 -07:00
Girish Ramakrishnan
221f7247e6 rename title 2019-09-19 12:00:35 -07:00
Girish Ramakrishnan
624bc88f74 Make separate section in updates view 2019-09-19 11:58:11 -07:00
Johannes Zellner
d6ca4458e4 Make active task overlay less verbose 2019-09-19 20:43:58 +02:00
Girish Ramakrishnan
1fe3e60468 Merge the email explanation 2019-09-19 11:30:30 -07:00
Girish Ramakrishnan
aa65b2b97c dataDir explanation 2019-09-19 11:22:57 -07:00
Johannes Zellner
6dea2475c7 Prevent some more angular warnings if app object isn't set yet 2019-09-19 19:46:20 +02:00
Girish Ramakrishnan
a666cb00eb Fixup uninstall UI 2019-09-19 10:28:19 -07:00
Johannes Zellner
f0fac9165c Fix open app button in app view 2019-09-19 19:19:51 +02:00
Johannes Zellner
e51eb8a9c1 Ensure back button from apps grid into app view works no first usage 2019-09-19 19:19:36 +02:00
Johannes Zellner
4822984e34 Some code cleanup 2019-09-19 18:41:08 +02:00
Johannes Zellner
4a558a7f65 Enabling backups and auto updates is not a danger action 2019-09-19 18:41:08 +02:00
Johannes Zellner
48d4935c7d Ensure error states are cleared 2019-09-19 18:41:08 +02:00
Girish Ramakrishnan
506accfe9b Show hr only when we have sftp section 2019-09-19 09:35:25 -07:00
Girish Ramakrishnan
c15aba47f5 Fix installation state label text to have ... 2019-09-19 09:28:33 -07:00
Johannes Zellner
fdafa8adf6 Only allow to open app when runState is also running 2019-09-19 18:27:02 +02:00
Johannes Zellner
23e15581f3 Make sure we cleanup polling timers on view switch 2019-09-18 18:18:43 +02:00
Johannes Zellner
1621f866a8 Cleanup apps view while removing update modal 2019-09-18 18:10:51 +02:00
Johannes Zellner
f03fe33b1f Prevent angular erros if app is null in some filter 2019-09-18 18:06:03 +02:00
Johannes Zellner
6ec2a5ea35 If app is in error state directly go to debug view 2019-09-18 17:48:07 +02:00
Johannes Zellner
0ae4d323f7 Allow deep linking into the app configure views 2019-09-18 17:45:13 +02:00
Johannes Zellner
930404e482 Ensure we use the same danger color also in form validations 2019-09-18 17:17:36 +02:00
Johannes Zellner
92257afdab Enable custom app data dir setting 2019-09-18 17:12:10 +02:00
Johannes Zellner
300ff09a47 Move app error handling to simply show the app configure view 2019-09-18 15:54:19 +02:00
Johannes Zellner
14dd1103eb Fix angular interval usage 2019-09-18 15:53:57 +02:00
Johannes Zellner
ff07eb1de0 Always poll for app status updates when an app configure view is open 2019-09-17 22:38:11 +02:00
Johannes Zellner
b81f45bf47 Selectively show the email configure view 2019-09-17 22:05:32 +02:00
Johannes Zellner
eb3232e049 Add update check and apply buttons in app view 2019-09-17 17:14:40 +02:00
Johannes Zellner
752f653f82 Improve the tab view 2019-09-17 16:19:43 +02:00
Johannes Zellner
ed90dbe7b7 Improve app backup view 2019-09-17 16:16:54 +02:00
Johannes Zellner
e1e0f2944b Make uninstall a separate view 2019-09-17 15:40:04 +02:00
Johannes Zellner
2269f15b66 Merge overview bits into other more relevant views 2019-09-17 15:32:43 +02:00
Johannes Zellner
5c0a53e02a Bring back email view in app configure 2019-09-17 15:09:39 +02:00
Johannes Zellner
8810439ffc Reset views on change 2019-09-17 14:52:22 +02:00
Johannes Zellner
9d61270937 Add artificial 1sec delay to simple app form submissions 2019-09-17 14:49:26 +02:00
Johannes Zellner
b602a9d15d Handle location form submit errors for (sub)domains 2019-09-16 19:58:15 +02:00
Johannes Zellner
bb0ab03ad9 Improve the configure overlay button 2019-09-16 14:24:17 +02:00
Johannes Zellner
d674dcaeef Add location display in overview 2019-09-16 14:18:51 +02:00
Johannes Zellner
a738ddb917 Add initial back to my apps link 2019-09-16 14:15:38 +02:00
Johannes Zellner
935c92b507 Make app configure sections a separate view 2019-09-16 14:03:13 +02:00
Johannes Zellner
10d1a2d8e4 Even further cleanup of dead code 2019-09-13 19:06:50 +02:00
Johannes Zellner
cf6d64646a remove more dead code 2019-09-13 17:19:53 +02:00
Johannes Zellner
c570e8b6fe Move app clone into app view 2019-09-13 17:18:37 +02:00
Johannes Zellner
849b9e0c80 Remove dead code 2019-09-13 17:10:12 +02:00
Johannes Zellner
8f8aa31304 Add restore and backup logic to app view 2019-09-13 17:08:50 +02:00
Johannes Zellner
a1fe79c876 Also remove the app info dialog code from apps grid 2019-09-13 16:07:55 +02:00
Johannes Zellner
7c9654a541 Remove app task cancel code in apps grid 2019-09-13 15:54:27 +02:00
Johannes Zellner
a86df7cdbf aRemove unused requires 2019-09-13 15:51:36 +02:00
Johannes Zellner
3d5cdd659b Remove all configure bits from the app grid page 2019-09-13 11:29:19 +02:00
Johannes Zellner
7a2a5d3846 Redirect back to app grid if app does not exist 2019-09-13 11:20:16 +02:00
Johannes Zellner
62fb0acb3c Move uninstall confirm dialog to app page 2019-09-13 11:18:43 +02:00
Johannes Zellner
c4dfe8a723 Fixup form submission state for memory limit and robots 2019-09-13 11:12:11 +02:00
Johannes Zellner
4af4df9288 Only enable submit button if memory limit is changed 2019-09-13 11:06:13 +02:00
Johannes Zellner
25b0e18ceb Do not make the app task overlay that busy 2019-09-13 11:02:13 +02:00
Johannes Zellner
c4aec8dfa6 Do not show app postinstall info in app view 2019-09-13 10:57:34 +02:00
Johannes Zellner
4056a3da43 Add debug section to app page 2019-09-13 10:34:12 +02:00
Johannes Zellner
2027f8052b Improve app overview and add restart action 2019-09-12 17:42:33 +02:00
Johannes Zellner
96bb293c1f Improve app location settings 2019-09-12 17:08:45 +02:00
Johannes Zellner
fd73b28d66 Improve app display configuration 2019-09-12 16:28:21 +02:00
Johannes Zellner
aafa698776 Add initial app configure jump links 2019-09-12 15:59:40 +02:00
Johannes Zellner
a99d31535c Always show configure icon in app grid for now 2019-09-12 14:40:16 +02:00
Johannes Zellner
fda8791d5a app postprocess is already run in getApp 2019-09-12 12:18:10 +02:00
Girish Ramakrishnan
9e2ac31a08 Make mail list members a textarea
Also, fix the error handling
2019-09-11 14:40:57 -07:00
Johannes Zellner
758b32a61c Add initial task popup for apptasks 2019-09-11 21:24:45 +02:00
Johannes Zellner
62b392e555 Do not throw error if app is null in state label filter 2019-09-11 21:24:45 +02:00
Johannes Zellner
a4c99fd361 Move all app configure tasks to separate view 2019-09-11 21:24:45 +02:00
Girish Ramakrishnan
8823656d70 add new installationStates 2019-09-10 14:30:44 -07:00
Girish Ramakrishnan
b82f5da112 Fixup event log for app configure 2019-09-10 14:18:44 -07:00
Girish Ramakrishnan
729f51b779 Make email a separate tab
This allows us to add some additional info that the app is pre-configured
to send email via the relay
2019-09-08 12:52:34 -07:00
Girish Ramakrishnan
1c1171e8a7 Add help link for relay 2019-09-08 12:23:27 -07:00
Johannes Zellner
44df319ff6 Do not show error page in notification on proxy upstream errors
This handles box being down and nginx delivers error page.
We do not want to show that in the notification, but other box crash
errors should be shown, they need to be fixed
2019-09-07 09:40:03 +02:00
Johannes Zellner
84dec337f0 Prevent angular errors when there is no appError.app set yet 2019-09-07 09:24:45 +02:00
Girish Ramakrishnan
2e60a9d43c init creatingBackup variable 2019-09-06 15:29:59 -07:00
Girish Ramakrishnan
4474766526 Hide when backup is active 2019-09-06 15:27:03 -07:00
Girish Ramakrishnan
ddc1d8117d from -> using 2019-09-06 14:39:39 -07:00
Girish Ramakrishnan
609bae4f1a Use reason code 2019-09-05 19:23:58 -07:00
Girish Ramakrishnan
ff16a4334f Move showError into appError scope 2019-09-05 17:50:10 -07:00
Johannes Zellner
739e308c0e Make offline banner a link to the troubleshooting page 2019-09-06 00:14:05 +02:00
Johannes Zellner
6b29f57e1d Give useful information when box crashes or a request is otherwise terminated 2019-09-06 00:01:41 +02:00
Johannes Zellner
21981829fd Make Client.error() persistent and allow to pass an action 2019-09-06 00:01:41 +02:00
Johannes Zellner
b6e00a3107 Do not redirect to error.html if the angular main application fails to init
We now only show the offline banner and retry the application init until
box comes back up
2019-09-05 22:23:28 +02:00
Johannes Zellner
8b8b137cad Fix rest api wrapper usage 2019-09-05 22:23:28 +02:00
Girish Ramakrishnan
1ba1286df0 cloudron -> server 2019-09-05 12:28:04 -07:00
Girish Ramakrishnan
0417a82f83 Show error if graph loading fails 2019-09-05 12:23:53 -07:00
Girish Ramakrishnan
4cc01a2152 Move optional field to the end 2019-09-05 11:38:55 -07:00
Girish Ramakrishnan
c7d434a091 Show backup progress inline 2019-09-05 11:30:27 -07:00
Girish Ramakrishnan
3e1e704a7f Bring back app progress message 2019-09-05 10:05:01 -07:00
Girish Ramakrishnan
12a9dcaa76 Better blocking update text 2019-09-04 21:10:34 -07:00
Girish Ramakrishnan
fc2dd148c5 The tooltip is useful to track progress 2019-09-04 21:10:34 -07:00
Johannes Zellner
ede6f36913 ng-cloak the offline bannder 2019-09-04 22:09:10 +02:00
Girish Ramakrishnan
f85143fb7b Add links to docs in user dialog for disable and admin 2019-09-04 11:06:49 -07:00
Girish Ramakrishnan
bbf3043fc3 graphs: add label to app tool tips 2019-09-04 09:20:05 -07:00
Girish Ramakrishnan
bbd73d361a Fixup error handling in apps view 2019-09-03 15:18:05 -07:00
Girish Ramakrishnan
7d44c87aff Use error codes and fields 2019-09-02 17:28:40 -07:00
Girish Ramakrishnan
7e81041b87 Use reason code for better error handling 2019-09-02 13:18:19 -07:00
Girish Ramakrishnan
e30698459b Use data.success before grabbing the error 2019-08-30 14:24:59 -07:00
Johannes Zellner
42399469a7 Prevent non-admins from showing the repair modal 2019-08-30 21:24:00 +02:00
Johannes Zellner
8ccc7bb734 Remove wrench and add action description on error 2019-08-30 21:21:44 +02:00
Girish Ramakrishnan
38a7c222a8 app.error is now an object 2019-08-30 11:38:31 -07:00
Johannes Zellner
9ea21606e5 Only show external ldap settings when ?ldap query is passed for now 2019-08-30 19:01:56 +02:00
Johannes Zellner
c809119d57 Ensure hand cursor is correctly shown also in error states 2019-08-30 18:15:34 +02:00
Johannes Zellner
b8ca009e69 Revert "Always show the hand cursor in app grid items"
This reverts commit 9df90e4edc.
2019-08-30 17:20:21 +02:00
Johannes Zellner
1e37d7da7d Refresh the user lising after ldap sync 2019-08-30 16:56:01 +02:00
Johannes Zellner
9df90e4edc Always show the hand cursor in app grid items 2019-08-30 16:51:50 +02:00
Johannes Zellner
4576e93deb Hide user account actions for external ldap users 2019-08-30 13:36:52 +02:00
Johannes Zellner
ea5e0b28da Hide certain user profile actions for external ldap users 2019-08-30 13:32:20 +02:00
Johannes Zellner
19c8a01969 Add more description how ldap sync works 2019-08-30 13:09:44 +02:00
Johannes Zellner
ebab88e7aa Rework the external ldap ui to follow usual modal dialog pattern 2019-08-30 12:40:23 +02:00
Johannes Zellner
b4248acd9a There is no ng-enabled, only ng-disabled 2019-08-30 12:34:31 +02:00
Johannes Zellner
c303174f0b Usernames can be even 1 character long 2019-08-30 11:02:07 +02:00
Johannes Zellner
91cf6465df Give external ldap sync task feedback 2019-08-30 10:20:08 +02:00
Johannes Zellner
426d2aab09 Add ability to trigger external ldap syncer task 2019-08-30 10:20:08 +02:00
Johannes Zellner
8c44e558a8 Add external LDAP configuration 2019-08-30 10:20:08 +02:00
Girish Ramakrishnan
6a08e08d7c Show errorMessage 2019-08-29 18:59:13 -07:00
Girish Ramakrishnan
2796ad12fe Show app cancel busy indicator 2019-08-29 14:51:21 -07:00
Girish Ramakrishnan
45d40297bf refresh app cache on cancel 2019-08-29 14:35:39 -07:00
Girish Ramakrishnan
57ac37c210 Fix configure/repair button text 2019-08-29 14:34:04 -07:00
Girish Ramakrishnan
8eee0b809c Add cancel action for active app tasks 2019-08-29 14:30:25 -07:00
Girish Ramakrishnan
5387054000 Refactor update functions 2019-08-29 11:33:22 -07:00
Girish Ramakrishnan
e08f072d95 Refactor uninstall functions 2019-08-29 11:30:37 -07:00
Girish Ramakrishnan
44db2ca02a lint 2019-08-29 11:18:02 -07:00
Girish Ramakrishnan
dc8564d18e Remove force update state 2019-08-29 10:59:15 -07:00
Girish Ramakrishnan
3b8bc9fdab Rename pending to queued 2019-08-28 22:13:28 -07:00
Girish Ramakrishnan
c5d65fa030 typo 2019-08-28 22:12:24 -07:00
Girish Ramakrishnan
0252b08c8f Remove app task crash action 2019-08-28 16:03:17 -07:00
Girish Ramakrishnan
8f29b7a91f Use task api to get app progress 2019-08-28 16:03:17 -07:00
Johannes Zellner
19e1bbdc1c Add missing graphite log viewer entry 2019-08-27 14:54:39 +02:00
Johannes Zellner
cd2baf105f Fix next color helper 2019-08-22 18:38:32 +02:00
Johannes Zellner
3366acde58 prettyDiskSize should return something descriptive instead of NaN
undefined
2019-08-22 18:37:32 +02:00
Johannes Zellner
775f6eff0b We are in the browser, avoid const 2019-08-22 18:33:37 +02:00
Girish Ramakrishnan
52d501dae8 use app label if available 2019-08-21 14:59:55 -07:00
Girish Ramakrishnan
eb24baf2c1 Better prettyDiskDize 2019-08-21 14:57:59 -07:00
Johannes Zellner
7fd0ef51b5 Use more consistent theme colors 2019-08-21 20:46:26 +02:00
Johannes Zellner
173acc5226 Show disk content separately inside the usage graph 2019-08-21 20:27:43 +02:00
Johannes Zellner
6643b825ee Fixup the disk usage gathering 2019-08-21 12:08:19 +02:00
Girish Ramakrishnan
a56f20584f add note 2019-08-20 19:38:53 -07:00
Girish Ramakrishnan
22664bea62 Add options to graphs 2019-08-20 19:38:32 -07:00
Johannes Zellner
f80bf65076 Fetch du- data for disk contents 2019-08-20 13:00:45 +02:00
Johannes Zellner
28634c59c8 Get the correct df data from graphite 2019-08-20 12:36:06 +02:00
Johannes Zellner
993377a40b Show email and docker data location in graphs 2019-08-20 12:23:07 +02:00
Girish Ramakrishnan
9633733bc4 Clarify that this is ubuntu related 2019-08-19 13:56:25 -07:00
Johannes Zellner
6b4893b854 Rework disk usage graphs and give more information what is installed where 2019-08-19 16:50:03 +02:00
Johannes Zellner
09f7c35dac Show red badge and subscription expired page depending on the subscriptipn status 2019-08-19 12:11:11 +02:00
Johannes Zellner
0fc4169b0b correctly display premium app requirement page 2019-08-19 09:13:37 +02:00
Johannes Zellner
78746be0f5 Distinguish between too many apps and premium apps 2019-08-14 16:38:02 +02:00
Johannes Zellner
2287a550d7 Move subscription setup outlink button inside iframe for better label control 2019-08-14 15:48:10 +02:00
Johannes Zellner
d6eb6d3318 Add small indicator if no users have been found 2019-08-13 15:30:40 +02:00
Girish Ramakrishnan
db2d36eaa1 Show ban icon for inactive users 2019-08-08 08:30:29 -07:00
Girish Ramakrishnan
151d20341e Add checkbox for user active 2019-08-08 08:25:59 -07:00
Johannes Zellner
2c51bc17f1 Reduce iframe size 2019-08-06 14:16:35 +02:00
Johannes Zellner
0448ad49ed Allow our appstore origins explicitly 2019-08-06 10:24:35 +02:00
Johannes Zellner
2d4129f8f7 Fix typo 2019-08-06 10:22:00 +02:00
Johannes Zellner
c42aa7c806 Remove caas case in setup screen 2019-08-06 10:03:12 +02:00
Johannes Zellner
debeb8dfd8 Ignore setup page info button in tab focus 2019-08-06 09:59:06 +02:00
Johannes Zellner
2227e1dd4b Allow to specify the appstore subscription helper origins 2019-08-05 20:32:56 +02:00
Johannes Zellner
1d7e73c162 Fix indentation 2019-08-05 20:04:27 +02:00
Johannes Zellner
3f451856a0 adjust to appstore filename change 2019-08-04 11:38:09 +02:00
Johannes Zellner
fdd0483c9f Allow localhost development iframe sources 2019-08-04 11:32:42 +02:00
Girish Ramakrishnan
c1a49a52e8 Remove the newline 2019-08-03 10:33:56 -07:00
Girish Ramakrishnan
d8394392c9 Fix tooltip 2019-08-03 09:43:30 -07:00
Girish Ramakrishnan
eb7a037f94 comment out display raw json event 2019-08-03 09:37:29 -07:00
Girish Ramakrishnan
eb905aab86 Add SPF doc links
Fixes cloudron/box#636
2019-08-02 13:30:45 -07:00
Girish Ramakrishnan
55892097d7 Fix error message when clicking on unhealthy app
Fixes cloudron/box#643
2019-07-31 11:48:38 -07:00
Girish Ramakrishnan
6bfcda9fdc Send app object to message filters
this lets us access the state of an app
2019-07-31 11:40:18 -07:00
Girish Ramakrishnan
02dcbb9a52 Add some comments on the various labels 2019-07-31 11:38:28 -07:00
Johannes Zellner
0c6a6e4173 Show premium app required site from appstore 2019-07-31 08:09:43 +02:00
Johannes Zellner
e044251df4 Show subscription setup page from appstore 2019-07-31 08:09:43 +02:00
Johannes Zellner
26d27a3f6a Allow iframes from *cloudron.io 2019-07-31 08:09:43 +02:00
Girish Ramakrishnan
c5b9fccedb Add wasabi to restore UI 2019-07-30 10:42:18 -07:00
Girish Ramakrishnan
0153e5212c Allow restore from IP 2019-07-26 22:43:59 -07:00
Girish Ramakrishnan
92835a5270 Show warning when sendmail is disabled for a domain 2019-07-24 22:11:05 -07:00
Girish Ramakrishnan
5f41c78305 Add wasabi 2019-07-22 16:57:01 -07:00
Johannes Zellner
1726b89dea Ensure large notification markdown payloads do not overflow the UI 2019-07-17 16:40:10 +02:00
Girish Ramakrishnan
2506e69cdc Add SparkPost as mail relay 2019-07-15 10:49:13 -07:00
Johannes Zellner
88bc30bbea Rework the restore/clone dialogs 2019-07-12 17:18:21 +02:00
Girish Ramakrishnan
2835d1bd87 We now have free trial 2019-07-11 11:18:48 -07:00
Girish Ramakrishnan
e590896f01 appstore: handle 402 2019-07-08 09:58:50 -07:00
Girish Ramakrishnan
dfb0836446 Revert "Enable dataDir setting"
This reverts commit 9515a060ab.
2019-07-03 07:53:30 -07:00
Girish Ramakrishnan
88fdd1f562 Remove ownerId use 2019-07-02 20:22:06 -07:00
Girish Ramakrishnan
ae07c7934e Fix login issue when admin is revoked
If this ex-admin user had installed an app, then we try to get app details
(since he is the owner) and we go into a loop
2019-07-02 19:02:39 -07:00
Girish Ramakrishnan
9515a060ab Enable dataDir setting 2019-06-25 10:49:39 -07:00
Girish Ramakrishnan
c550416c9d reset the app icon on show 2019-06-21 13:30:33 -07:00
Girish Ramakrishnan
712883373a scaleway-os: restore UI 2019-06-21 11:10:19 -07:00
Girish Ramakrishnan
50930ee609 exoscale: do not overwrite endpoint 2019-06-21 10:46:07 -07:00
Girish Ramakrishnan
78bffad99f exoscale: add CH-GVA-2 2019-06-21 10:44:24 -07:00
Girish Ramakrishnan
b3760a961d Prefix the env vars with CLOUDRON_ for manifest v2 2019-06-19 09:16:41 -07:00
Girish Ramakrishnan
813d92ce32 Show domain removal error
this got removed by mistake with the password edit
2019-06-18 11:38:30 -07:00
Girish Ramakrishnan
b02570e679 Hide data dir UI again for this release 2019-06-13 15:14:25 -07:00
Girish Ramakrishnan
b7d1979d0d Always show the DNS status section 2019-06-12 17:48:06 -07:00
Johannes Zellner
6e6846835a Do not create notification links with target blank
Most links are pointing the user within the dashboard
2019-06-12 16:15:55 +02:00
Johannes Zellner
d899935b56 Add Digitalocean Spaces Frankfurt region 2019-06-12 10:11:46 +02:00
Girish Ramakrishnan
2a07c063ab deleting app does not remove backups 2019-06-08 11:08:16 -07:00
Johannes Zellner
3ab9d77930 Give alternate domain rows some space 2019-06-05 20:22:44 +02:00
Johannes Zellner
5537507646 Distinguish alternate and main domain errors 2019-06-05 20:22:34 +02:00
Girish Ramakrishnan
215dd03751 Show dataDir UI 2019-06-01 09:05:15 -07:00
Girish Ramakrishnan
3fe73ba198 Move dataDir and memory to resources section 2019-06-01 09:02:05 -07:00
Girish Ramakrishnan
6bc7edea67 Revert "Attempt to finally fix checkboxes and radio button layout"
This reverts commit f95a98d3ee.

This breaks access restriction page spacing
2019-05-31 15:24:13 -07:00
Girish Ramakrishnan
c44e69c396 Add help link for redirection 2019-05-31 14:41:47 -07:00
Girish Ramakrishnan
f6ad697755 Replace big plus button with text link 2019-05-31 14:39:35 -07:00
Girish Ramakrishnan
2abca93333 Make the preview icon smaller 2019-05-31 14:25:03 -07:00
Girish Ramakrishnan
788e7c40e9 Add Access tab to configure UI 2019-05-31 14:14:34 -07:00
Johannes Zellner
dca43f3e57 Allow to configure more than one alternate domain 2019-05-31 15:19:07 +02:00
Johannes Zellner
f95a98d3ee Attempt to finally fix checkboxes and radio button layout 2019-05-31 12:31:02 +02:00
Girish Ramakrishnan
11fe3dc492 support: show create ticket result 2019-05-28 10:04:18 -07:00
Girish Ramakrishnan
4277244150 fix text: Event Log 2019-05-24 19:03:13 -07:00
Girish Ramakrishnan
8458bcf10e make the info text small 2019-05-24 10:28:42 -07:00
Girish Ramakrishnan
6c8c7751fd Add note that access control allows SFTP access 2019-05-24 10:25:34 -07:00
Girish Ramakrishnan
d6096d04d9 remove twitter from footer 2019-05-22 11:56:55 -07:00
Girish Ramakrishnan
bffe6327a0 setup: have a single error object 2019-05-22 11:14:33 -07:00
Girish Ramakrishnan
28845d6f33 state and not status 2019-05-22 10:50:58 -07:00
Girish Ramakrishnan
6ec7da9071 fix text 2019-05-22 09:38:13 -07:00
Johannes Zellner
5dbe564afb Use the same size aspect ratio for custom app icon selector as in the app grid 2019-05-22 09:32:10 +02:00
Johannes Zellner
4794791167 Style the custom app icon configuration the same way the avatar is 2019-05-22 09:29:17 +02:00
Girish Ramakrishnan
7b2ae2c457 Make reset show the original icon 2019-05-21 00:18:29 -07:00
Girish Ramakrishnan
f0093c5e4f Add random string to icon to invalidate it 2019-05-21 00:06:33 -07:00
Girish Ramakrishnan
96117216ee Allow icon to be set 2019-05-20 22:24:58 -07:00
Girish Ramakrishnan
9982557909 setAdmin is unused 2019-05-20 19:05:31 -07:00
Girish Ramakrishnan
530331f9ee Fix texts in dialogs 2019-05-20 19:05:12 -07:00
Girish Ramakrishnan
23018abdf6 Put some text in delete user dialog 2019-05-20 19:05:12 -07:00
Girish Ramakrishnan
23b72620a1 domain remove does not require password 2019-05-20 19:05:08 -07:00
Girish Ramakrishnan
a80c21d77f domainMigrate is unused 2019-05-20 18:58:33 -07:00
Johannes Zellner
765307ddef Fix domain filter select 2019-05-20 23:40:02 +02:00
Johannes Zellner
9a859629bc Remove unused usedDomains 2019-05-20 23:23:07 +02:00
Johannes Zellner
cc7b203f93 Only allow selecting one domain in the apps filter 2019-05-20 21:06:13 +02:00
Girish Ramakrishnan
8744eadca0 Fixup text 2019-05-20 10:55:46 -07:00
Girish Ramakrishnan
76eaee5b1a Remove X-Frame-Options 2019-05-20 10:10:36 -07:00
Girish Ramakrishnan
7adde2a880 Make tag matching an AND operation 2019-05-17 13:01:23 -07:00
Girish Ramakrishnan
c02eced029 no comma 2019-05-16 10:14:29 -07:00
Girish Ramakrishnan
ad5ca50273 filter on alt domains also 2019-05-16 09:55:24 -07:00
Girish Ramakrishnan
767756ba9b sort the tags 2019-05-16 09:42:34 -07:00
Girish Ramakrishnan
c3cf5ff84c Use changeDashboardDomain custom config 2019-05-14 19:21:15 -07:00
Girish Ramakrishnan
a82c790855 Always show the change dashboard domain
otherwise, people don't know this feature exists!
2019-05-14 19:12:45 -07:00
Johannes Zellner
4b22e3e0a8 Hide the token, session and oauch client ui for non admins 2019-05-14 16:55:29 +02:00
Johannes Zellner
2039a143ac Do not crash if some apis are only for admins 2019-05-14 16:41:08 +02:00
Girish Ramakrishnan
84473dc10d poll subscription status only when button got clicked
otherwise, both stripe, our appstore db is bombarded
2019-05-13 16:40:51 -07:00
Girish Ramakrishnan
5695da1d86 refresh notifications every minute
no reason to bombard the server, it's not like this is realtime
2019-05-13 16:13:09 -07:00
Johannes Zellner
30583cce21 Do not require password for user and group deletion 2019-05-13 23:55:54 +02:00
Johannes Zellner
27c7c0438f account password change now returns 400 2019-05-13 23:42:06 +02:00
Johannes Zellner
b67d5eec3d Remove password requirement for app uninstall and restore 2019-05-13 23:31:45 +02:00
Johannes Zellner
7c2ea6288c Fix the default app grid with to avoid occasional overflow 2019-05-13 23:04:40 +02:00
Johannes Zellner
9a1d71face Prevent notification collapse toggle when attempting selecting message details 2019-05-13 21:06:20 +02:00
Girish Ramakrishnan
8e346bf676 Add checkbox to skip backup 2019-05-12 13:44:10 -07:00
Johannes Zellner
53a00a8d76 markdown injects block elements so break out the version 2019-05-11 13:48:22 +02:00
Johannes Zellner
ea1e556197 Allow the footer content to be configured 2019-05-11 13:33:02 +02:00
Johannes Zellner
402d75bfe0 Make the ssh remote support text more generic 2019-05-11 13:26:21 +02:00
Johannes Zellner
a444b61edf Remove twitter link from footer and pull version to the right 2019-05-11 10:24:11 +02:00
Johannes Zellner
ce41af14db Make sure the tooltip text does not get cropped 2019-05-11 10:04:21 +02:00
Girish Ramakrishnan
d52d606088 Use ticketBodyMarkdown 2019-05-10 17:35:51 -07:00
Girish Ramakrishnan
475311f63a Fixup uiSpec use 2019-05-10 16:09:13 -07:00
Johannes Zellner
5509089c49 Fixup the checkbox dom 2019-05-10 23:47:36 +02:00
Girish Ramakrishnan
3698220b8f features -> uiSpec 2019-05-10 11:23:53 -07:00
Girish Ramakrishnan
b22dba00a2 Make login work after user becomes admin 2019-05-10 09:45:37 -07:00
Girish Ramakrishnan
3d8ec5531c Fix avatarUrl use 2019-05-10 09:27:43 -07:00
Girish Ramakrishnan
7df0ae0ba3 Allow email to be enabled without dns setup
This helps in importing existing mail and also configuring mailboxes
before going live
2019-05-09 15:41:37 -07:00
Johannes Zellner
05d37cc6c6 Show domain filter only if we have more than one domain 2019-05-09 19:48:00 +02:00
Johannes Zellner
df03f783f8 Change the demo link 2019-05-08 18:46:41 +02:00
Girish Ramakrishnan
cd9263711f feedback -> ticket 2019-05-07 11:36:12 -07:00
Girish Ramakrishnan
48c3372c33 Use config.features to customize UI 2019-05-07 10:11:54 -07:00
Girish Ramakrishnan
5d1ff97bf3 remove edition flag 2019-05-06 20:05:18 -07:00
Girish Ramakrishnan
1decfe8063 Show proper error if available 2019-05-06 20:05:12 -07:00
Johannes Zellner
a3d0ffb7de Avoid throwing error on quick view switch away from the appstore 2019-05-06 11:47:57 +02:00
Johannes Zellner
59a54f8683 Do not show error and empty appstore details if not yet setup 2019-05-06 11:39:45 +02:00
Johannes Zellner
83e2bd6ade Distinguish between not yet registered and invalid appstore token
This is to avoid throwing errors
2019-05-06 11:07:20 +02:00
Girish Ramakrishnan
a59aca10ec Fixup subscription routes 2019-05-05 13:02:23 -07:00
Girish Ramakrishnan
9ac6e65087 Wait for app list before setting ready flag 2019-05-05 11:52:42 -07:00
Girish Ramakrishnan
deb8e117ad After login/register, get the latest subscription 2019-05-05 11:28:42 -07:00
Girish Ramakrishnan
9c3cae5eca lint: quotes 2019-05-05 09:05:06 -07:00
Girish Ramakrishnan
1fbbeba5bc Get subscription first and then get apps 2019-05-05 08:16:33 -07:00
Girish Ramakrishnan
8317972078 Fix typo when fetching groups 2019-05-05 07:40:11 -07:00
Girish Ramakrishnan
0a9947dbb9 No need to get unstable config
this is now handled in the backend
2019-05-05 07:38:34 -07:00
Girish Ramakrishnan
51521926e7 Use new registration API 2019-05-04 22:02:02 -07:00
Girish Ramakrishnan
8e08ac2ce1 Use new subscription API in settings controller 2019-05-04 21:57:53 -07:00
Girish Ramakrishnan
fec82d127e Use the new getSubscription API in main controller 2019-05-04 21:57:49 -07:00
Girish Ramakrishnan
ceb0770ea0 Add the subscription API 2019-05-04 19:22:24 -07:00
Girish Ramakrishnan
34eadebe00 Remove spaces code 2019-05-04 18:43:59 -07:00
Girish Ramakrishnan
e7f614cdf3 Remove unused caas functions 2019-05-04 18:23:32 -07:00
Girish Ramakrishnan
18507f79b1 Use the new appstore API 2019-05-04 18:22:41 -07:00
Johannes Zellner
ee1c7dbf03 Mark unstable apps in the appstore view 2019-05-03 15:56:59 +02:00
Girish Ramakrishnan
868af95ff2 appstore: remove unused getCloudronDetails 2019-05-02 15:28:07 -07:00
Johannes Zellner
01f59d39e0 Support unstable app listing setting in appstore view 2019-04-29 14:58:42 +02:00
Johannes Zellner
0226a5603d Add settings UI to enable unstable apps listing 2019-04-27 22:44:41 +02:00
Girish Ramakrishnan
1629be3788 Add tag placeholder 2019-04-25 10:49:07 -07:00
Johannes Zellner
480bc630da show app label if present 2019-04-24 14:31:52 +02:00
Johannes Zellner
165cc279de Allow to set app label 2019-04-24 14:25:37 +02:00
Girish Ramakrishnan
2ec5a2acff List unstable apps by default 2019-04-23 21:16:11 -07:00
Girish Ramakrishnan
6914e83dde Typo 2019-04-23 15:47:20 -07:00
Girish Ramakrishnan
263762c0bc relay: Add UI to accept self-signed certs 2019-04-23 15:44:18 -07:00
Girish Ramakrishnan
f8b8a574a6 Add new provider with no auth 2019-04-22 17:00:34 -07:00
Girish Ramakrishnan
79c80b351d relay: remove hardcoding of providers 2019-04-22 16:56:43 -07:00
Girish Ramakrishnan
2c86fb17fc Revert "Allow empty mail relay username/password"
This reverts commit 2680b415c6.
2019-04-22 15:46:07 -07:00
Girish Ramakrishnan
e205ffafdf Fix wording 2019-04-22 11:15:57 -07:00
Johannes Zellner
2680b415c6 Allow empty mail relay username/password 2019-04-22 14:48:32 +02:00
Johannes Zellner
62d8b35545 Bring back old update badge 2019-04-16 09:21:20 +02:00
Johannes Zellner
2b578efdd6 Do not add empty app tags 2019-04-15 15:44:02 +02:00
Johannes Zellner
a7f37df34d only show tag or domains filter if any are available 2019-04-15 14:38:35 +02:00
Johannes Zellner
3edb119422 Remove unused function 2019-04-15 14:36:15 +02:00
Johannes Zellner
07d4d5051a Simplify the app grid filter to basic dropdowns for now 2019-04-15 14:31:12 +02:00
Girish Ramakrishnan
0b8e5a75f1 Fix backup route 2019-04-13 18:09:56 -07:00
Girish Ramakrishnan
f263c73df7 Add scaleway object storage 2019-04-12 10:56:12 -07:00
Johannes Zellner
f89f201764 Show initial tag sidebar 2019-04-12 11:06:56 +02:00
Johannes Zellner
9f8dcdf8ea Revert "Initial attempt to show tags on the apps if any"
This reverts commit f3baf31dcd.
2019-04-12 09:52:47 +02:00
Johannes Zellner
f3baf31dcd Initial attempt to show tags on the apps if any 2019-04-11 18:43:03 +02:00
Johannes Zellner
a9400785ca Add ability to attach freeform text tags to apps 2019-04-11 18:43:01 +02:00
Johannes Zellner
7c76ad2088 Fix reboot button explanation to not confuse the user when reboot is in fact required 2019-04-09 11:17:11 +02:00
Girish Ramakrishnan
6a5839d8cd Add sftp logs 2019-04-05 11:01:47 -07:00
Girish Ramakrishnan
744f39623f Make restart button always animate 2019-04-05 10:59:46 -07:00
Girish Ramakrishnan
97e57c74e4 Update progress every 3 secs 2019-04-03 11:46:02 -07:00
Johannes Zellner
ff0d6b658b Simplify app icon handling
Previously we even attempted to use an appstore origin icon
2019-04-03 14:43:22 +02:00
Johannes Zellner
1c946a438d Simplify app grid item dom 2019-04-02 13:17:30 +02:00
Johannes Zellner
9b047a1927 Simplify the update indicator 2019-04-02 13:11:00 +02:00
Johannes Zellner
3fc6141d57 Use exact domain matching to filter apps for a domain 2019-04-02 12:41:23 +02:00
Johannes Zellner
daf7e2313b Reduce domain header font size 2019-03-25 16:33:49 +01:00
Johannes Zellner
75642d785e Only show the headers if we have more than one domain with apps 2019-03-25 16:32:15 +01:00
Johannes Zellner
2621b5c047 Ensure we set the correct display type on the app actions 2019-03-25 16:31:49 +01:00
Johannes Zellner
57cb9a1d0b Only show domains where apps are installed at 2019-03-25 16:17:53 +01:00
Johannes Zellner
6318ae046c Make app actions always visible on mobile
Not great at all, but until we have a better idea at least it is
functional
2019-03-25 16:01:14 +01:00
Johannes Zellner
57a41cde9d Group apps by domain 2019-03-25 10:42:31 +01:00
Johannes Zellner
ac86b7a954 Add sftp description in dashboard 2019-03-20 21:29:24 -07:00
Johannes Zellner
0bc500e34f Add robots.txt template 2019-03-20 09:53:10 -07:00
Johannes Zellner
a60e065e43 Make the documentation link a more visible button 2019-03-20 09:32:33 -07:00
Johannes Zellner
5563b6a786 Show upstream version if available in the info dialog 2019-03-20 09:22:54 -07:00
Girish Ramakrishnan
0345c52aba Add noop relay backend
Part of cloudron/box#622
2019-03-15 14:25:19 -07:00
Girish Ramakrishnan
05c858df9e Default to DO for mp image 2019-03-11 18:44:48 -07:00
Girish Ramakrishnan
0b4ef21762 DO typo 2019-03-11 18:44:38 -07:00
Girish Ramakrishnan
64a58921a8 caas: show notifications
update notification will be moved to backend
2019-03-07 13:25:20 -08:00
Girish Ramakrishnan
b96098b909 Fix wording a bit more 2019-03-06 09:56:15 -08:00
Johannes Zellner
b1dbb2c408 Show when a subscription is already canceled 2019-03-06 15:54:51 +01:00
Johannes Zellner
3b1a08c67e Add pretty date only showing date 2019-03-06 15:54:21 +01:00
Johannes Zellner
07d37e133f more users is not part of the paid plans 2019-03-06 14:32:29 +01:00
Girish Ramakrishnan
161eb8bef9 reduce the timeout 2019-03-04 21:47:14 -08:00
Girish Ramakrishnan
eb518c673c Revert "Revert "Hide dataDir setting for this release""
This reverts commit 347c8a8716.
2019-03-04 18:16:40 -08:00
Girish Ramakrishnan
6f32a0d6de Use encodeURIComponent for encoding query params 2019-03-04 12:59:41 -08:00
Girish Ramakrishnan
b0684ce29c encode the filename 2019-03-04 11:54:41 -08:00
Girish Ramakrishnan
b2c8a4d8ef Pass crashId to view crash logs 2019-03-01 16:35:52 -08:00
Johannes Zellner
d09ac5bcc6 stop notification click event propagation 2019-02-28 15:56:19 +01:00
Girish Ramakrishnan
c25c3e9daa never show hyphenatedSubdomains UI
can't think of a case where this is required in the UI. for the hosting
provider, they will have to do API automation for the initial domain
setup anyway.
2019-02-26 17:16:01 -08:00
Johannes Zellner
c1cb5c36a1 Remove unnecessary elements 2019-02-24 19:18:43 +01:00
Johannes Zellner
20118f941e Show clipboard copy indication 2019-02-24 19:17:38 +01:00
Johannes Zellner
f40eee4577 Update package lock 2019-02-24 19:16:57 +01:00
Girish Ramakrishnan
bee05afc87 Add button to copy the backup id 2019-02-23 19:33:48 -08:00
Girish Ramakrishnan
efdc533849 Update to gulp 4 2019-02-21 14:54:16 -08:00
Girish Ramakrishnan
1f7c6d59c1 Update modules for latest node 2019-02-21 14:14:28 -08:00
Girish Ramakrishnan
981622f414 Add games section 2019-02-21 10:17:41 -08:00
Girish Ramakrishnan
347c8a8716 Revert "Hide dataDir setting for this release"
This reverts commit 7424a226c9.
2019-02-19 14:52:24 -08:00
Girish Ramakrishnan
4542564709 Move reboot required check to server side notification 2019-02-19 09:15:15 -08:00
Girish Ramakrishnan
cb889ce06d token now has an id 2019-02-15 14:21:10 -08:00
Girish Ramakrishnan
db54a305b0 Token is now shown again anymore 2019-02-15 13:29:16 -08:00
Girish Ramakrishnan
8ccf17543a abbreviate version 2019-02-11 14:53:23 -08:00
Girish Ramakrishnan
72e99885aa Fix wording 2019-02-11 14:50:03 -08:00
Girish Ramakrishnan
18d2a9cab6 do not use audit source to generate the details 2019-02-11 14:38:05 -08:00
Girish Ramakrishnan
9c57702afc keep it sorted 2019-02-11 13:03:12 -08:00
Girish Ramakrishnan
b708eb94d2 Add app up event 2019-02-11 12:34:47 -08:00
Girish Ramakrishnan
82c5531d04 render notification message as markdown 2019-02-10 23:11:14 -08:00
Girish Ramakrishnan
e6f49b2d3b Add Email troubleshooting links 2019-02-10 21:04:42 -08:00
Girish Ramakrishnan
8ac97e2c8f Add help links in the backup ui 2019-02-10 17:05:11 -08:00
Johannes Zellner
db7174b0f3 Fixup long tooltip in setup email field 2019-02-10 08:52:54 +01:00
Girish Ramakrishnan
a47911048c use data.fqdn to show full domain name 2019-02-09 16:49:06 -08:00
Girish Ramakrishnan
5a2bdbf966 Fix link 2019-02-09 16:41:18 -08:00
Girish Ramakrishnan
aa562228ef plural 2019-02-09 16:34:56 -08:00
Girish Ramakrishnan
98a70aedf2 Add doc links for zone name and cert provider 2019-02-09 10:17:01 -08:00
Girish Ramakrishnan
9b9da5664b namecheap: use token instead of ApiKey 2019-02-08 20:33:21 -08:00
Girish Ramakrishnan
2f2314d2f8 Clarify 2019-02-08 14:54:24 -08:00
Girish Ramakrishnan
715ebf0747 Remove password max-length restriction
The backend has a limit of 256
2019-02-08 09:50:41 -08:00
Johannes Zellner
bb0443b967 Attempt to parse the notification message as json and show accordingly 2019-02-08 14:05:31 +01:00
Johannes Zellner
2cf0b528f0 Ensure the notification badge has plenty of space 2019-02-08 14:05:31 +01:00
Girish Ramakrishnan
6a95d481f0 Fix domain setup help links 2019-02-06 16:11:24 -08:00
Girish Ramakrishnan
d281b21832 bump license year 2019-02-06 15:36:06 -08:00
Johannes Zellner
1d5cf43e68 Remove unused style selector 2019-02-06 17:29:23 +01:00
Johannes Zellner
6d6b2300a8 Make danger color more popping 2019-02-06 17:28:01 +01:00
Johannes Zellner
640ee55772 Handle notifications without an eventId 2019-02-06 16:33:57 +01:00
Johannes Zellner
7ec12f487b Remove client side backup configuration checks 2019-02-06 15:48:35 +01:00
Johannes Zellner
63b42d64b1 Cleanup some code 2019-02-05 16:41:17 +01:00
Johannes Zellner
667506172a Do not rely on the whole app object in the event but use appId 2019-02-05 16:40:46 +01:00
Girish Ramakrishnan
518bb74fbf Add dashboard domain update event 2019-02-04 20:24:23 -08:00
Girish Ramakrishnan
9038538718 inform user about email records as well 2019-02-04 20:02:26 -08:00
Girish Ramakrishnan
5234f50453 Show email UI even if domain is disabled
This way when a user tries to delete a domain, he can still clear
the mailboxes.

Fixes cloudron/box##610
2019-01-31 12:24:27 -08:00
Girish Ramakrishnan
6eabf73ece typo 2019-01-25 10:15:17 -08:00
Johannes Zellner
651d01564d Add link to docs when using namecheap for email 2019-01-25 13:39:42 +01:00
Girish Ramakrishnan
52cdec8d3c Pass the task id to stopTask 2019-01-24 15:56:17 -08:00
Girish Ramakrishnan
998c9bdeb7 clone: Do not send disabled ports
fixes cloudron/box#611
2019-01-24 10:06:00 -08:00
Girish Ramakrishnan
318ee89e89 restore: Add missing exoscale region dropdown 2019-01-23 18:04:43 -08:00
Johannes Zellner
031d7a1f18 Load eventlog details per notification 2019-01-23 17:04:28 +01:00
Girish Ramakrishnan
7424a226c9 Hide dataDir setting for this release
Let's keep this hidden till we fix the app repair issue
2019-01-22 11:38:53 -08:00
Johannes Zellner
30a1997fd9 Add missing html bits 2019-01-22 20:03:26 +01:00
Johannes Zellner
778ea0b720 add namecheap dns provider to dns setup 2019-01-22 16:45:48 +01:00
Johannes Zellner
353517f9c6 Add hint about IP whitelisting for namecheap 2019-01-22 14:35:17 +01:00
Johannes Zellner
e651b2ee13 Add namecheap to domain config 2019-01-22 11:26:24 +01:00
Johannes Zellner
018b3a876f We use eslint by now, so make it as happy as it can be 2019-01-22 10:54:03 +01:00
Girish Ramakrishnan
1b9586011e Fix twitter icon in restore UI 2019-01-21 11:56:37 -08:00
Girish Ramakrishnan
cb856ce2bb Fix error handling and tab focus 2019-01-19 22:08:29 -08:00
Johannes Zellner
8d3c1c9f9e Add more event types 2019-01-19 15:53:49 +01:00
Girish Ramakrishnan
1ec0f67b29 dataDir can be empty to revert back 2019-01-18 15:18:59 -08:00
Girish Ramakrishnan
093491c5b4 Make dataDir configurable 2019-01-17 09:21:38 -08:00
Johannes Zellner
56191d0cd9 Better text for app down eventlog item 2019-01-17 17:27:26 +01:00
Johannes Zellner
7342268eb8 Support app oom in eventlog 2019-01-17 17:23:34 +01:00
Johannes Zellner
3a09cbf42b Add app oom event type 2019-01-17 15:49:53 +01:00
Johannes Zellner
b268368e3d Make linter happy 2019-01-17 13:26:47 +01:00
Johannes Zellner
59c8211c41 Do not show notification bubbles for notification items 2019-01-17 13:26:17 +01:00
Johannes Zellner
14560fff0a Hide notification action button 2019-01-17 13:21:54 +01:00
Johannes Zellner
adf3172ebb Speed up user listing by performing parallel requests 2019-01-16 14:27:32 +01:00
Johannes Zellner
4ead9cbf6a Remove leftover copynpasted string 2019-01-16 13:16:10 +01:00
Girish Ramakrishnan
0863dc785f Just pass through all the data 2019-01-15 11:13:04 -08:00
Johannes Zellner
342538358d add pagination and filter panel to users view 2019-01-15 13:30:42 +01:00
Johannes Zellner
a8b79055ef Better fix for tooltip overflow 2019-01-15 13:30:42 +01:00
Girish Ramakrishnan
ec3be4c36a s3: Add Paris/Stockholm/Osaka 2019-01-14 09:57:50 -08:00
Girish Ramakrishnan
0a2ef0e041 update events in activitiy view 2019-01-14 09:29:00 -08:00
Johannes Zellner
e7b623ea16 Use bootstrap tooltips in users view 2019-01-14 17:02:36 +01:00
Johannes Zellner
87777017a0 Ensure tooltips don't wrap text 2019-01-14 17:02:23 +01:00
Johannes Zellner
bf2c7a18d1 Until the ui supports full pagination list up to 1k users 2019-01-14 16:57:41 +01:00
Girish Ramakrishnan
b5505bcd87 Fixup restore eventlog 2019-01-13 14:54:19 -08:00
Girish Ramakrishnan
bdf9fbae71 Fixup text 2019-01-12 10:08:32 -08:00
Girish Ramakrishnan
04c1afc9ce Add dyndns event 2019-01-12 09:58:30 -08:00
Girish Ramakrishnan
458c51bdaa Improve eventlog messages 2019-01-11 12:48:51 -08:00
Girish Ramakrishnan
90a736ba43 mailboxes: owner may not exist 2019-01-10 14:25:08 -08:00
Johannes Zellner
661ce4fc1d Ensure we callback if the request was killed by the browser 2019-01-10 14:37:43 +01:00
Johannes Zellner
b764f1c861 For now make the notification bubble action go to notification view 2019-01-10 13:29:59 +01:00
Johannes Zellner
182949d8d2 Skip already acknowledged notifications on clear 2019-01-09 17:36:33 +01:00
Johannes Zellner
a879bdeb47 Ensure busy states are reflected in the ui 2019-01-09 17:26:44 +01:00
Johannes Zellner
9c66a4ef4e Rework the notification style now that it is in its own view 2019-01-09 17:16:41 +01:00
Johannes Zellner
d2d75b8e41 Move notifications into a separate view 2019-01-09 15:18:10 +01:00
Girish Ramakrishnan
e36c15f770 lint 2019-01-08 20:46:39 -08:00
Girish Ramakrishnan
8dc6da2b7a Escape html tags
In streaming view, logs like <foo@bar.com> was not appearing
2019-01-08 19:49:52 -08:00
Girish Ramakrishnan
d3ae252740 Use the smart host term 2019-01-08 14:53:23 -08:00
Girish Ramakrishnan
29f48bcba6 Use -1 to download full logs
Part of cloudron/box#604
2019-01-08 13:13:39 -08:00
Girish Ramakrishnan
e6fe5adca7 Use lines argument 2019-01-08 13:12:37 -08:00
Girish Ramakrishnan
82a96ec91d Keep it alphabetical 2019-01-08 10:24:16 -08:00
Johannes Zellner
db02cbb575 Always refresh notifications 2019-01-08 14:33:47 +01:00
Johannes Zellner
749dd20704 Poll for new notifications every 10sec 2019-01-08 14:08:29 +01:00
Johannes Zellner
b9db6040f4 Show label if no new notifications exist 2019-01-08 13:49:21 +01:00
Johannes Zellner
c9628c0f75 Do not show ack button on old notifications 2019-01-08 13:45:49 +01:00
Johannes Zellner
979af88a40 Make notification badge a friendlier green 2019-01-08 13:45:49 +01:00
Johannes Zellner
98b4cd330f Add button to show older notifications 2019-01-08 13:45:40 +01:00
Johannes Zellner
5ab390c3db Show notification time 2019-01-08 13:24:05 +01:00
Johannes Zellner
71eaf9966f Improve notification view layout and add relevant actions 2019-01-08 13:18:35 +01:00
Johannes Zellner
9653d07ae2 Rename activity log to eventlog 2019-01-08 12:42:03 +01:00
Johannes Zellner
f1663d0fbf Do not make notifications persistent 2019-01-08 12:41:52 +01:00
Johannes Zellner
38cb2201a9 Update toplevel notification status 2019-01-08 12:36:08 +01:00
Johannes Zellner
fa04bea64b List unread notifications in accounts view 2019-01-07 18:05:02 +01:00
Johannes Zellner
2bc66af55d Add notification ack api wrapper 2019-01-07 18:04:52 +01:00
Johannes Zellner
db5892d0ae Make linter happy 2019-01-07 17:34:10 +01:00
Johannes Zellner
59c7c1e302 Use the new notification onClick api in places 2019-01-07 17:30:01 +01:00
Johannes Zellner
48f63ec761 Update angular-ui-notification and show unread notifications 2019-01-07 17:23:26 +01:00
Girish Ramakrishnan
4051e34e20 Fix text 2019-01-06 16:25:25 -08:00
Girish Ramakrishnan
428bd43d60 Fix change dashboard domain UI issues 2019-01-06 16:02:05 -08:00
Girish Ramakrishnan
67415ff715 Check if scope is already destroyed 2019-01-06 15:29:38 -08:00
Girish Ramakrishnan
fbc494abc9 Hello 2019! 2019-01-04 19:37:51 -08:00
Girish Ramakrishnan
0816af3cf1 Use new support API
Part of cloudron/box#600
2018-12-19 13:27:59 -08:00
Girish Ramakrishnan
bb575fff5b Fix feedback API route 2018-12-19 10:58:50 -08:00
Girish Ramakrishnan
cbe632839c Add UI to switch domain 2018-12-18 15:27:26 -08:00
Girish Ramakrishnan
7c972758af Show progress message in setup and restore 2018-12-16 11:04:25 -08:00
Girish Ramakrishnan
236f66f56f Fix create invite post request
(cherry picked from commit 0688c272c2)
2018-12-15 09:28:35 -08:00
Girish Ramakrishnan
a485df2f79 Fix usage of webadminStatus 2018-12-14 16:34:57 -08:00
Girish Ramakrishnan
54b9154457 post requires extra data argument
broken by e6ad14f8
2018-12-14 16:33:10 -08:00
Girish Ramakrishnan
37aabcee4f Show the renew certificates header 2018-12-13 15:54:06 -08:00
Girish Ramakrishnan
b2d18560be this is handled by the managed case 2018-12-13 10:52:08 -08:00
Girish Ramakrishnan
1429aa1edc more caas removal 2018-12-13 10:50:37 -08:00
Girish Ramakrishnan
5d4f942d46 remove caas plan change UI 2018-12-13 09:36:21 -08:00
Johannes Zellner
30ea7e854d Fix wording 2018-12-13 13:58:08 +01:00
Johannes Zellner
907f82338e Fix twitter logo on setup screens 2018-12-12 08:49:28 +01:00
Girish Ramakrishnan
dcb0160b64 Remove blank line 2018-12-11 11:04:36 -08:00
Girish Ramakrishnan
fccd7fa438 Add a progress bar for the renewal task 2018-12-11 10:55:32 -08:00
Girish Ramakrishnan
c39711a87e Remove unused fields 2018-12-11 10:43:02 -08:00
Johannes Zellner
a8de003cf0 Remove obsolete js-update gulp task 2018-12-11 19:25:26 +01:00
Johannes Zellner
6db54fc3b5 remove uglifier
The resulting code was actually a tiny bit larger
2018-12-11 19:25:26 +01:00
Girish Ramakrishnan
d058536011 Fix indent 2018-12-11 10:19:13 -08:00
Girish Ramakrishnan
02ad4ba98d return taskId in Client.renewCerts 2018-12-11 10:19:13 -08:00
Johannes Zellner
a68a76112c Fix eventlog usage 2018-12-11 19:03:42 +01:00
Girish Ramakrishnan
975c545081 Make it a separate section 2018-12-11 10:00:56 -08:00
Johannes Zellner
fcfee9082b assert if the rest wrappers are misused 2018-12-11 18:55:25 +01:00
Johannes Zellner
e6ad14f8d4 Rework the rest wrapper usage and add offline banner 2018-12-11 18:17:53 +01:00
Johannes Zellner
1670f15732 Use toplevel cert renewal api for all domains 2018-12-11 12:41:42 +01:00
Johannes Zellner
5cd696792b Improve inline update progress layout 2018-12-10 16:50:40 +01:00
Johannes Zellner
fbc399f5fa sort services by name 2018-12-10 11:36:47 +01:00
Johannes Zellner
3d6413ae05 Finalize rename addon -> service 2018-12-10 11:36:47 +01:00
Girish Ramakrishnan
97120a6b04 Fixup update UI to use task id 2018-12-09 12:06:28 -08:00
Johannes Zellner
226162ee57 Give backup progress detail more horizontal space 2018-12-09 21:01:46 +01:00
Johannes Zellner
a888ec265f Fix backup progress display layout 2018-12-09 20:59:32 +01:00
Girish Ramakrishnan
6fb7555f01 fix task logs 2018-12-08 21:58:23 -08:00
Girish Ramakrishnan
a8d0e25866 Update task status by id 2018-12-08 21:17:36 -08:00
Girish Ramakrishnan
970f7fe69b Add flag for per-app automatic update 2018-12-07 09:38:55 -08:00
Girish Ramakrishnan
c507df902e create recvmail mailbox automatically 2018-12-06 22:31:38 -08:00
Girish Ramakrishnan
7fa5ef8165 Do not filter out app mailboxes 2018-12-06 21:55:04 -08:00
Girish Ramakrishnan
92cb768c4b Fix reboot message 2018-12-06 10:12:21 -08:00
Johannes Zellner
8ec406c2e0 Hide empty memory usage bar for unsupported services 2018-12-06 14:32:39 +01:00
Johannes Zellner
9473c108f0 Support unbound logs in logviewer 2018-12-05 16:22:43 +01:00
Girish Ramakrishnan
14c43d9f7e Show error message directly 2018-12-04 14:12:35 -08:00
Girish Ramakrishnan
ce9a03a5a8 Check updateStatus on ready 2018-12-04 14:05:55 -08:00
Johannes Zellner
04e8b14fc4 Give more space in the error page 2018-12-04 16:35:58 +01:00
Girish Ramakrishnan
43b747676c addon -> service
some day we will also add nginx, unbound etc here
2018-12-02 18:55:05 -08:00
Girish Ramakrishnan
bd40cf9947 Move server below the addons
server restart will ideally be rarely used.
2018-12-02 18:51:34 -08:00
Girish Ramakrishnan
203b31d81f Handle split of addon and services 2018-12-02 18:45:33 -08:00
Girish Ramakrishnan
0430fb2772 rename addon route to services 2018-12-02 17:46:58 -08:00
Girish Ramakrishnan
d3746d6859 Move system below support 2018-11-30 21:11:05 -08:00
Girish Ramakrishnan
d8dfa89f87 rework update ui
- this is not modal anymore
- can be canceled
2018-11-30 20:55:37 -08:00
Girish Ramakrishnan
cbdb90d06b do not use result 2018-11-30 20:22:53 -08:00
Girish Ramakrishnan
63e040ea79 Use active field instead of percent 2018-11-29 23:14:00 -08:00
Girish Ramakrishnan
fd1a0f3b0a result is not used anymore in backup view 2018-11-29 23:00:30 -08:00
Johannes Zellner
abaf8a676c Fix edit icons in settings and account view 2018-11-29 16:43:28 +01:00
Johannes Zellner
0b96fc4701 Use Cloudron style fallback icon 2018-11-29 12:04:56 +01:00
Johannes Zellner
400e210d37 Show memory usage for addons 2018-11-28 12:53:35 +01:00
Johannes Zellner
ea0c697ad3 Make admins go to the docs in error.html 2018-11-27 10:48:31 +01:00
Johannes Zellner
edf8c32a0f caas errorCodes in error.html are gone now 2018-11-26 20:05:55 +01:00
Johannes Zellner
ccef5da7d9 Minor fixes to the error.html 2018-11-26 19:32:19 +01:00
Johannes Zellner
ddf213aec4 Remove logs from support view, they are now in system view 2018-11-26 14:59:39 +01:00
Johannes Zellner
8bd9237951 Use addon log routes in logviewer 2018-11-26 14:49:50 +01:00
Johannes Zellner
ae488312a1 Add ui bits to be able to reboot the server 2018-11-26 09:24:58 +01:00
Johannes Zellner
1ed45656e4 Rename addons view to system
We can rename the menu entry further but I wasn't sure if diagnostics
will be nicer. Diagnostics kinda overlaps with the graphs there and all
2018-11-26 08:59:31 +01:00
Johannes Zellner
07edcc5f94 Show notification if reboot is required 2018-11-26 08:48:58 +01:00
Johannes Zellner
1783059fd4 Better addon restart feedback 2018-11-22 22:12:00 +01:00
Johannes Zellner
aab766e8ff Add button to reset addon memory to platform defaults 2018-11-21 17:06:01 +01:00
Johannes Zellner
158514f334 Replace addon start/stop with restart 2018-11-21 16:14:02 +01:00
Johannes Zellner
77d29c3728 Patch up the addon memory configuration 2018-11-21 15:57:26 +01:00
Johannes Zellner
3c3383ac03 Show memory config in list 2018-11-20 17:04:53 +01:00
Johannes Zellner
6e46240fd7 Add addon configure dialog 2018-11-20 17:01:46 +01:00
Johannes Zellner
d01c46bfee Refresh addon status automatically 2018-11-20 14:47:39 +01:00
Johannes Zellner
1e5007ec8b Add logs button and set default action in repair dialog 2018-11-20 11:39:32 +01:00
Johannes Zellner
deed95e9a9 Update progress needs to be checked some other way now 2018-11-20 11:36:17 +01:00
Johannes Zellner
082323511a Handle addon state correctly 2018-11-20 11:09:20 +01:00
Johannes Zellner
c07224cab5 Fix tooltips 2018-11-20 11:09:20 +01:00
Johannes Zellner
1604a96f41 Support addon logs in logviewer 2018-11-20 11:09:20 +01:00
Johannes Zellner
50963f00c0 Add basic UI controls for start, stop, logs and show status 2018-11-20 11:09:20 +01:00
Johannes Zellner
699db93b18 Add initial addons view 2018-11-20 11:09:20 +01:00
Girish Ramakrishnan
85e467581c Use the new task API 2018-11-19 17:34:14 -08:00
Girish Ramakrishnan
42e4588e9c Fix backup route 2018-11-19 14:40:47 -08:00
Girish Ramakrishnan
93c194cff7 Add button to stop backup 2018-11-17 20:47:51 -08:00
Girish Ramakrishnan
00450dc048 Fix backup API routes 2018-11-17 20:47:51 -08:00
Johannes Zellner
c319fd5862 Fix all animated spinners for new fontawesome 2018-11-16 17:03:21 +01:00
Johannes Zellner
5048b5b585 Fixup the twitter icon 2018-11-15 23:28:06 +01:00
Johannes Zellner
e7f24084af Fixup all missed icons 2018-11-15 18:07:18 +01:00
Johannes Zellner
c57b9b4fa3 Update fontawesome from v3 to v5 2018-11-15 17:42:29 +01:00
Girish Ramakrishnan
ac5b7a4469 Add certificate.new event 2018-11-14 20:37:58 -08:00
Girish Ramakrishnan
884faa0e27 Add note on where to check cert status 2018-11-14 20:22:14 -08:00
Girish Ramakrishnan
50b4b7bb92 this event is no more 2018-11-14 11:20:11 -08:00
Girish Ramakrishnan
cf259ace47 more events 2018-11-10 01:34:50 -08:00
Girish Ramakrishnan
270389a18c Add new domain events 2018-11-10 01:09:06 -08:00
Girish Ramakrishnan
a340eea769 Add new mail events 2018-11-10 00:32:37 -08:00
Girish Ramakrishnan
22589e7103 Fix the checkbox 2018-11-09 11:24:19 -08:00
Girish Ramakrishnan
2b6423d3b7 Move the dyndns setting to the domains view 2018-11-09 10:37:50 -08:00
Girish Ramakrishnan
50bf193fd1 Hide "remote support" option for managed cloudrons 2018-11-06 21:50:10 -08:00
Girish Ramakrishnan
c2ba059ced Remove the "not recommended" 2018-11-06 14:04:30 -08:00
Johannes Zellner
856ed0c765 Use config.managed for dyndns ui 2018-11-01 09:35:22 +01:00
Johannes Zellner
a73681ce8b settings view is only available to admins no need to check here 2018-11-01 09:34:28 +01:00
Johannes Zellner
1426ed952b Hide dynamic dns settings for caas and non operators 2018-11-01 09:33:46 +01:00
Johannes Zellner
d6bf6eb0a0 Add dynamic dns settings 2018-11-01 09:33:46 +01:00
Girish Ramakrishnan
97b24079f7 Show lock icon for locked domains 2018-10-31 11:01:10 -07:00
Girish Ramakrishnan
707f84839e typoe 2018-10-30 23:52:09 -07:00
Girish Ramakrishnan
643d2f3fad typo 2018-10-30 22:51:59 -07:00
Girish Ramakrishnan
92660e037d replace operatorActions with managed 2018-10-30 21:07:37 -07:00
Girish Ramakrishnan
2e6a0411fb Never show hyphenated feature for now 2018-10-30 20:52:03 -07:00
Girish Ramakrishnan
5d57a5fabb Use new setup route 2018-10-30 14:15:43 -07:00
Girish Ramakrishnan
cb90ad803b provider is never empty anymore 2018-10-29 19:20:16 -07:00
Girish Ramakrishnan
937e8ce1ed Add the new exoscale-sos regions 2018-10-27 14:44:13 -07:00
Girish Ramakrishnan
c1976d5b13 Cloudflare HTTP proxying works now 2018-10-26 15:03:27 -07:00
Girish Ramakrishnan
8070e88564 Add Certs to menu bar 2018-10-25 13:23:42 -07:00
Girish Ramakrishnan
15c0c691ff Add button to renew certs of a domain 2018-10-24 15:51:02 -07:00
Girish Ramakrishnan
f68912b466 copy/paste error
(cherry picked from commit 53fed09a5d)
2018-09-26 22:30:38 -07:00
Girish Ramakrishnan
dfa4e20a8f Set default cert provider to wildcard
Change it to non-wildcard for manual/noop/wildcard dns
2018-09-26 19:53:09 -07:00
Girish Ramakrishnan
ee1a194305 Change the cert provider selection box text 2018-09-26 18:15:09 -07:00
Girish Ramakrishnan
0fa88855e5 Move information text out of advanced view 2018-09-26 18:10:21 -07:00
Girish Ramakrishnan
eda3d5c143 Remove invalid dns config notification
The issue is that this value is never really updated unless the box
code is restarted.

Instead, we will fix it to check all domains periodically and send
some email notification.

Fixes cloudron/box#586
2018-09-26 15:13:44 -07:00
Girish Ramakrishnan
b450efe5c2 Add SFO2 region in restore UI 2018-09-26 12:02:03 -07:00
Girish Ramakrishnan
ca76626d55 Add checkbox UI for mailbox location
part of cloudron/box#587
2018-09-25 11:27:43 -07:00
Girish Ramakrishnan
ed887953b6 typo 2018-09-12 20:19:40 -07:00
Girish Ramakrishnan
04debe3ea3 port80 requirement is more complex 2018-09-12 15:57:01 -07:00
Girish Ramakrishnan
4312096dd2 Add a wildcard provider option 2018-09-12 13:12:40 -07:00
Girish Ramakrishnan
94b079fa7b Show the title in port bindings instead of the long description 2018-09-12 11:22:02 -07:00
Girish Ramakrishnan
0373d86349 Fix error code 2018-09-10 11:22:29 -07:00
Girish Ramakrishnan
0f5c290785 we now return 424 2018-09-10 10:37:15 -07:00
Girish Ramakrishnan
c79f43bb27 do spaces: sfo2 is now available 2018-09-10 09:27:08 -07:00
Girish Ramakrishnan
184ad3bc4e wildcard dns is now a provider 2018-09-06 20:12:25 -07:00
Girish Ramakrishnan
aa0a4ae3e9 Handle locked domains 2018-09-05 23:23:27 -07:00
Girish Ramakrishnan
ff9c4b407f Add help text 2018-09-05 21:46:26 -07:00
Girish Ramakrishnan
c3b01d477e Typo 2018-09-05 21:31:44 -07:00
Girish Ramakrishnan
3c0641745b backups: Hide prefix for noop 2018-09-05 17:32:53 -07:00
Girish Ramakrishnan
7186a0c41b information text for hyphenated subdomain 2018-09-05 17:20:17 -07:00
Girish Ramakrishnan
4c3bc7450e domain: hide del button for admin domain 2018-09-05 17:15:30 -07:00
Girish Ramakrishnan
02f04e2d33 Disable various views for non-operators 2018-09-05 15:35:30 -07:00
Johannes Zellner
97b6e4c672 Just display subscription error message in the ui 2018-09-05 17:14:01 +02:00
Girish Ramakrishnan
2fd1caa2aa caas: Fix display of alternateDomain 2018-09-04 12:10:21 -07:00
Girish Ramakrishnan
cb25217c48 Fix the edition name 2018-08-31 08:06:04 -07:00
Johannes Zellner
ab70bc663d No need to show button to setup billing
Either it has a subscription or not, no trials anymore
This will not show any cc setup button now for other plans like
education
2018-08-29 23:24:24 +02:00
Johannes Zellner
0cfe931cd1 We do not have trials anymore 2018-08-29 23:22:47 +02:00
Girish Ramakrishnan
29bddb5fcb Fix derivation of adminFqdn 2018-08-28 22:35:02 -07:00
Girish Ramakrishnan
cb7d160346 Add more backup interval secs 2018-08-28 22:10:17 -07:00
Girish Ramakrishnan
507c8b8786 Add hyphenatedSubdomains checkbox to setup page 2018-08-28 21:59:03 -07:00
Girish Ramakrishnan
60107147c2 derive features from edition 2018-08-28 21:58:59 -07:00
Girish Ramakrishnan
be2afec86b spaces: Fix text 2018-08-28 20:32:07 -07:00
Girish Ramakrishnan
d316d216db spaces: use edition instead of setting 2018-08-28 19:36:26 -07:00
Girish Ramakrishnan
dd53d0d575 caas+spaces: location suffix fix 2018-08-28 14:22:40 -07:00
Girish Ramakrishnan
0f6c0a2ccd Use spaces suffix that replaces dots in username
This assumes usernames only have . or - but not both
2018-08-28 12:23:14 -07:00
Girish Ramakrishnan
937a165711 spaces: Strip the trailing username when configuring 2018-08-28 10:38:53 -07:00
Girish Ramakrishnan
22c402ca3d clone: subdomain hyphenation 2018-08-27 21:40:49 -07:00
Girish Ramakrishnan
eddbd4fddc Do the filter later 2018-08-27 21:06:07 -07:00
Girish Ramakrishnan
0e43ca31a3 spaces: add username suffix when installing apps 2018-08-27 20:45:09 -07:00
Girish Ramakrishnan
9c90a20b4d Get token name as input 2018-08-27 16:04:16 -07:00
Girish Ramakrishnan
764e7e7d1f Fix indent 2018-08-27 15:40:23 -07:00
Girish Ramakrishnan
0e8cb00233 Display token name 2018-08-27 15:34:46 -07:00
Girish Ramakrishnan
0a1a011338 Move the API token to account page
The OAuth page is less and less useful. Moreover, the tokens are
actually tied to the user and not for the system.
2018-08-27 15:26:52 -07:00
Girish Ramakrishnan
3dfcd9324d invite -> setup link 2018-08-27 15:08:09 -07:00
Girish Ramakrishnan
3e4ac4a0ca Keep it short (and abstract) 2018-08-27 13:50:13 -07:00
Girish Ramakrishnan
be1795d50d domains: make the certs setup more descriptive 2018-08-27 13:19:57 -07:00
Girish Ramakrishnan
0b0b06baa9 certs: Rename Self-Signed to custom 2018-08-27 12:04:55 -07:00
Girish Ramakrishnan
b789cd2af0 Fix incorrect title of invitation dialog 2018-08-27 11:43:24 -07:00
Johannes Zellner
0871403c0a Support hyphenated subdomains in install and configure dialogs 2018-08-22 17:25:27 +02:00
Johannes Zellner
53a34d9352 Support hyphenatedSubdomains property in domains view 2018-08-22 11:54:37 +02:00
Girish Ramakrishnan
fe23551b04 Show the doc link in the post install confirm body 2018-08-21 19:21:03 -07:00
Girish Ramakrishnan
484b6477d3 Fix duplicate id in app configure form 2018-08-20 09:44:30 -07:00
Girish Ramakrishnan
8ebe04c2ff Do not send invite email when invite button is pressed 2018-08-17 16:26:16 -07:00
Girish Ramakrishnan
672d6b0856 Add backup interval
Part of cloudron/box#568
2018-08-13 22:40:05 -07:00
Girish Ramakrishnan
0c066fafa2 remove backup default comment 2018-08-13 22:22:46 -07:00
Girish Ramakrishnan
6c574ead94 Make UDP ports configurable
Part of cloudron/box#504
2018-08-13 09:15:21 -07:00
Girish Ramakrishnan
31a62313bb Reconfigure email apps when email is enabled/disabled 2018-08-12 13:21:17 -07:00
Johannes Zellner
4dacf7064f Apps already use singular document tag 2018-08-06 22:22:49 +02:00
Johannes Zellner
e900e4de77 Add Documents category 2018-08-06 21:46:24 +02:00
Girish Ramakrishnan
4ce6939b79 spaces: show based on plan id 2018-08-06 10:53:16 -07:00
Girish Ramakrishnan
8430fd1473 Fix more errors in clone UI 2018-08-06 00:46:10 -07:00
Girish Ramakrishnan
ac7c54e273 Fix errors in the clone form 2018-08-06 00:34:40 -07:00
Girish Ramakrishnan
6c9a3b530d Display restore error on page load 2018-08-05 23:30:22 -07:00
Girish Ramakrishnan
2f2c70d1df Set the users when creating group 2018-08-05 22:19:54 -07:00
Girish Ramakrishnan
a78c991330 Give some fixed width to the columns 2018-08-05 22:10:45 -07:00
Girish Ramakrishnan
8f9349ec53 Remove double "this" 2018-08-05 21:43:34 -07:00
Girish Ramakrishnan
bc6be6a9ad Fix indent 2018-08-05 21:40:18 -07:00
Girish Ramakrishnan
a9b7c2795a Fix styling 2018-08-05 21:34:47 -07:00
Girish Ramakrishnan
cd81cc8cb8 Refine the text 2018-08-05 21:09:16 -07:00
Girish Ramakrishnan
473b35d807 Query backup config only for admins 2018-08-03 23:35:37 -07:00
Girish Ramakrishnan
0c04d5bfc8 spaces: fetch users/groups/domains 2018-08-03 23:27:21 -07:00
Girish Ramakrishnan
eed460f435 Fetch complete app object for owner 2018-08-03 23:00:25 -07:00
Girish Ramakrishnan
d742982973 spaces: default the access restriction to just the user 2018-08-03 22:51:53 -07:00
Girish Ramakrishnan
c8263077a2 appstore app object has no location or accessRestriction 2018-08-03 22:29:52 -07:00
Girish Ramakrishnan
eae01bdbd9 appId is not needed in configure route 2018-08-03 18:44:30 -07:00
Girish Ramakrishnan
1ebafbbc20 spaces: fixup user interface 2018-08-03 18:38:00 -07:00
Girish Ramakrishnan
a525bb0257 Missed this 2018-08-03 17:47:02 -07:00
Girish Ramakrishnan
cf5cf9e42f Remove usage of tokenScopes and caps 2018-08-03 10:13:57 -07:00
Girish Ramakrishnan
7969dff043 Add UI for enabling spaces 2018-08-03 09:44:56 -07:00
Girish Ramakrishnan
d73f7304b3 Copy admin flag 2018-08-03 09:34:58 -07:00
Johannes Zellner
4400b0117a Fix linter issues 2018-08-02 22:17:27 +02:00
Johannes Zellner
739c91b1c6 Do not throw errors if a group has a uid which is not yet known
This can happen if the users have not yet loaded fully
2018-08-02 22:16:57 +02:00
Girish Ramakrishnan
510115ade9 Show danger color if update fails 2018-08-01 17:02:09 -07:00
Girish Ramakrishnan
8c2d79b75e Show app id in info dialog 2018-08-01 12:37:17 -07:00
Johannes Zellner
1a31fb78e5 Add homescreen icons for mobile 2018-07-30 22:05:20 +02:00
Girish Ramakrishnan
97f4d5e3ac Show busy indicator when toggling email 2018-07-30 11:30:49 -07:00
Girish Ramakrishnan
d0b17f7e7b Delete any endpoint configuration when using s3 2018-07-30 07:29:22 -07:00
Girish Ramakrishnan
eb74aaff3b Display restore errors
Part of cloudron/box#505
2018-07-29 20:48:37 -07:00
Girish Ramakrishnan
9108b665a8 restore: show encrytion field for rsync format 2018-07-29 19:51:44 -07:00
Girish Ramakrishnan
e449147ed4 setup: Make it wider 2018-07-29 19:46:52 -07:00
Girish Ramakrishnan
53e82876dd setup: add link to hide advanced settings 2018-07-29 19:43:36 -07:00
Girish Ramakrishnan
dd4a4518b3 Allow backup key to be set for rsync format
Part of #440
2018-07-28 09:13:42 -07:00
Girish Ramakrishnan
a9e46c64b1 Show group members 2018-07-26 23:58:25 -07:00
Girish Ramakrishnan
fb85770fd3 admin group is now gone 2018-07-26 23:42:38 -07:00
Girish Ramakrishnan
9e9e651714 admin is now simply a flag 2018-07-26 15:54:21 -07:00
Girish Ramakrishnan
314da7ace8 Fix API of Client.createUser 2018-07-26 15:52:10 -07:00
Girish Ramakrishnan
54103ca120 Revert role support 2018-07-26 11:38:20 -07:00
Girish Ramakrishnan
be86a3022f Call the new setDnsRecords route 2018-07-25 10:52:06 -07:00
Girish Ramakrishnan
91ecab08da Fix typo 2018-07-24 22:40:27 -07:00
Girish Ramakrishnan
cae445556e Allow groups to be set during user add 2018-07-24 22:38:53 -07:00
Girish Ramakrishnan
8c2af87857 Fix coding style 2018-07-24 22:31:22 -07:00
Girish Ramakrishnan
2d44e356d3 Add user multi-select to group edit dialog 2018-07-24 22:25:44 -07:00
Girish Ramakrishnan
dec1931f07 Make groups a multiselect
With many groups, it overflows and very cluttered
2018-07-24 21:36:52 -07:00
Girish Ramakrishnan
46473c3756 Show displayName in user listing 2018-07-24 15:20:25 -07:00
Girish Ramakrishnan
cd893edfcf Add display name to user edit 2018-07-24 15:17:51 -07:00
Girish Ramakrishnan
84302c1739 Fix the text 2018-07-24 14:38:47 -07:00
Girish Ramakrishnan
d6f6b4bfe5 Display hostname in mail status
Many are copy/pasting the domain directly into the DNS UI and it fails.
2018-07-24 14:34:26 -07:00
Girish Ramakrishnan
8c6531b6fb Add Mailjet 2018-07-23 16:47:24 -07:00
Girish Ramakrishnan
f4993a7e58 Change redirection text 2018-07-16 18:39:22 -07:00
Girish Ramakrishnan
cc812c2177 Add user transfer event 2018-07-05 13:54:05 -07:00
Girish Ramakrishnan
e11dc028d1 Transfer deleted user's resources 2018-07-05 13:44:00 -07:00
Johannes Zellner
e314910a76 Ensure we uri encode the email query param for setup links 2018-07-04 11:10:13 +02:00
Johannes Zellner
ee9140c365 Use the correct object to reset alternateDomains 2018-07-03 18:04:25 +02:00
Johannes Zellner
ce4ccc21dd Set better defaults and placeholder text for alternate domains 2018-07-03 18:04:03 +02:00
Johannes Zellner
6108fcf17b Put the alternate domain settings behind a checkbox 2018-07-03 18:00:22 +02:00
Johannes Zellner
cd3fb77033 Add support for one alternate domain which redirects 2018-06-29 23:42:13 +02:00
Johannes Zellner
0697274311 Remove very odd unused line 2018-06-29 23:42:13 +02:00
Girish Ramakrishnan
11f5aaaf3b Pass on the tokenScopes 2018-06-29 09:09:25 -07:00
Girish Ramakrishnan
8f0b66bd98 Rework config routes
The config route now returns non-sensitive information under the
profile scope.

Caas config is a separate route

Update config is a separate route
2018-06-28 17:50:33 -07:00
Girish Ramakrishnan
3be660dcd9 If user has no appstore scope, we cannot get subscription info 2018-06-27 18:15:48 -07:00
Girish Ramakrishnan
3bb82d5e68 Use app ts to determine whether to refetch app 2018-06-26 19:54:18 -07:00
Girish Ramakrishnan
3f9f1480d3 The uninstall id gets cleared 2018-06-26 19:47:48 -07:00
Girish Ramakrishnan
948c446362 typo 2018-06-26 19:47:48 -07:00
Girish Ramakrishnan
25f888e0d8 Get detailed app information if user can manage apps 2018-06-26 17:56:23 -07:00
Girish Ramakrishnan
98661de24e Fix error message display in configure dialog 2018-06-26 17:35:48 -07:00
Girish Ramakrishnan
a833ceb737 Insert the app sorted into the cache 2018-06-26 17:12:55 -07:00
Girish Ramakrishnan
b41d0379f0 Only refresh the individual app that is being managed 2018-06-26 10:22:38 -07:00
Girish Ramakrishnan
df6da7dd1c logs: installedApps is not used 2018-06-26 10:21:43 -07:00
Girish Ramakrishnan
24ca5bc990 Refactor logic into _updateAppCache 2018-06-26 10:21:36 -07:00
Girish Ramakrishnan
e3e62b8407 refresh immediately 2018-06-26 10:19:50 -07:00
Girish Ramakrishnan
0c98e6f4ca Mark it as internal function 2018-06-26 08:34:05 -07:00
Girish Ramakrishnan
6034121695 Make clone return data 2018-06-26 08:33:04 -07:00
Girish Ramakrishnan
afe837e30a Remove used of Client.onApps 2018-06-25 20:15:24 -07:00
Girish Ramakrishnan
f3cf640e21 terminal: use Client.getApp instead of refreshInstalledApps 2018-06-25 19:19:56 -07:00
Girish Ramakrishnan
8d98cefcca terminal: Remove unused dropdown logic 2018-06-25 19:10:26 -07:00
Girish Ramakrishnan
bdf57a5c0a remove dead code 2018-06-25 19:10:00 -07:00
Girish Ramakrishnan
37f108d9f7 logs: remove dep on refreshInstalledApps 2018-06-25 18:58:11 -07:00
Girish Ramakrishnan
091663afe0 Add Client.getApp that uses REST API 2018-06-25 18:55:07 -07:00
Girish Ramakrishnan
a77918bef9 Client.onReady has already loaded the app list 2018-06-25 18:27:40 -07:00
Girish Ramakrishnan
f167714ea1 add note 2018-06-25 18:27:05 -07:00
Girish Ramakrishnan
1cab172169 Adapt UI logic to get user/group configuration for each user/group 2018-06-25 16:23:28 -07:00
Girish Ramakrishnan
35c3df5a18 Adapt UI logic to get domain configuration for each domain 2018-06-25 15:33:21 -07:00
Girish Ramakrishnan
b9a6f46543 Check for 403 for incorrect password 2018-06-18 18:57:00 -07:00
Girish Ramakrishnan
12b1909c7a Add roles UI creating and editing a group 2018-06-18 18:48:54 -07:00
Girish Ramakrishnan
5bd57b6dbd lint 2018-06-18 18:34:19 -07:00
Girish Ramakrishnan
961220be3f tokenScope -> tokenScopes 2018-06-18 15:09:16 -07:00
Girish Ramakrishnan
4db703aeb1 Make the UI capability based 2018-06-17 18:24:45 -07:00
Girish Ramakrishnan
cec1cc7086 scope -> tokenScope 2018-06-17 15:29:10 -07:00
Girish Ramakrishnan
2bacbe6701 caas: disable enable email button instead of hiding it 2018-06-16 11:28:13 -07:00
Girish Ramakrishnan
3c65d88c65 caas: disable editing managed domain 2018-06-16 11:22:41 -07:00
Girish Ramakrishnan
726a1c37cc caas: show the backups view, just not the configure button 2018-06-16 11:14:45 -07:00
Girish Ramakrishnan
63f2bbb253 wrong password is 401 2018-06-15 20:54:15 -07:00
Girish Ramakrishnan
7f11cc0daf add note 2018-06-15 17:16:50 -07:00
Girish Ramakrishnan
f32884b3b2 Add button for backup logs 2018-06-15 09:55:19 -07:00
Johannes Zellner
97465c1bd8 Last one to open the terminal in a new tab from within the logs view 2018-06-15 17:03:00 +02:00
Johannes Zellner
ce0a1ce38a Also open platform and email logs in a new tab instead of a window 2018-06-15 16:45:34 +02:00
Johannes Zellner
f5060a0d4f Open logs and terminal in a new tab instead of a window 2018-06-15 16:42:04 +02:00
Johannes Zellner
bb34c8a242 Ignore button clicks when post install is not yet confirmed
Angular does not remove the click handler on ng-disabled :-/
2018-06-15 13:39:33 +02:00
Johannes Zellner
34fd733bb7 Fix mouse cursor state in app grid 2018-06-15 13:39:07 +02:00
Johannes Zellner
19b65460ff Do not show postinstall if the app is not ready yet 2018-06-15 13:35:46 +02:00
Girish Ramakrishnan
edf277fcaf Feedback API has moved to cloudron scope 2018-06-14 20:04:38 -07:00
Girish Ramakrishnan
9db334c2a4 Show backup notification in main.js instead 2018-06-14 12:59:25 -07:00
Johannes Zellner
1039d9c95e Remove postinstall message from the appstore view
This is now shown on first click
2018-06-14 16:07:29 +02:00
Johannes Zellner
37c8b2b57f Make the user confirm the post install message on first time clicking the app icon 2018-06-14 15:46:55 +02:00
Girish Ramakrishnan
461fb0144e Fix wording 2018-06-13 12:25:29 -07:00
Girish Ramakrishnan
60a9c60f40 Fix typo preventing email from getting enabled 2018-06-12 19:18:47 -07:00
Girish Ramakrishnan
869a6b5a51 Add email to setupLink 2018-06-12 17:59:04 -07:00
Girish Ramakrishnan
133e101f83 Fix download logs button 2018-06-12 14:50:18 -07:00
Girish Ramakrishnan
6ecadb2308 Remove unused readFileLocally 2018-06-12 14:33:19 -07:00
Girish Ramakrishnan
0d3ff81d6c Fix UI jumpiness 2018-06-12 14:24:17 -07:00
Girish Ramakrishnan
e938886629 Add box logs to support view 2018-06-12 14:05:58 -07:00
Girish Ramakrishnan
aa32055aa8 lint 2018-06-12 13:37:31 -07:00
Girish Ramakrishnan
59481c37bc Remove redundant user.admin check 2018-06-12 13:31:23 -07:00
Girish Ramakrishnan
0b868dad2d remove the thanks (it is a bug report) 2018-06-12 13:20:08 -07:00
Girish Ramakrishnan
3c063a2263 Remove one section since I want to add the logs section 2018-06-12 12:55:44 -07:00
131 changed files with 21076 additions and 22005 deletions

View File

@@ -1,8 +0,0 @@
{
"node": true,
"browser": true,
"unused": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true
}

View File

@@ -1,5 +1,5 @@
The Cloudron Subscription license
Copyright (c) 2018 Cloudron UG
Copyright (c) 2020 Cloudron UG
With regard to the Cloudron Software:

View File

@@ -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.me (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.

View File

@@ -7,15 +7,79 @@ var argv = require('yargs').argv,
concat = require('gulp-concat'),
cssnano = require('gulp-cssnano'),
ejs = require('gulp-ejs'),
execSync = require('child_process').execSync,
gulp = require('gulp'),
rimraf = require('rimraf'),
sass = require('gulp-sass'),
serve = require('gulp-serve'),
sourcemaps = require('gulp-sourcemaps'),
uglify = require('gulp-uglify');
sourcemaps = require('gulp-sourcemaps');
gulp.task('3rdparty', function () {
gulp.src([
if (argv.help || argv.h) {
console.log('Supported arguments for "gulp develop":');
console.log(' --api-origin <cloudron api uri>');
console.log(' --revision <revision>');
console.log(' --appstore-web-origin <appstore web uri>');
console.log(' --appstore-api-origin <appstore api uri>');
process.exit(1);
}
const revision = argv.revision || '';
let apiOrigin = '';
if (argv.apiOrigin) {
if (argv.apiOrigin.indexOf('https://') === 0) apiOrigin = argv.apiOrigin;
else apiOrigin = 'https://' + argv.apiOrigin;
}
var appstore = {
webOrigin: argv.appstoreWebOrigin || '',
apiOrigin: argv.appstoreApiOrigin || ''
};
console.log();
console.log('Cloudron API: %s', apiOrigin || 'default');
console.log('Building for revision: %s', revision);
console.log();
console.log('Overriding appstore origin:');
console.log(' Website: %s', appstore.webOrigin || 'no');
console.log(' Api: %s', appstore.apiOrigin || 'no');
console.log();
gulp.task('fontawesome', function () {
return gulp.src('node_modules/@fortawesome/fontawesome-free/**/*')
.pipe(gulp.dest('dist/3rdparty/fontawesome/'));
});
gulp.task('bootstrap', function () {
return gulp.src('node_modules/bootstrap-sass/assets/javascripts/bootstrap.min.js')
.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',
'src/3rdparty/**/*.map',
'src/3rdparty/**/*.css',
@@ -23,177 +87,109 @@ gulp.task('3rdparty', function () {
'src/3rdparty/**/*.eot',
'src/3rdparty/**/*.svg',
'src/3rdparty/**/*.gif',
'src/3rdparty/**/*.ttf',
'src/3rdparty/**/*.woff',
'src/3rdparty/**/*.woff2'
])
'src/3rdparty/**/*.ttf'
])
.pipe(gulp.dest('dist/3rdparty/'));
gulp.src('node_modules/bootstrap-sass/assets/javascripts/bootstrap.min.js')
.pipe(gulp.dest('dist/3rdparty/js'));
});
gulp.task('3rdparty', gulp.series(['3rdparty-copy', 'monaco', 'xterm', 'bootstrap', 'fontawesome']));
// --------------
// JavaScript
// --------------
if (argv.help || argv.h) {
console.log('Supported arguments for "gulp develop":');
console.log(' --client-id <clientId>');
console.log(' --client-secret <clientSecret>');
console.log(' --api-origin <cloudron api uri>');
console.log(' --revision <revision>');
process.exit(1);
}
gulp.task('js', ['js-index', 'js-logs', 'js-terminal', 'js-setup', 'js-setupdns', 'js-restore', 'js-update'], function () {});
var oauth = {
clientId: argv.clientId || 'cid-webadmin',
clientSecret: argv.clientSecret || 'unused',
apiOrigin: argv.apiOrigin || '',
};
var revision = argv.revision || '';
console.log();
console.log('Using OAuth credentials:');
console.log(' ClientId: %s', oauth.clientId);
console.log(' ClientSecret: %s', oauth.clientSecret);
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
console.log();
console.log('Building for revision: %s', revision);
console.log();
gulp.task('js-index', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src([
return gulp.src([
'src/js/index.js',
'src/js/client.js',
'src/js/appstore.js',
'src/js/main.js',
'src/views/*.js'
])
.pipe(ejs({ oauth: oauth, revision: revision }, {}, { ext: '.js' }))
])
.pipe(ejs({ apiOrigin: apiOrigin, revision: revision, appstore: appstore }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('index.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-logs', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['src/js/logs.js', 'src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
return gulp.src(['src/js/logs.js', 'src/js/client.js'])
.pipe(ejs({ apiOrigin: apiOrigin, appstore: appstore }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('logs.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-filemanager', function () {
return gulp.src(['src/js/filemanager.js', 'src/js/client.js'])
.pipe(ejs({ apiOrigin: apiOrigin, 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 () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['src/js/terminal.js', 'src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
return gulp.src(['src/js/terminal.js', 'src/js/client.js'])
.pipe(ejs({ apiOrigin: apiOrigin, appstore: appstore }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('terminal.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-login', function () {
return gulp.src(['src/js/login.js'])
.pipe(ejs({ apiOrigin: apiOrigin, appstore: appstore }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('login.js', { newLine: ';' }))
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-setupaccount', function () {
return gulp.src(['src/js/setupaccount.js'])
.pipe(ejs({ apiOrigin: apiOrigin, appstore: appstore }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setupaccount.js', { newLine: ';' }))
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-setup', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['src/js/setup.js', 'src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
return gulp.src(['src/js/setup.js', 'src/js/client.js'])
.pipe(ejs({ apiOrigin: apiOrigin, appstore: appstore }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setup.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-setupdns', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['src/js/setupdns.js', 'src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
return gulp.src(['src/js/setupdns.js', 'src/js/client.js'])
.pipe(ejs({ apiOrigin: apiOrigin, appstore: appstore }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setupdns.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-restore', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['src/js/restore.js', 'src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
return gulp.src(['src/js/restore.js', 'src/js/client.js'])
.pipe(ejs({ apiOrigin: apiOrigin, appstore: appstore }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('restore.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js-update', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['src/js/update.js'])
.pipe(sourcemaps.init())
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('dist/js'));
});
gulp.task('js', gulp.series([ 'js-index', 'js-logs', 'js-filemanager', 'js-terminal', 'js-login', 'js-setupaccount', 'js-setup', 'js-setupdns', 'js-restore' ]));
// --------------
// HTML
// --------------
gulp.task('html', ['html-views', 'html-templates'], function () {
return gulp.src('src/*.html').pipe(ejs({ revision: revision }, {}, { ext: '.html' })).pipe(gulp.dest('dist'));
});
gulp.task('html-views', function () {
return gulp.src('src/views/**/*.html').pipe(gulp.dest('dist/views'));
});
@@ -202,6 +198,12 @@ gulp.task('html-templates', function () {
return gulp.src('src/templates/**/*.html').pipe(gulp.dest('dist/templates'));
});
gulp.task('html-raw', function () {
return gulp.src('src/*.html').pipe(ejs({ apiOrigin: apiOrigin, revision: revision }, {}, { ext: '.html' })).pipe(gulp.dest('dist'));
});
gulp.task('html', gulp.series(['html-views', 'html-templates', 'html-raw']));
// --------------
// CSS
// --------------
@@ -221,30 +223,43 @@ gulp.task('images', function () {
.pipe(gulp.dest('dist/img'));
});
gulp.task('timezones', function (done) {
execSync('./scripts/createTimezones.js ./dist/js/timezones.js');
done();
});
// --------------
// Utilities
// --------------
gulp.task('watch', ['default'], function () {
gulp.watch(['src/*.scss'], ['css']);
gulp.watch(['src/img/*'], ['images']);
gulp.watch(['src/**/*.html'], ['html']);
gulp.watch(['src/views/*.html'], ['html-views']);
gulp.watch(['src/templates/*.html'], ['html-templates']);
gulp.watch(['src/js/update.js'], ['js-update']);
gulp.watch(['src/js/setup.js', 'src/js/client.js'], ['js-setup']);
gulp.watch(['src/js/setupdns.js', 'src/js/client.js'], ['js-setupdns']);
gulp.watch(['src/js/restore.js', 'src/js/client.js'], ['js-restore']);
gulp.watch(['src/js/logs.js', 'src/js/client.js'], ['js-logs']);
gulp.watch(['src/js/terminal.js', 'src/js/client.js'], ['js-terminal']);
gulp.watch(['src/js/index.js', 'src/js/client.js', 'src/js/appstore.js', 'src/js/main.js', 'src/views/*.js'], ['js-index']);
gulp.watch(['src/3rdparty/**/*'], ['3rdparty']);
});
gulp.task('clean', function () {
gulp.task('clean', function (done) {
rimraf.sync('dist');
done();
});
gulp.task('default', ['clean', 'html', 'js', '3rdparty', 'images', 'css'], function () {});
gulp.task('default', gulp.series(['clean', 'html', 'js', 'timezones', '3rdparty', 'images', 'css']));
gulp.task('watch', function (done) {
gulp.watch(['src/*.scss'], gulp.series(['css']));
gulp.watch(['src/img/*'], gulp.series(['images']));
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/filemanager.js', 'src/js/client.js'], gulp.series(['js-filemanager']));
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(['src/3rdparty/**/*'], gulp.series(['3rdparty']));
done();
});
gulp.task('serve', serve({ root: 'dist', port: 4000 }));
gulp.task('develop', gulp.series(['default', 'watch', 'serve']));
gulp.task('develop', ['watch'], serve({ root: 'dist', port: 4000 }));

4966
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,17 +12,26 @@
"author": "",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"bootstrap-sass": "^3.3.7",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^5.0.0",
"@fortawesome/fontawesome-free": "^5.14.0",
"bootstrap-sass": "^3.4.1",
"gulp": "^4.0.2",
"gulp-autoprefixer": "^7.0.1",
"gulp-concat": "^2.6.1",
"gulp-cssnano": "^2.1.3",
"gulp-ejs": "^3.1.2",
"gulp-sass": "^4.0.1",
"gulp-ejs": "^5.1.0",
"gulp-sass": "^4.1.0",
"gulp-serve": "^1.4.0",
"gulp-sourcemaps": "^2.6.4",
"gulp-uglify": "^3.0.0",
"rimraf": "^2.6.2",
"yargs": "^11.0.0"
"gulp-sourcemaps": "^2.6.5",
"monaco-editor": "^0.20.0",
"rimraf": "^3.0.2",
"xterm": "^4.9.0",
"xterm-addon-attach": "^0.6.0",
"xterm-addon-fit": "^0.4.0",
"yargs": "^16.0.3"
},
"eslintConfig": {
"env": {
"browser": true
}
}
}

66
scripts/addUsers.js Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env node
// This script creates many users to test the UI for such a case
// WARNING keep those in sync with delUsers.js
const USERNAME_PREFIX = 'manyuser';
const PASSOWRD_PREFIX = 'password';
const DISPLAYNAME_PREFIX = 'User ';
const EMAIL_DOMAIN = 'example.com'; // addresses will be username@EMAIL_DOMAIN
const COUNT = 100;
var async = require('async'),
readlineSync = require('readline-sync'),
superagent = require('superagent');
if (process.argv.length !== 3) {
console.log('Usage: ./addUsers.js <cloudronDomain>');
process.exit(1);
}
const cloudronDomain = process.argv[2];
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) {
if (error || result.statusCode !== 200) {
console.log('Login failed');
return getAccessToken(callback);
}
callback(result.body.accessToken);
});
}
console.log(`Login to ${cloudronDomain}`);
getAccessToken(function (accessToken) {
console.log(`Now creating ${COUNT} users...`);
async.timesLimit(COUNT, 5, function (n, next) {
let user = {
username: USERNAME_PREFIX + n,
password: PASSOWRD_PREFIX + n,
email: USERNAME_PREFIX + n + '@' + EMAIL_DOMAIN,
displayName: DISPLAYNAME_PREFIX + n
};
superagent.post(`https://${cloudronDomain}/api/v1/users`, user).query({ access_token: accessToken }).end(function (error) {
if (error) return next(error);
process.stdout.write('.');
next();
});
}, function (error) {
console.log();
if (error) {
console.error(error);
process.exit(1);
}
console.log('Done');
});
});

387
scripts/createTimezones.js Executable file
View File

@@ -0,0 +1,387 @@
#!/usr/bin/env node
// This script creates a specific timezones.js to be consumed by the dashboard
var execSync = require('child_process').execSync,
fs = require('fs'),
path = require('path');
if (process.argv.length !== 3) {
console.log('Usage: createTimezones.js <output.js>');
process.exit(1);
}
const destinationFilePath = path.resolve(process.argv[2]);
console.log('Creating timezone info at:', destinationFilePath);
var ubuntuTimezones = execSync('timedatectl list-timezones --no-pager').toString().split('\n').filter(function (t) { return !!t; });
// from https://github.com/dmfilipenko/timezones.json/blob/master/timezones.json
var details = [
{ name: 'UTC', offset: '+00:00' },
{ name: 'Africa/Abidjan', offset: '+00:00' },
{ name: 'Africa/Accra', offset: '+00:00' },
{ name: 'Africa/Algiers', offset: '+01:00' },
{ name: 'Africa/Bissau', offset: '+00:00' },
{ name: 'Africa/Cairo', offset: '+02:00' },
{ name: 'Africa/Casablanca', offset: '+01:00' },
{ name: 'Africa/Ceuta', offset: '+01:00' },
{ name: 'Africa/El_Aaiun', offset: '+00:00' },
{ name: 'Africa/Johannesburg', offset: '+02:00' },
{ name: 'Africa/Juba', offset: '+03:00' },
{ name: 'Africa/Khartoum', offset: '+02:00' },
{ name: 'Africa/Lagos', offset: '+01:00' },
{ name: 'Africa/Maputo', offset: '+02:00' },
{ name: 'Africa/Monrovia', offset: '+00:00' },
{ name: 'Africa/Nairobi', offset: '+03:00' },
{ name: 'Africa/Ndjamena', offset: '+01:00' },
{ name: 'Africa/Tripoli', offset: '+02:00' },
{ name: 'Africa/Tunis', offset: '+01:00' },
{ name: 'Africa/Windhoek', offset: '+02:00' },
{ name: 'America/Adak', offset: '10:00' },
{ name: 'America/Anchorage', offset: '09:00' },
{ name: 'America/Araguaina', offset: '03:00' },
{ name: 'America/Argentina/Buenos_Aires', offset: '03:00' },
{ name: 'America/Argentina/Catamarca', offset: '03:00' },
{ name: 'America/Argentina/Cordoba', offset: '03:00' },
{ name: 'America/Argentina/Jujuy', offset: '03:00' },
{ name: 'America/Argentina/La_Rioja', offset: '03:00' },
{ name: 'America/Argentina/Mendoza', offset: '03:00' },
{ name: 'America/Argentina/Rio_Gallegos', offset: '03:00' },
{ name: 'America/Argentina/Salta', offset: '03:00' },
{ name: 'America/Argentina/San_Juan', offset: '03:00' },
{ name: 'America/Argentina/San_Luis', offset: '03:00' },
{ name: 'America/Argentina/Tucuman', offset: '03:00' },
{ name: 'America/Argentina/Ushuaia', offset: '03:00' },
{ name: 'America/Asuncion', offset: '04:00' },
{ name: 'America/Atikokan', offset: '05:00' },
{ name: 'America/Bahia', offset: '03:00' },
{ name: 'America/Bahia_Banderas', offset: '06:00' },
{ name: 'America/Barbados', offset: '04:00' },
{ name: 'America/Belem', offset: '03:00' },
{ name: 'America/Belize', offset: '06:00' },
{ name: 'America/Blanc-Sablon', offset: '04:00' },
{ name: 'America/Boa_Vista', offset: '04:00' },
{ name: 'America/Bogota', offset: '05:00' },
{ name: 'America/Boise', offset: '07:00' },
{ name: 'America/Cambridge_Bay', offset: '07:00' },
{ name: 'America/Campo_Grande', offset: '04:00' },
{ name: 'America/Cancun', offset: '05:00' },
{ name: 'America/Caracas', offset: '04:00' },
{ name: 'America/Cayenne', offset: '03:00' },
{ name: 'America/Chicago', offset: '06:00' },
{ name: 'America/Chihuahua', offset: '07:00' },
{ name: 'America/Costa_Rica', offset: '06:00' },
{ name: 'America/Creston', offset: '07:00' },
{ name: 'America/Cuiaba', offset: '04:00' },
{ name: 'America/Curacao', offset: '04:00' },
{ name: 'America/Danmarkshavn', offset: '+00:00' },
{ name: 'America/Dawson', offset: '08:00' },
{ name: 'America/Dawson_Creek', offset: '07:00' },
{ name: 'America/Denver', offset: '07:00' },
{ name: 'America/Detroit', offset: '05:00' },
{ name: 'America/Edmonton', offset: '07:00' },
{ name: 'America/Eirunepe', offset: '05:00' },
{ name: 'America/El_Salvador', offset: '06:00' },
{ name: 'America/Fort_Nelson', offset: '07:00' },
{ name: 'America/Fortaleza', offset: '03:00' },
{ name: 'America/Glace_Bay', offset: '04:00' },
{ name: 'America/Godthab', offset: '03:00' },
{ name: 'America/Goose_Bay', offset: '04:00' },
{ name: 'America/Grand_Turk', offset: '05:00' },
{ name: 'America/Guatemala', offset: '06:00' },
{ name: 'America/Guayaquil', offset: '05:00' },
{ name: 'America/Guyana', offset: '04:00' },
{ name: 'America/Halifax', offset: '04:00' },
{ name: 'America/Havana', offset: '05:00' },
{ name: 'America/Hermosillo', offset: '07:00' },
{ name: 'America/Indiana/Indianapolis', offset: '05:00' },
{ name: 'America/Indiana/Knox', offset: '06:00' },
{ name: 'America/Indiana/Marengo', offset: '05:00' },
{ name: 'America/Indiana/Petersburg', offset: '05:00' },
{ name: 'America/Indiana/Tell_City', offset: '06:00' },
{ name: 'America/Indiana/Vevay', offset: '05:00' },
{ name: 'America/Indiana/Vincennes', offset: '05:00' },
{ name: 'America/Indiana/Winamac', offset: '05:00' },
{ name: 'America/Inuvik', offset: '07:00' },
{ name: 'America/Iqaluit', offset: '05:00' },
{ name: 'America/Jamaica', offset: '05:00' },
{ name: 'America/Juneau', offset: '09:00' },
{ name: 'America/Kentucky/Louisville', offset: '05:00' },
{ name: 'America/Kentucky/Monticello', offset: '05:00' },
{ name: 'America/La_Paz', offset: '04:00' },
{ name: 'America/Lima', offset: '05:00' },
{ name: 'America/Los_Angeles', offset: '08:00' },
{ name: 'America/Maceio', offset: '03:00' },
{ name: 'America/Managua', offset: '06:00' },
{ name: 'America/Manaus', offset: '04:00' },
{ name: 'America/Martinique', offset: '04:00' },
{ name: 'America/Matamoros', offset: '06:00' },
{ name: 'America/Mazatlan', offset: '07:00' },
{ name: 'America/Menominee', offset: '06:00' },
{ name: 'America/Merida', offset: '06:00' },
{ name: 'America/Metlakatla', offset: '09:00' },
{ name: 'America/Mexico_City', offset: '06:00' },
{ name: 'America/Miquelon', offset: '03:00' },
{ name: 'America/Moncton', offset: '04:00' },
{ name: 'America/Monterrey', offset: '06:00' },
{ name: 'America/Montevideo', offset: '03:00' },
{ name: 'America/Nassau', offset: '05:00' },
{ name: 'America/New_York', offset: '05:00' },
{ name: 'America/Nipigon', offset: '05:00' },
{ name: 'America/Nome', offset: '09:00' },
{ name: 'America/Noronha', offset: '02:00' },
{ name: 'America/North_Dakota/Beulah', offset: '06:00' },
{ name: 'America/North_Dakota/Center', offset: '06:00' },
{ name: 'America/North_Dakota/New_Salem', offset: '06:00' },
{ name: 'America/Ojinaga', offset: '07:00' },
{ name: 'America/Panama', offset: '05:00' },
{ name: 'America/Pangnirtung', offset: '05:00' },
{ name: 'America/Paramaribo', offset: '03:00' },
{ name: 'America/Phoenix', offset: '07:00' },
{ name: 'America/Port_of_Spain', offset: '04:00' },
{ name: 'America/Port-au-Prince', offset: '05:00' },
{ name: 'America/Porto_Velho', offset: '04:00' },
{ name: 'America/Puerto_Rico', offset: '04:00' },
{ name: 'America/Punta_Arenas', offset: '03:00' },
{ name: 'America/Rainy_River', offset: '06:00' },
{ name: 'America/Rankin_Inlet', offset: '06:00' },
{ name: 'America/Recife', offset: '03:00' },
{ name: 'America/Regina', offset: '06:00' },
{ name: 'America/Resolute', offset: '06:00' },
{ name: 'America/Rio_Branco', offset: '05:00' },
{ name: 'America/Santarem', offset: '03:00' },
{ name: 'America/Santiago', offset: '04:00' },
{ name: 'America/Santo_Domingo', offset: '04:00' },
{ name: 'America/Sao_Paulo', offset: '03:00' },
{ name: 'America/Scoresbysund', offset: '01:00' },
{ name: 'America/Sitka', offset: '09:00' },
{ name: 'America/St_Johns', offset: '03:30' },
{ name: 'America/Swift_Current', offset: '06:00' },
{ name: 'America/Tegucigalpa', offset: '06:00' },
{ name: 'America/Thule', offset: '04:00' },
{ name: 'America/Thunder_Bay', offset: '05:00' },
{ name: 'America/Tijuana', offset: '08:00' },
{ name: 'America/Toronto', offset: '05:00' },
{ name: 'America/Vancouver', offset: '08:00' },
{ name: 'America/Whitehorse', offset: '08:00' },
{ name: 'America/Winnipeg', offset: '06:00' },
{ name: 'America/Yakutat', offset: '09:00' },
{ name: 'America/Yellowknife', offset: '07:00' },
{ name: 'Antarctica/Casey', offset: '+11:00' },
{ name: 'Antarctica/Davis', offset: '+07:00' },
{ name: 'Antarctica/DumontDUrville', offset: '+10:00' },
{ name: 'Antarctica/Macquarie', offset: '+11:00' },
{ name: 'Antarctica/Mawson', offset: '+05:00' },
{ name: 'Antarctica/Palmer', offset: '03:00' },
{ name: 'Antarctica/Rothera', offset: '03:00' },
{ name: 'Antarctica/Syowa', offset: '+03:00' },
{ name: 'Antarctica/Troll', offset: '+00:00' },
{ name: 'Antarctica/Vostok', offset: '+06:00' },
{ name: 'Asia/Almaty', offset: '+06:00' },
{ name: 'Asia/Amman', offset: '+02:00' },
{ name: 'Asia/Anadyr', offset: '+12:00' },
{ name: 'Asia/Aqtau', offset: '+05:00' },
{ name: 'Asia/Aqtobe', offset: '+05:00' },
{ name: 'Asia/Ashgabat', offset: '+05:00' },
{ name: 'Asia/Atyrau', offset: '+05:00' },
{ name: 'Asia/Baghdad', offset: '+03:00' },
{ name: 'Asia/Baku', offset: '+04:00' },
{ name: 'Asia/Bangkok', offset: '+07:00' },
{ name: 'Asia/Barnaul', offset: '+07:00' },
{ name: 'Asia/Beirut', offset: '+02:00' },
{ name: 'Asia/Bishkek', offset: '+06:00' },
{ name: 'Asia/Brunei', offset: '+08:00' },
{ name: 'Asia/Chita', offset: '+09:00' },
{ name: 'Asia/Choibalsan', offset: '+08:00' },
{ name: 'Asia/Colombo', offset: '+05:30' },
{ name: 'Asia/Damascus', offset: '+02:00' },
{ name: 'Asia/Dhaka', offset: '+06:00' },
{ name: 'Asia/Dili', offset: '+09:00' },
{ name: 'Asia/Dubai', offset: '+04:00' },
{ name: 'Asia/Dushanbe', offset: '+05:00' },
{ name: 'Asia/Famagusta', offset: '+02:00' },
{ name: 'Asia/Gaza', offset: '+02:00' },
{ name: 'Asia/Hebron', offset: '+02:00' },
{ name: 'Asia/Ho_Chi_Minh', offset: '+07:00' },
{ name: 'Asia/Hong_Kong', offset: '+08:00' },
{ name: 'Asia/Hovd', offset: '+07:00' },
{ name: 'Asia/Irkutsk', offset: '+08:00' },
{ name: 'Asia/Jakarta', offset: '+07:00' },
{ name: 'Asia/Jayapura', offset: '+09:00' },
{ name: 'Asia/Jerusalem', offset: '+02:00' },
{ name: 'Asia/Kabul', offset: '+04:30' },
{ name: 'Asia/Kamchatka', offset: '+12:00' },
{ name: 'Asia/Karachi', offset: '+05:00' },
{ name: 'Asia/Kathmandu', offset: '+05:45' },
{ name: 'Asia/Khandyga', offset: '+09:00' },
{ name: 'Asia/Kolkata', offset: '+05:30' },
{ name: 'Asia/Krasnoyarsk', offset: '+07:00' },
{ name: 'Asia/Kuala_Lumpur', offset: '+08:00' },
{ name: 'Asia/Kuching', offset: '+08:00' },
{ name: 'Asia/Macau', offset: '+08:00' },
{ name: 'Asia/Magadan', offset: '+11:00' },
{ name: 'Asia/Makassar', offset: '+08:00' },
{ name: 'Asia/Manila', offset: '+08:00' },
{ name: 'Asia/Novokuznetsk', offset: '+07:00' },
{ name: 'Asia/Novosibirsk', offset: '+07:00' },
{ name: 'Asia/Omsk', offset: '+06:00' },
{ name: 'Asia/Oral', offset: '+05:00' },
{ name: 'Asia/Pontianak', offset: '+07:00' },
{ name: 'Asia/Pyongyang', offset: '+09:00' },
{ name: 'Asia/Qatar', offset: '+03:00' },
{ name: 'Asia/Qyzylorda', offset: '+05:00' },
{ name: 'Asia/Riyadh', offset: '+03:00' },
{ name: 'Asia/Sakhalin', offset: '+11:00' },
{ name: 'Asia/Samarkand', offset: '+05:00' },
{ name: 'Asia/Seoul', offset: '+09:00' },
{ name: 'Asia/Shanghai', offset: '+08:00' },
{ name: 'Asia/Singapore', offset: '+08:00' },
{ name: 'Asia/Srednekolymsk', offset: '+11:00' },
{ name: 'Asia/Taipei', offset: '+08:00' },
{ name: 'Asia/Tashkent', offset: '+05:00' },
{ name: 'Asia/Tbilisi', offset: '+04:00' },
{ name: 'Asia/Tehran', offset: '+03:30' },
{ name: 'Asia/Thimphu', offset: '+06:00' },
{ name: 'Asia/Tokyo', offset: '+09:00' },
{ name: 'Asia/Tomsk', offset: '+07:00' },
{ name: 'Asia/Ulaanbaatar', offset: '+08:00' },
{ name: 'Asia/Urumqi', offset: '+06:00' },
{ name: 'Asia/Ust-Nera', offset: '+10:00' },
{ name: 'Asia/Vladivostok', offset: '+10:00' },
{ name: 'Asia/Yakutsk', offset: '+09:00' },
{ name: 'Asia/Yangon', offset: '+06:30' },
{ name: 'Asia/Yekaterinburg', offset: '+05:00' },
{ name: 'Asia/Yerevan', offset: '+04:00' },
{ name: 'Atlantic/Azores', offset: '01:00' },
{ name: 'Atlantic/Bermuda', offset: '04:00' },
{ name: 'Atlantic/Canary', offset: '+00:00' },
{ name: 'Atlantic/Cape_Verde', offset: '01:00' },
{ name: 'Atlantic/Faroe', offset: '+00:00' },
{ name: 'Atlantic/Madeira', offset: '+00:00' },
{ name: 'Atlantic/Reykjavik', offset: '+00:00' },
{ name: 'Atlantic/South_Georgia', offset: '02:00' },
{ name: 'Atlantic/Stanley', offset: '03:00' },
{ name: 'Australia/Adelaide', offset: '+09:30' },
{ name: 'Australia/Brisbane', offset: '+10:00' },
{ name: 'Australia/Broken_Hill', offset: '+09:30' },
{ name: 'Australia/Currie', offset: '+10:00' },
{ name: 'Australia/Darwin', offset: '+09:30' },
{ name: 'Australia/Eucla', offset: '+08:45' },
{ name: 'Australia/Hobart', offset: '+10:00' },
{ name: 'Australia/Lindeman', offset: '+10:00' },
{ name: 'Australia/Lord_Howe', offset: '+10:30' },
{ name: 'Australia/Melbourne', offset: '+10:00' },
{ name: 'Australia/Perth', offset: '+08:00' },
{ name: 'Australia/Sydney', offset: '+10:00' },
{ name: 'Europe/Amsterdam', offset: '+01:00' },
{ name: 'Europe/Andorra', offset: '+01:00' },
{ name: 'Europe/Astrakhan', offset: '+04:00' },
{ name: 'Europe/Athens', offset: '+02:00' },
{ name: 'Europe/Belgrade', offset: '+01:00' },
{ name: 'Europe/Berlin', offset: '+01:00' },
{ name: 'Europe/Brussels', offset: '+01:00' },
{ name: 'Europe/Bucharest', offset: '+02:00' },
{ name: 'Europe/Budapest', offset: '+01:00' },
{ name: 'Europe/Chisinau', offset: '+02:00' },
{ name: 'Europe/Copenhagen', offset: '+01:00' },
{ name: 'Europe/Dublin', offset: '+00:00' },
{ name: 'Europe/Gibraltar', offset: '+01:00' },
{ name: 'Europe/Helsinki', offset: '+02:00' },
{ name: 'Europe/Istanbul', offset: '+03:00' },
{ name: 'Europe/Kaliningrad', offset: '+02:00' },
{ name: 'Europe/Kiev', offset: '+02:00' },
{ name: 'Europe/Kirov', offset: '+03:00' },
{ name: 'Europe/Lisbon', offset: '+00:00' },
{ name: 'Europe/London', offset: '+00:00' },
{ name: 'Europe/Luxembourg', offset: '+01:00' },
{ name: 'Europe/Madrid', offset: '+01:00' },
{ name: 'Europe/Malta', offset: '+01:00' },
{ name: 'Europe/Minsk', offset: '+03:00' },
{ name: 'Europe/Monaco', offset: '+01:00' },
{ name: 'Europe/Moscow', offset: '+03:00' },
{ name: 'Asia/Nicosia', offset: '+02:00' },
{ name: 'Europe/Oslo', offset: '+01:00' },
{ name: 'Europe/Paris', offset: '+01:00' },
{ name: 'Europe/Prague', offset: '+01:00' },
{ name: 'Europe/Riga', offset: '+02:00' },
{ name: 'Europe/Rome', offset: '+01:00' },
{ name: 'Europe/Samara', offset: '+04:00' },
{ name: 'Europe/Saratov', offset: '+04:00' },
{ name: 'Europe/Simferopol', offset: '+03:00' },
{ name: 'Europe/Sofia', offset: '+02:00' },
{ name: 'Europe/Stockholm', offset: '+01:00' },
{ name: 'Europe/Tallinn', offset: '+02:00' },
{ name: 'Europe/Tirane', offset: '+01:00' },
{ name: 'Europe/Ulyanovsk', offset: '+04:00' },
{ name: 'Europe/Uzhgorod', offset: '+02:00' },
{ name: 'Europe/Vienna', offset: '+01:00' },
{ name: 'Europe/Vilnius', offset: '+02:00' },
{ name: 'Europe/Volgograd', offset: '+04:00' },
{ name: 'Europe/Warsaw', offset: '+01:00' },
{ name: 'Europe/Zaporozhye', offset: '+02:00' },
{ name: 'Europe/Zurich', offset: '+01:00' },
{ name: 'Indian/Chagos', offset: '+06:00' },
{ name: 'Indian/Christmas', offset: '+07:00' },
{ name: 'Indian/Cocos', offset: '+06:30' },
{ name: 'Indian/Kerguelen', offset: '+05:00' },
{ name: 'Indian/Mahe', offset: '+04:00' },
{ name: 'Indian/Maldives', offset: '+05:00' },
{ name: 'Indian/Mauritius', offset: '+04:00' },
{ name: 'Indian/Reunion', offset: '+04:00' },
{ name: 'Pacific/Apia', offset: '+13:00' },
{ name: 'Pacific/Auckland', offset: '+12:00' },
{ name: 'Pacific/Bougainville', offset: '+11:00' },
{ name: 'Pacific/Chatham', offset: '+12:45' },
{ name: 'Pacific/Chuuk', offset: '+10:00' },
{ name: 'Pacific/Easter', offset: '06:00' },
{ name: 'Pacific/Efate', offset: '+11:00' },
{ name: 'Pacific/Enderbury', offset: '+13:00' },
{ name: 'Pacific/Fakaofo', offset: '+13:00' },
{ name: 'Pacific/Fiji', offset: '+12:00' },
{ name: 'Pacific/Funafuti', offset: '+12:00' },
{ name: 'Pacific/Galapagos', offset: '06:00' },
{ name: 'Pacific/Gambier', offset: '09:00' },
{ name: 'Pacific/Guadalcanal', offset: '+11:00' },
{ name: 'Pacific/Guam', offset: '+10:00' },
{ name: 'Pacific/Honolulu', offset: '10:00' },
{ name: 'Pacific/Kiritimati', offset: '+14:00' },
{ name: 'Pacific/Kosrae', offset: '+11:00' },
{ name: 'Pacific/Kwajalein', offset: '+12:00' },
{ name: 'Pacific/Majuro', offset: '+12:00' },
{ name: 'Pacific/Marquesas', offset: '09:30' },
{ name: 'Pacific/Nauru', offset: '+12:00' },
{ name: 'Pacific/Niue', offset: '11:00' },
{ name: 'Pacific/Norfolk', offset: '+11:00' },
{ name: 'Pacific/Noumea', offset: '+11:00' },
{ name: 'Pacific/Pago_Pago', offset: '11:00' },
{ name: 'Pacific/Palau', offset: '+09:00' },
{ name: 'Pacific/Pitcairn', offset: '08:00' },
{ name: 'Pacific/Pohnpei', offset: '+11:00' },
{ name: 'Pacific/Port_Moresby', offset: '+10:00' },
{ name: 'Pacific/Rarotonga', offset: '10:00' },
{ name: 'Pacific/Tahiti', offset: '10:00' },
{ name: 'Pacific/Tarawa', offset: '+12:00' },
{ name: 'Pacific/Tongatapu', offset: '+13:00' },
{ name: 'Pacific/Wake', offset: '+12:00' },
{ name: 'Pacific/Wallis', offset: '+12:00' },
];
var timezones = ubuntuTimezones.map(function (t) {
var detail = details.find(function (d) {
return d.name === t;
});
if (!detail) return null;
return {
id: t,
display: `${t} (UTC${detail.offset})`
}
}).filter(function (t) { return !!t; });
var output = `(function () { window.timezones = ${JSON.stringify(timezones)}; })();\n`;
fs.writeFileSync(destinationFilePath, output, 'utf-8');
console.log('Done');

69
scripts/delUsers.js Executable file
View File

@@ -0,0 +1,69 @@
#!/usr/bin/env node
// This script deletes many users to test the UI for such a case
// WARNING keep those in sync with addUsers.js
const USERNAME_PREFIX = 'manyuser';
var async = require('async'),
readlineSync = require('readline-sync'),
superagent = require('superagent');
if (process.argv.length !== 3) {
console.log('Usage: ./delUsers.js <cloudronDomain>');
process.exit(1);
}
const cloudronDomain = process.argv[2];
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) {
if (error || result.statusCode !== 200) {
console.log('Login failed');
return getAccessToken(callback);
}
callback(result.body.accessToken);
});
}
console.log(`Login to ${cloudronDomain}`);
getAccessToken(function (accessToken) {
console.log('Listing users...');
superagent.get(`https://${cloudronDomain}/api/v1/users`).query({ access_token: accessToken, per_page: 1000 }).end(function (error, result) {
if (error) {
console.error(error);
process.exit(1);
}
console.log(`Found ${result.body.users.length} users`);
let matchingUsers = result.body.users.filter(function (u) { return u.username.indexOf(USERNAME_PREFIX) === 0; });
console.log(`Found ${matchingUsers.length} users with matching prefix`);
console.log('Deleting users...');
async.eachLimit(matchingUsers, 5, function (user, next) {
superagent.del(`https://${cloudronDomain}/api/v1/users/${user.id}`).query({ access_token: accessToken }).end(function (error) {
if (error) return next(error);
process.stdout.write('.');
next();
});
}, function (error) {
console.log();
if (error) {
console.error(error);
process.exit(1);
}
console.log('Done');
});
});
});

163
scripts/package-lock.json generated Normal file
View File

@@ -0,0 +1,163 @@
{
"name": "scripts",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"async": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/async/-/async-3.1.0.tgz",
"integrity": "sha512-4vx/aaY6j/j3Lw3fbCHNWP0pPaTCew3F6F3hYyl/tHs/ndmV1q7NW9T5yuJ2XAGwdQrP+6Wu20x06U4APo/iQQ=="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"cookiejar": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
"integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA=="
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "^2.1.1"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"fast-safe-stringify": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
"integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA=="
},
"form-data": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
},
"formidable": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz",
"integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg=="
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
"integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA=="
},
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
},
"mime-types": {
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"requires": {
"mime-db": "1.40.0"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"qs": {
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.0.tgz",
"integrity": "sha512-27RP4UotQORTpmNQDX8BHPukOnBP3p1uUJY5UnDhaJB+rMt9iMsok724XL+UHU23bEFOHRMQ2ZhI99qOWUMGFA=="
},
"readable-stream": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
"integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"readline-sync": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
"integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="
},
"safe-buffer": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"requires": {
"safe-buffer": "~5.2.0"
}
},
"superagent": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-5.1.0.tgz",
"integrity": "sha512-7V6JVx5N+eTL1MMqRBX0v0bG04UjrjAvvZJTF/VDH/SH2GjSLqlrcYepFlpTrXpm37aSY6h3GGVWGxXl/98TKA==",
"requires": {
"component-emitter": "^1.3.0",
"cookiejar": "^2.1.2",
"debug": "^4.1.1",
"fast-safe-stringify": "^2.0.6",
"form-data": "^2.3.3",
"formidable": "^1.2.1",
"methods": "^1.1.2",
"mime": "^2.4.4",
"qs": "^6.7.0",
"readable-stream": "^3.4.0",
"semver": "^6.1.1"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
}
}
}

16
scripts/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "scripts",
"version": "1.0.0",
"description": "",
"main": "manyUsers.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"async": "^3.1.0",
"readline-sync": "^1.4.10",
"superagent": "^5.1.0"
}
}

1
src/3rdparty/Chart/Chart.min.css vendored Normal file
View File

@@ -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}

7
src/3rdparty/Chart/Chart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,85 @@
/**
* angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating
* @author Alex_Crack
* @version v0.3.6
* @link https://github.com/alexcrack/angular-ui-notification
* @license MIT
*/
.ui-notification
{
position: fixed;
z-index: 9999;
width: 300px;
-webkit-transition: all ease .5s;
-o-transition: all ease .5s;
transition: all ease .5s;
color: #fff;
border-radius: 0;
background: #337ab7;
box-shadow: 5px 5px 10px rgba(0, 0, 0, .3);
}
.ui-notification.clickable
{
cursor: pointer;
}
.ui-notification.clickable:hover
{
opacity: .7;
}
.ui-notification.killed
{
-webkit-transition: opacity ease 1s;
-o-transition: opacity ease 1s;
transition: opacity ease 1s;
opacity: 0;
}
.ui-notification > h3
{
font-size: 14px;
font-weight: bold;
display: block;
margin: 10px 10px 0 10px;
padding: 0 0 5px 0;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, .3);
}
.ui-notification a
{
color: #fff;
}
.ui-notification a:hover
{
text-decoration: underline;
}
.ui-notification > .message
{
margin: 10px 10px 10px 10px;
}
.ui-notification.warning
{
color: #fff;
background: #f0ad4e;
}
.ui-notification.error
{
color: #fff;
background: #d9534f;
}
.ui-notification.success
{
color: #fff;
background: #5cb85c;
}
.ui-notification.info
{
color: #fff;
background: #5bc0de;
}

View File

@@ -1,8 +0,0 @@
/**
* angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating
* @author Alex_Crack
* @version v0.3.5
* @link https://github.com/alexcrack/angular-ui-notification
* @license MIT
*/
.ui-notification{position:fixed;z-index:9999;width:300px;-webkit-transition:all ease .5s;-o-transition:all ease .5s;transition:all ease .5s;color:#fff;border-radius:0;background:#337ab7;box-shadow:5px 5px 10px rgba(0,0,0,.3)}.ui-notification.clickable{cursor:pointer}.ui-notification.clickable:hover{opacity:.7}.ui-notification.killed{-webkit-transition:opacity ease 1s;-o-transition:opacity ease 1s;transition:opacity ease 1s;opacity:0}.ui-notification>h3{font-size:14px;font-weight:700;display:block;margin:10px 10px 0;padding:0 0 5px;text-align:left;border-bottom:1px solid rgba(255,255,255,.3)}.ui-notification a{color:#fff}.ui-notification a:hover{text-decoration:underline}.ui-notification>.message{margin:10px}.ui-notification.warning{color:#fff;background:#f0ad4e}.ui-notification.error{color:#fff;background:#d9534f}.ui-notification.success{color:#fff;background:#5cb85c}.ui-notification.info{color:#fff;background:#5bc0de}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,271 @@
/**
* angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating
* @author Alex_Crack
* @version v0.3.6
* @link https://github.com/alexcrack/angular-ui-notification
* @license MIT
*/
angular.module('ui-notification', []);
angular.module('ui-notification').provider('Notification', function () {
this.options = {
delay: 5000,
startTop: 10,
startRight: 10,
verticalSpacing: 10,
horizontalSpacing: 10,
positionX: 'right',
positionY: 'top',
replaceMessage: false,
templateUrl: 'angular-ui-notification.html',
onClose: undefined,
onClick: undefined,
closeOnClick: true,
maxCount: 0, // 0 - Infinite
container: 'body',
priority: 10
};
this.setOptions = function (options) {
if (!angular.isObject(options)) throw new Error("Options should be an object!");
this.options = angular.extend({}, this.options, options);
};
this.$get = ["$timeout", "$http", "$compile", "$templateCache", "$rootScope", "$injector", "$sce", "$q", "$window", function ($timeout, $http, $compile, $templateCache, $rootScope, $injector, $sce, $q, $window) {
var options = this.options;
var startTop = options.startTop;
var startRight = options.startRight;
var verticalSpacing = options.verticalSpacing;
var horizontalSpacing = options.horizontalSpacing;
var delay = options.delay;
var messageElements = [];
var isResizeBound = false;
var notify = function (args, t) {
var deferred = $q.defer();
if (typeof args !== 'object' || args === null) {
args = {message: args};
}
args.scope = args.scope ? args.scope : $rootScope;
args.template = args.templateUrl ? args.templateUrl : options.templateUrl;
args.delay = !angular.isUndefined(args.delay) ? args.delay : delay;
args.type = t || args.type || options.type || '';
args.positionY = args.positionY ? args.positionY : options.positionY;
args.positionX = args.positionX ? args.positionX : options.positionX;
args.replaceMessage = args.replaceMessage ? args.replaceMessage : options.replaceMessage;
args.onClose = args.onClose ? args.onClose : options.onClose;
args.onClick = args.onClick ? args.onClick : options.onClick;
args.closeOnClick = (args.closeOnClick !== null && args.closeOnClick !== undefined) ? args.closeOnClick : options.closeOnClick;
args.container = args.container ? args.container : options.container;
args.priority = args.priority ? args.priority : options.priority;
var template = $templateCache.get(args.template);
if (template) {
processNotificationTemplate(template);
} else {
// load it via $http only if it isn't default template and template isn't exist in template cache
// cache:true means cache it for later access.
$http.get(args.template, {cache: true})
.then(function (response) {
processNotificationTemplate(response.data);
})
.catch(function (data) {
throw new Error('Template (' + args.template + ') could not be loaded. ' + data);
});
}
function processNotificationTemplate(template) {
var scope = args.scope.$new();
scope.message = $sce.trustAsHtml(args.message);
scope.title = $sce.trustAsHtml(args.title);
scope.t = args.type.substr(0, 1);
scope.delay = args.delay;
scope.onClose = args.onClose;
scope.onClick = args.onClick;
var priorityCompareTop = function (a, b) {
return a._priority - b._priority;
};
var priorityCompareBtm = function (a, b) {
return b._priority - a._priority;
};
var reposite = function () {
var j = 0;
var k = 0;
var lastTop = startTop;
var lastRight = startRight;
var lastPosition = [];
if (args.positionY === 'top') {
messageElements.sort(priorityCompareTop);
} else if (args.positionY === 'bottom') {
messageElements.sort(priorityCompareBtm);
}
for (var i = messageElements.length - 1; i >= 0; i--) {
var element = messageElements[i];
if (args.replaceMessage && i < messageElements.length - 1) {
element.addClass('killed');
continue;
}
var elHeight = parseInt(element[0].offsetHeight);
var elWidth = parseInt(element[0].offsetWidth);
var position = lastPosition[element._positionY + element._positionX];
if ((top + elHeight) > window.innerHeight) {
position = startTop;
k++;
j = 0;
}
var top = (lastTop = position ? (j === 0 ? position : position + verticalSpacing) : startTop);
var right = lastRight + (k * (horizontalSpacing + elWidth));
element.css(element._positionY, top + 'px');
if (element._positionX === 'center') {
element.css('left', parseInt(window.innerWidth / 2 - elWidth / 2) + 'px');
} else {
element.css(element._positionX, right + 'px');
}
lastPosition[element._positionY + element._positionX] = top + elHeight;
if (options.maxCount > 0 && messageElements.length > options.maxCount && i === 0) {
element.scope().kill(true);
}
j++;
}
};
var templateElement = $compile(template)(scope);
templateElement._positionY = args.positionY;
templateElement._positionX = args.positionX;
templateElement._priority = args.priority;
templateElement.addClass(args.type);
var closeEvent = function (e) {
e = e.originalEvent || e;
if (e.type === 'click' || e.propertyName === 'opacity' && e.elapsedTime >= 1) {
if (scope.onClose) {
scope.$apply(scope.onClose(templateElement));
}
if (e.type === 'click')
if (scope.onClick) {
scope.$apply(scope.onClick(templateElement));
}
templateElement.remove();
messageElements.splice(messageElements.indexOf(templateElement), 1);
scope.$destroy();
reposite();
}
};
if (args.closeOnClick) {
templateElement.addClass('clickable');
templateElement.bind('click', closeEvent);
}
templateElement.bind('webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd', closeEvent);
if (angular.isNumber(args.delay)) {
$timeout(function () {
templateElement.addClass('killed');
}, args.delay);
}
setCssTransitions('none');
angular.element(document.querySelector(args.container)).append(templateElement);
var offset = -(parseInt(templateElement[0].offsetHeight) + 50);
templateElement.css(templateElement._positionY, offset + "px");
messageElements.push(templateElement);
if (args.positionX == 'center') {
var elWidth = parseInt(templateElement[0].offsetWidth);
templateElement.css('left', parseInt(window.innerWidth / 2 - elWidth / 2) + 'px');
}
$timeout(function () {
setCssTransitions('');
});
function setCssTransitions(value) {
['-webkit-transition', '-o-transition', 'transition'].forEach(function (prefix) {
templateElement.css(prefix, value);
});
}
scope._templateElement = templateElement;
scope.kill = function (isHard) {
if (isHard) {
if (scope.onClose) {
scope.$apply(scope.onClose(scope._templateElement));
}
messageElements.splice(messageElements.indexOf(scope._templateElement), 1);
scope._templateElement.remove();
scope.$destroy();
$timeout(reposite);
} else {
scope._templateElement.addClass('killed');
}
};
$timeout(reposite);
if (!isResizeBound) {
angular.element($window).bind('resize', function (e) {
$timeout(reposite);
});
isResizeBound = true;
}
deferred.resolve(scope);
}
return deferred.promise;
};
notify.primary = function (args) {
return this(args, 'primary');
};
notify.error = function (args) {
return this(args, 'error');
};
notify.success = function (args) {
return this(args, 'success');
};
notify.info = function (args) {
return this(args, 'info');
};
notify.warning = function (args) {
return this(args, 'warning');
};
notify.clearAll = function () {
angular.forEach(messageElements, function (element) {
element.addClass('killed');
});
};
return notify;
}];
});
angular.module("ui-notification").run(["$templateCache", function($templateCache) {$templateCache.put("angular-ui-notification.html","<div class=\"ui-notification\"><h3 ng-show=\"title\" ng-bind-html=\"title\"></h3><div class=\"message\" ng-bind-html=\"message\"></div></div>");}]);

View File

@@ -1,8 +0,0 @@
/**
* angular-ui-notification - Angular.js service providing simple notifications using Bootstrap 3 styles with css transitions for animating
* @author Alex_Crack
* @version v0.3.5
* @link https://github.com/alexcrack/angular-ui-notification
* @license MIT
*/
angular.module("ui-notification",[]),angular.module("ui-notification").provider("Notification",function(){this.options={delay:5e3,startTop:10,startRight:10,verticalSpacing:10,horizontalSpacing:10,positionX:"right",positionY:"top",replaceMessage:!1,templateUrl:"angular-ui-notification.html",onClose:void 0,closeOnClick:!0,maxCount:0,container:"body"},this.setOptions=function(e){if(!angular.isObject(e))throw new Error("Options should be an object!");this.options=angular.extend({},this.options,e)},this.$get=["$timeout","$http","$compile","$templateCache","$rootScope","$injector","$sce","$q","$window",function(e,t,n,i,o,s,a,l,r){var c=this.options,p=c.startTop,d=c.startRight,u=c.verticalSpacing,f=c.horizontalSpacing,m=c.delay,g=[],h=!1,C=function(s,C){function y(t){function i(e){["-webkit-transition","-o-transition","transition"].forEach(function(t){m.css(t,e)})}var o=s.scope.$new();o.message=a.trustAsHtml(s.message),o.title=a.trustAsHtml(s.title),o.t=s.type.substr(0,1),o.delay=s.delay,o.onClose=s.onClose;var l=function(){for(var e=0,t=0,n=p,i=d,o=[],a=g.length-1;a>=0;a--){var l=g[a];if(s.replaceMessage&&a<g.length-1)l.addClass("killed");else{var r=parseInt(l[0].offsetHeight),m=parseInt(l[0].offsetWidth),h=o[l._positionY+l._positionX];C+r>window.innerHeight&&(h=p,t++,e=0);var C=n=h?0===e?h:h+u:p,y=i+t*(f+m);l.css(l._positionY,C+"px"),"center"==l._positionX?l.css("left",parseInt(window.innerWidth/2-m/2)+"px"):l.css(l._positionX,y+"px"),o[l._positionY+l._positionX]=C+r,c.maxCount>0&&g.length>c.maxCount&&0===a&&l.scope().kill(!0),e++}}},m=n(t)(o);m._positionY=s.positionY,m._positionX=s.positionX,m.addClass(s.type);var C=function(e){e=e.originalEvent||e,("click"===e.type||"opacity"===e.propertyName&&e.elapsedTime>=1)&&(o.onClose&&o.$apply(o.onClose(m)),m.remove(),g.splice(g.indexOf(m),1),o.$destroy(),l())};s.closeOnClick&&(m.addClass("clickable"),m.bind("click",C)),m.bind("webkitTransitionEnd oTransitionEnd otransitionend transitionend msTransitionEnd",C),angular.isNumber(s.delay)&&e(function(){m.addClass("killed")},s.delay),i("none"),angular.element(document.querySelector(s.container)).append(m);var y=-(parseInt(m[0].offsetHeight)+50);if(m.css(m._positionY,y+"px"),g.push(m),"center"==s.positionX){var k=parseInt(m[0].offsetWidth);m.css("left",parseInt(window.innerWidth/2-k/2)+"px")}e(function(){i("")}),o._templateElement=m,o.kill=function(t){t?(o.onClose&&o.$apply(o.onClose(o._templateElement)),g.splice(g.indexOf(o._templateElement),1),o._templateElement.remove(),o.$destroy(),e(l)):o._templateElement.addClass("killed")},e(l),h||(angular.element(r).bind("resize",function(t){e(l)}),h=!0),v.resolve(o)}var v=l.defer();"object"!=typeof s&&(s={message:s}),s.scope=s.scope?s.scope:o,s.template=s.templateUrl?s.templateUrl:c.templateUrl,s.delay=angular.isUndefined(s.delay)?m:s.delay,s.type=C||s.type||c.type||"",s.positionY=s.positionY?s.positionY:c.positionY,s.positionX=s.positionX?s.positionX:c.positionX,s.replaceMessage=s.replaceMessage?s.replaceMessage:c.replaceMessage,s.onClose=s.onClose?s.onClose:c.onClose,s.closeOnClick=null!==s.closeOnClick&&void 0!==s.closeOnClick?s.closeOnClick:c.closeOnClick,s.container=s.container?s.container:c.container;var k=i.get(s.template);return k?y(k):t.get(s.template,{cache:!0}).then(y)["catch"](function(e){throw new Error("Template ("+s.template+") could not be loaded. "+e)}),v.promise};return C.primary=function(e){return this(e,"primary")},C.error=function(e){return this(e,"error")},C.success=function(e){return this(e,"success")},C.info=function(e){return this(e,"info")},C.warning=function(e){return this(e,"warning")},C.clearAll=function(){angular.forEach(g,function(e){e.addClass("killed")})},C}]}),angular.module("ui-notification").run(["$templateCache",function(e){e.put("angular-ui-notification.html",'<div class="ui-notification"><h3 ng-show="title" ng-bind-html="title"></h3><div class="message" ng-bind-html="message"></div></div>')}]);

1
src/3rdparty/js/async-3.2.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

639
src/3rdparty/js/contextMenu.js vendored Normal file
View File

@@ -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 = '&lt;';
$boldStyle.float = 'left';
} else {
$arrow = '&gt;';
$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);

10
src/3rdparty/js/mimer.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
src/3rdparty/js/mimer.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
src/3rdparty/js/showdown-1.9.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -1,10 +0,0 @@
.xterm.fullscreen {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: auto;
height: auto;
z-index: 255;
}

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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;
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -22,6 +22,9 @@
height: 100%;
width: 100%;
text-align: center;
font-family: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 13px;
line-height: 1.846;
}
.content {
@@ -37,23 +40,31 @@
bottom: 0;
width: 100%;
color: #555;
font-size: 14px;
font-size: 12px;
text-align: center;
padding: 5px;
z-index: 1000;
position: fixed;
opacity: .5;
transition: all .25s;
}
footer a {
color: #62bdfc;
padding: 10px;
footer:hover {
opacity: 1;
}
a {
color: #2196f3;
text-decoration: none;
background-color: transparent;
}
h1, h2, p {
margin: 10px;
a:hover {
color: #0a6ebd;
text-decoration: underline;
}
</style>
</style>
</head>
@@ -65,10 +76,8 @@
<p>This app is currently not responding. Try refreshing the page.</p>
</div>
<footer class="text-center">
<footer>
<span class="text-muted"><a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter</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>

View File

@@ -1,98 +0,0 @@
<!DOCTYPE html>
<html ng-app="Application" ng-controller="Controller">
<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 Error </title>
<link id="favicon" type="image/png" rel="icon" href="/api/v1/cloudron/avatar">
<!-- Theme CSS -->
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- external fonts and CSS -->
<link type="text/css" rel="stylesheet" href="/3rdparty/css/font-awesome.min.css">
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.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>
'use strict';
// create main application module
var app = angular.module('Application', []);
app.controller('Controller', ['$scope', '$http', function ($scope, $http) {
$scope.errorMessage = '';
$scope.statusOk = false;
$scope.avatarUrl = '/api/v1/cloudron/avatar?' + Math.random();
var favicon = $('#favicon');
if (favicon) favicon.attr('href', $scope.avatarUrl);
// try to fetch the cloudron status
$http.get('/api/v1/cloudron/status').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return console.error(status, data);
$scope.statusOk = true;
}).error(function (data, status) {
console.error(status, data);
$scope.statusOk = false;
});
var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.errorCode = search.errorCode || 0;
$scope.errorContext = search.errorContext || '';
}]);
</script>
</head>
<body class="status-page">
<div class="wrapper">
<div class="content">
<img ng-src="avatarUrl" width="128" height="128" onerror="this.src = '/img/logo.png'"/>
<h1> Cloudron </h1>
<div ng-show="errorCode == 0">
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Something has gone wrong </h3>
<span ng-show="statusOk">Please try again reloading the page <a href="/">here</a>.</span>
</div>
<div ng-show="errorCode == 1">
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Cloudron is not setup </h3>
Please use the setup link you received via mail.
</div>
<div ng-show="errorCode == 2">
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Setup requires a setupToken in the query </h3>
Please use the setup link you received via mail.
</div>
<div ng-show="errorCode == 3">
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Setup requires an email in the query </h3>
Please use the setup link you received via mail.
</div>
</div>
</div>
<footer class="text-center">
<span class="text-muted">&copy;2018 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></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>

299
src/filemanager.html Normal file
View File

@@ -0,0 +1,299 @@
<!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"/>
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- Fontawesome -->
<link type="text/css" rel="stylesheet" href="/3rdparty/fontawesome/css/all.css?<%= revision %>"/>
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.min.js"></script>
<!-- async -->
<script type="text/javascript" src="/3rdparty/js/async-3.2.0.min.js"></script>
<!-- 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"></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>
<!-- 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>
<!-- colors -->
<script type="text/javascript" src="/3rdparty/js/colors.js"></script>
<!-- moment -->
<script type="text/javascript" src="/3rdparty/js/moment.min.js"></script>
<!-- https://github.com/data-uri/mimer -->
<script type="text/javascript" src="/3rdparty/js/mimer.min.js"></script>
<!-- ui.bootstrap.contextMenu -->
<script type="text/javascript" src="/3rdparty/js/contextMenu.js"></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"></script>
<!-- Main Application -->
<script type="text/javascript" src="/js/filemanager.js"></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> Cloudron is offline. Reconnecting...</a>
<!-- Modal image/video viewer -->
<div class="modal fade" id="mediaViewerModal" tabindex="-1" role="dialog">
<div class="modal-dialog" style="width: calc(100% - 60px); max-width: 1280px; height: 800px; 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;" />
<video ng-show="mediaViewer.type === 'video'" controls preload="auto" autoplay ng-src="{{ mediaViewer.src | trustUrl}}"></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>
<h4 ng-hide="entryRemove.error">Really delete {{ entryRemove.entry.fileName }}?</h4>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">No</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> Yes</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">New Folder</h4>
</div>
<div class="modal-body">
<form role="form" name="newDirectoryForm" ng-submit="newDirectory.submit()" autocomplete="off">
<fieldset>
<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">{{ newDirectory.error }}</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="newDirectoryForm.$invalid || newDirectory.busy"/>
</fieldset>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" ng-click="newDirectory.submit()" ng-disabled="newDirectory.busy"><i class="fa fa-circle-notch fa-spin" ng-show="newDirectory.busy"></i> Create</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">Rename {{ renameEntry.entry.fileName }}</h4>
</div>
<div class="modal-body">
<form role="form" name="renameEntryForm" ng-submit="renameEntry.submit()" autocomplete="off">
<fieldset>
<div class="form-group" ng-class="{ 'has-error': (renameEntryForm.newName.$dirty && renameEntryForm.newName.$invalid) }">
<label class="control-label">New Name</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"/>
</fieldset>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" 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> Rename</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">
<h4 class="modal-title">Change ownership for {{ chownEntry.entry.fileName }}</h4>
</div>
<div class="modal-body">
<form role="form" name="chownEntryForm" ng-submit="chownEntry.submit()" autocomplete="off">
<fieldset>
<div class="form-group" ng-class="{ 'has-error': (chownEntryForm.newOwner.$dirty && chownEntry.error) }">
<label class="control-label">New Owner</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.entry.isDirectory">
<input type="checkbox" id="inputNewOwnerRecursive" ng-model="chownEntry.recursive">
<label class="control-label" for="inputNewOwnerRecursive">Change ownership recursively</label>
</div>
<input class="ng-hide" type="submit" ng-disabled="chownEntryForm.$invalid || chownEntry.busy"/>
</fieldset>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" 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> Change Owner</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">Uploading files ({{ uploadStatus.countDone }}/{{ uploadStatus.count }})</h4>
</div>
<div class="modal-body">
<div class="progress progress-striped active">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ uploadStatus.percentDone || 0 }}%"></div>
</div>
<p class="no-wrap">{{ uploadStatus.fileName }}</p>
</div>
<div class="modal-footer" style="text-align: left;">
<small>Do not refresh the page until upload has finished.</small>
</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">File has unsaved changes</h4>
</div>
<div class="modal-body">
<p class="text-bold text-danger">Your changes will be lost if you don't save them</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" ng-click="textEditor.close()">Don't Save</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</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> Save</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="app">
<div class="col-md-12 text-center">
<h3>App not found</h3>
</div>
</div>
<div class="card card-large" ng-show="app">
<input type="file" id="uploadFileInput" style="display: none" multiple/>
<input type="file" id="uploadFolderInput" style="display: none" multiple webkitdirectory directory/>
<h4 class="text-center">{{ app.fqdn }}</h4>
<div style="margin-bottom: 10px;">
<div class="btn-group" role="group">
<button class="btn btn-primary" ng-click="goDirectoryUp()" ng-disabled="cwd === '/'"><i class="fas fa-arrow-left"></i></button>
<button class="btn btn-primary" ng-click="refresh()"><i class="fas fa-sync-alt"></i></button>
</div>
<div class="btn-group" role="group">
<button class="btn btn-default" ng-disabled="cwd === '/'" ng-click="changeDirectory('/')"><i class="fas fa-home"></i> / </button>
<button class="btn btn-default" ng-disabled="part.path === cwd" ng-click="changeDirectory(part.path)" ng-repeat="part in cwdParts">{{ part.name }}</button>
</div>
<div class="btn-group pull-right" role="group">
<button class="btn btn-primary" ng-click="newDirectory.show()">New Folder</button>
<button class="btn btn-primary" ng-click="onUploadFile()">Upload File</button>
<button class="btn btn-primary" ng-click="onUploadFolder()">Upload Folder</button>
</div>
</div>
<div class="file-list" ng-class="{ 'entry-hovered': dropToBody, 'busy': busy }">
<table class="table table-hover" style="margin: 0;">
<thead>
<tr>
<th style="width: 3%;">&nbsp;</th>
<th style="width:82%">Name</th>
<th style="width:10%">Size</th>
<th style="width:10%">Owner</th>
<th style="width: 5%">&nbsp;</th>
</tr>
</thead>
<tbody>
<tr ng-show="entries.length === 0">
<td colspan="5" class="text-center">No files</td>
</tr>
<tr ng-repeat="entry in entries | orderBy:sortProperty:sortAsc | orderBy:'isDirectory':true" 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 }">
<td ng-click="open(entry)" ng-class="{ 'hand': !entry.isSymbolicLink }" class="text-center">
<i class="fas fa-lg {{ entry.icon }}" ng-class="{ 'text-primary': entry.isDirectory }"></i>
</td>
<td ng-class="{ 'hand': !entry.isSymbolicLink }" class="elide-table-cell" ng-click="open(entry)">{{ entry.fileName }}<span ng-show="entry.isSymbolicLink" class="text-muted" style="margin-left: 20px;">symlink to {{ entry.target }}</span></td>
<td ng-class="{ 'hand': !entry.isSymbolicLink }" class="elide-table-cell" ng-click="open(entry)">{{ entry.size | prettyByteSize }}</td>
<td ng-class="{ 'hand': !entry.isSymbolicLink }" class="elide-table-cell" ng-click="open(entry)">{{ entry.uid | prettyOwner }}</td>
<td class="text-right no-wrap" style="vertical-align: bottom">
<button class="btn btn-xs btn-link" context-menu="menuOptions" model="entry" context-menu-on="click"><i class="fas fa-bars"></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-success" ng-click="textEditor.save()" ng-disabled="textEditor.busy"><i class="fa fa-circle-notch fa-spin" ng-show="textEditor.busy"></i> Save</button>
<button type="button" class="btn btn-primary" ng-click="textEditor.maybeClose()">Close</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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,102 @@
<?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="546.13336"
height="546.13336"
viewBox="0 0 512.00001 512.00001"
id="svg4519"
version="1.1"
inkscape:version="0.92.2 2405546, 2018-03-11"
sodipodi:docname="appicon_fallback.svg"
inkscape:export-filename="/home/nebulon/projects/yellowtent/dashboard/src/img/appicon_fallback.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4521" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="89.894291"
inkscape:cy="162.5294"
inkscape:document-units="px"
inkscape:current-layer="g4496"
showgrid="false"
units="px"
inkscape:window-width="2880"
inkscape:window-height="1565"
inkscape:window-x="0"
inkscape:window-y="55"
inkscape:window-maximized="1" />
<metadata
id="metadata4524">
<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></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-540.36216)">
<g
id="g4467"
transform="matrix(20.50952,0,0,20.859456,-526.58031,-94.042799)">
<g
inkscape:export-ydpi="67.349998"
inkscape:export-xdpi="67.349998"
transform="matrix(0.59473169,0,0,0.59473169,31.04719,102.48374)"
id="g4382">
<g
id="g4496">
<path
sodipodi:type="star"
style="opacity:1;fill:#03a9f4;fill-opacity:1;stroke:none;stroke-width:1.10000002;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path4162"
sodipodi:sides="6"
sodipodi:cx="12.46875"
sodipodi:cy="-99.893143"
sodipodi:r1="19.266006"
sodipodi:r2="16.307295"
sodipodi:arg1="-0.52224059"
sodipodi:arg2="0.0013581913"
inkscape:flatsided="true"
inkscape:rounded="0.12490573"
inkscape:randomized="0"
d="m 29.166669,-109.50348 c 1.200386,2.08567 1.17988,17.183595 -0.02617,19.265993 -1.206046,2.082397 -14.291486,9.613601 -16.697919,9.610333 -2.406432,-0.0033 -15.4713664,-7.56999 -16.671752,-9.655655 -1.2003857,-2.085666 -1.1798799,-17.183591 0.026167,-19.265991 1.2060467,-2.0824 14.2914862,-9.6136 16.6979192,-9.61033 2.406432,0.003 15.471366,7.56999 16.671752,9.65565 z"
transform="rotate(-30,10.993604,-99.259973)"
inkscape:export-xdpi="67.349998"
inkscape:export-ydpi="67.349998" />
<rect
inkscape:transform-center-x="0.66390665"
ry="3.9522502"
y="-107.69034"
x="4.8100815"
height="14.288903"
width="14.288903"
id="rect4168-1-1"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:3.75875854;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
inkscape:transform-center-y="3.7035412e-06" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src/img/avatars/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -5,22 +5,27 @@
<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>&lrm;</title>
<link id="favicon" type="image/png" rel="icon" href="/api/v1/cloudron/avatar">
<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/slick.css?<%= revision %>"/>
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.min.css?<%= revision %>"/>
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css?<%= revision %>"/>
<link type="text/css" rel="stylesheet" href="/3rdparty/bootstrap-slider/bootstrap-slider.min.css?<%= revision %>"/>
<link type="text/css" rel="stylesheet" href="/theme.css?<%= revision %>">
<!-- Custom Fonts -->
<link type="text/css" rel="stylesheet" href="/3rdparty/css/font-awesome.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>
@@ -39,7 +44,7 @@
<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-slick.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/angular-fittext.min.js?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/autofill-event.js?<%= revision %>"></script>
@@ -50,13 +55,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?<%= revision %>"></script>
<script type="text/javascript" src="/3rdparty/js/Chart.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>
@@ -71,6 +78,9 @@
<!-- moment -->
<script type="text/javascript" src="/3rdparty/js/moment.min.js?<%= revision %>"></script>
<!-- timezone list -->
<script type="text/javascript" src="/js/timezones.js?<%= revision %>"></script>
<!-- Main Application -->
<script type="text/javascript" src="/js/index.js?<%= revision %>"></script>
@@ -88,27 +98,7 @@
</div>
</script>
<!-- Modal setup subscription -->
<div class="modal fade" id="setupSubscriptionModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Setup Subscription</h4>
</div>
<div class="modal-body">
<p ng-show="config.update.box">
You can update to the next version once you have <a ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.emailEncoded + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank">setup billing</a>.
</p>
<p>
Our paid plans allow you to install more apps and create more users.
</div>
<div class="modal-footer">
<a class="btn btn-success" ng-click="waitForPlanSelection()" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.emailEncoded + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank" ng-disabled="waitingForPlanSelection"><i class="fa fa-circle-o-notch fa-spin" ng-show="waitingForPlanSelection"></i> Setup Subscription</a>
</div>
</div>
</div>
</div>
<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="animateMe ng-hide layout-root" ng-show="initialized">
@@ -128,47 +118,49 @@
<!-- /.navbar-header -->
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right" ng-hide="hideNavBarActions">
<li ng-show="ready && subscription.plan.id === 'free'">
<a ng-href="" ng-click="showSubscriptionModal()" style="cursor: pointer">
<!-- trial expired -->
<span class="badge badge-success">Setup Subscription</span>
</a>
</li>
<li ng-show="ready && subscription && subscription.status === 'trialing' && !appstoreConfig.profile.billing">
<a ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=credit_card&email=' + appstoreConfig.profile.emailEncoded }}" target="_blank">
<!-- in trial -->
<span class="badge badge-success">Setup Subscription</span>
</a>
</li>
<li>
<a ng-class="{ active: isActive('/apps')}" href="#/apps"><i class="fa fa-cloud-download fa-fw"></i> My Apps</a>
</li>
<li ng-show="user.admin">
<a ng-class="{ active: isActive('/appstore')}" href="#/appstore"><i class="fa fa-th-large fa-fw"></i> App Store</a>
</li>
<li ng-show="user.admin">
<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.gravatar}}" style="margin-top: -4px;"/> {{user.username}} <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li>
<li ng-show="user.admin"><a href="#/activity"><i class="fa fa-list-alt fa-fw"></i> Activity</a></li>
<li ng-show="user.admin"><a href="#/tokens"><i class="fa fa-key fa-fw"></i> API Access</a></li>
<li ng-show="user.admin"><a href="#/backups"><i class="fa fa-archive fa-fw"></i> Backups</a></li>
<li ng-show="user.admin"><a href="#/domains"><i class="fa fa-globe fa-fw"></i> Domains</a></li>
<li ng-show="user.admin"><a href="#/email"><i class="fa fa-envelope fa-fw"></i> Email</a></li>
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li>
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
<li ng-show="user.admin" class="divider"></li>
<li ng-show="user.admin"><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
<li class="divider"></li>
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li>
</ul>
</li>
</ul>
<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-th fa-fw"></i> My Apps</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> 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>
<a href="#/notifications">
<i class="fas fa-bell" ng-show="notifications.length"></i>
<i class="far fa-bell" ng-hide="notifications.length"></i>
<span class="badge badge-danger" ng-show="notifications.length">{{ notifications.length }}</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</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.role === 'owner'"><a href="#/branding"><i class="fa fa-passport fa-fw"></i> Branding</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="#/network"><i class="fas fa-network-wired fa-fw"></i> Network</a></li>
<li ng-show="user.isAtLeastAdmin"><a href="#/services"><i class="fa fa-cogs fa-fw"></i> Services</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-chart-area fa-fw"></i> System</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>
</div>
</div>
</nav>
@@ -176,10 +168,7 @@
<div ng-view id="ng-view" class="layout-content"></div>
<footer class="text-center ng-cloak">
<span class="text-muted">&copy; 2018 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"> v{{config.version}}</span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://forum.cloudron.io" target="_blank">Forum <i class="fa fa-comments"></i></a></span>
<span class="text-muted" ng-bind-html="config.footer | markdown2html"></span>
</footer>
</div>

View File

@@ -1,213 +0,0 @@
'use strict';
/* global angular:false */
angular.module('Application').service('AppStore', ['$http', '$base64', 'Client', function ($http, $base64, Client) {
function AppStoreError(statusCode, message) {
Error.call(this);
this.name = this.constructor.name;
this.statusCode = statusCode;
if (typeof message == 'string') {
this.message = message;
} else {
this.message = JSON.stringify(message);
}
}
function AppStore() {
this._appsCache = [];
}
AppStore.prototype.getApps = function (callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
var that = this;
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/apps', { params: { boxVersion: Client.getConfig().version } }).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
angular.copy(data.apps, that._appsCache);
return callback(null, that._appsCache);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getAppsFast = function (callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
if (this._appsCache.length !== 0) return callback(null, this._appsCache);
this.getApps(callback);
};
AppStore.prototype.getAppById = function (appId, callback) {
var that = this;
// check cache
for (var app in this._appsCache) {
if (this._appsCache[app].id === appId) return callback(null, this._appsCache[app]);
}
this.getApps(function (error) {
if (error) return callback(error);
// recheck cache
for (var app in that._appsCache) {
if (that._appsCache[app].id === appId) return callback(null, that._appsCache[app]);
}
callback(new AppStoreError(404, 'Not found'));
});
};
AppStore.prototype.getAppByIdAndVersion = function (appId, version, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
// check cache
for (var app in this._appsCache) {
if (this._appsCache[app].id === appId && this._appsCache[app].manifest.version === version) return callback(null, this._appsCache[app]);
}
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/apps/' + appId + '/versions/' + version).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getAppById = function (appId, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
// do not check cache, always get the latest
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/apps/' + appId).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getManifest = function (appId, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
var manifestUrl = Client.getConfig().apiServerOrigin + '/api/v1/apps/' + appId;
console.log('Getting the manifest of ', appId, manifestUrl);
$http.get(manifestUrl).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.manifest);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getSizes = function (callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/sizes').success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.sizes);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getRegions = function (callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/regions').success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.regions);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.register = function (email, password, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
var data = {
email: email,
password: password
};
$http.post(Client.getConfig().apiServerOrigin + '/api/v1/users', data).success(function (data, status) {
if (status !== 201) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.login = function (email, password, totpToken, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
var data = {
email: email,
password: password,
persistent: true,
totpToken: totpToken
};
$http.post(Client.getConfig().apiServerOrigin + '/api/v1/login', data).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.logout = function (email, password, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.post(Client.getConfig().apiServerOrigin + '/api/v1/logout').success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getProfile = function (token, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/profile', { params: { accessToken: token }}).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
// just some helper property, since angular bindings cannot dot his easily
data.profile.emailEncoded = encodeURIComponent(data.profile.email);
return callback(null, data.profile);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getCloudronDetails = function (appstoreConfig, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId, { params: { accessToken: appstoreConfig.token }}).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.cloudron);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getSubscription = function (appstoreConfig, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/subscription', { params: { accessToken: appstoreConfig.token }}).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.subscription);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
return new AppStore();
}]);

File diff suppressed because it is too large Load Diff

641
src/js/filemanager.js Normal file
View File

@@ -0,0 +1,641 @@
'use strict';
/* global angular, $, async, monaco, Mimer */
require.config({ paths: { 'vs': '3rdparty/vs' }});
// create main application module
var app = angular.module('Application', ['angular-md5', 'ui-notification', 'ngDrag', 'ui.bootstrap', 'ui.bootstrap.contextMenu']);
angular.module('Application').filter('prettyOwner', function () {
return function (uid) {
if (uid === 0) return 'root';
if (uid === 33) return 'www-data';
if (uid === 1000) return 'cloudron';
if (uid === 1001) return 'git';
return uid;
}
});
// disable sce for footer https://code.angularjs.org/1.5.8/docs/api/ng/service/$sce
app.config(function ($sceProvider) {
$sceProvider.enabled(false);
});
app.filter("trustUrl", ['$sce', function ($sce) {
return function (recordingUrl) {
return $sce.trustAsResourceUrl(recordingUrl);
};
}]);
// https://stackoverflow.com/questions/25621321/angularjs-ng-drag
var ngDragEventDirectives = {};
angular.forEach(
'drag dragend dragenter dragexit dragleave dragover dragstart drop'.split(' '),
function(eventName) {
var directiveName = 'ng' + eventName.charAt(0).toUpperCase() + eventName.slice(1);
ngDragEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) {
return {
restrict: 'A',
compile: function($element, attr) {
var fn = $parse(attr[directiveName], null, true);
return function ngDragEventHandler(scope, element) {
element.on(eventName, function(event) {
var callback = function() {
fn(scope, {$event: event});
};
scope.$apply(callback);
});
};
}
};
}];
}
);
angular.module('ngDrag', []).directive(ngDragEventDirectives);
app.controller('FileManagerController', ['$scope', '$timeout', 'Client', function ($scope, $timeout, Client) {
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.initialized = false;
$scope.status = null;
$scope.busy = true;
$scope.client = Client;
$scope.cwd = '';
$scope.cwdParts = [];
$scope.appId = search.appId;
$scope.app = null;
$scope.entries = [];
$scope.dropToBody = false;
$scope.sortAsc = true;
$scope.sortProperty = 'fileName';
$scope.view = 'fileTree';
$scope.owners = [
{ name: 'cloudron', value: 1000 },
{ name: 'www-data', value: 33 },
{ name: 'git', value: 1001 },
{ name: 'root', value: 0 }
];
$scope.menuOptions = [
{
text: 'Download',
enabled: function ($itemScope, $event, entry) { return !entry.isDirectory },
click: function ($itemScope, $event, entry) { download(entry); }
}, {
text: 'Rename',
click: function ($itemScope, $event, entry) { $scope.renameEntry.show(entry); }
}, {
text: 'Change Ownership',
click: function ($itemScope, $event, entry) { $scope.chownEntry.show(entry); }
}, {
text: 'Delete',
hasTopDivider: true,
click: function ($itemScope, $event, entry) { $scope.entryRemove.show(entry); }
}
];
var LANGUAGES = [];
require(['vs/editor/editor.main'], function() {
LANGUAGES = monaco.languages.getLanguages();
});
function getLanguage(filename) {
var ext = '.' + filename.split('.').pop();
var language = LANGUAGES.find(function (l) { return !!l.extensions.find(function (e) { return e === ext; }); }) || '';
return language ? language.id : '';
}
function sanitize(filePath) {
filePath = filePath.split('/').filter(function (a) { return !!a; }).reduce(function (a, v) {
if (v === '.'); // do nothing
else if (v === '..') a.pop();
else a.push(v);
return a;
}, []).join('/');
return '/' + filePath;
}
function download(entry) {
var filePath = sanitize($scope.cwd + '/' + entry.fileName);
if (entry.isDirectory) return;
Client.filesGet($scope.appId, filePath, 'download', function (error) {
if (error) return Client.error(error);
});
};
$scope.dragEnter = function ($event, entry) {
$event.originalEvent.stopPropagation();
$event.originalEvent.preventDefault();
if (entry && entry.isDirectory) entry.hovered = true;
else $scope.dropToBody = true;
$event.originalEvent.dataTransfer.dropEffect = 'copy';
}
$scope.dragExit = function ($event, entry) {
$event.originalEvent.stopPropagation();
$event.originalEvent.preventDefault();
if (entry && entry.isDirectory) entry.hovered = false;
$scope.dropToBody = false;
$event.originalEvent.dataTransfer.dropEffect = 'copy';
}
$scope.drop = function (event, entry) {
event.originalEvent.stopPropagation();
event.originalEvent.preventDefault();
$scope.dropToBody = false;
if (!event.originalEvent.dataTransfer.items[0]) return;
var targetFolder = entry && entry.isDirectory ? entry.fileName : '';
// figure if a folder was dropped on a modern browser, in this case the first would have to be a directory
var folderItem;
try {
folderItem = event.originalEvent.dataTransfer.items[0].webkitGetAsEntry();
if (folderItem.isFile) return uploadFiles(event.originalEvent.dataTransfer.files, targetFolder);
} catch (e) {
return uploadFiles(event.originalEvent.dataTransfer.files, targetFolder);
}
// if we got here we have a folder drop and a modern browser
// now traverse the folder tree and create a file list
$scope.uploadStatus.busy = true;
$scope.uploadStatus.count = 0;
var fileList = [];
function traverseFileTree(item, path, callback) {
if (item.isFile) {
// Get file
item.file(function (file) {
fileList.push(file);
++$scope.uploadStatus.count;
callback();
});
} else if (item.isDirectory) {
// Get folder contents
var dirReader = item.createReader();
dirReader.readEntries(function (entries) {
async.each(entries, function (entry, callback) {
traverseFileTree(entry, path + item.name + '/', callback);
}, callback);
});
}
}
traverseFileTree(folderItem, '', function (error) {
$scope.uploadStatus.busy = false;
$scope.uploadStatus.count = 0;
if (error) return console.error(error);
uploadFiles(fileList, targetFolder);
});
}
$scope.refresh = function () {
$scope.busy = true;
Client.filesGet($scope.appId, $scope.cwd, 'data', function (error, result) {
$scope.busy = false;
if (error) return Client.error(error);
// amend icons
result.entries.forEach(function (e) {
e.icon = 'fa-file';
if (e.isDirectory) e.icon = 'fa-folder';
if (e.isSymbolicLink) e.icon = 'fa-link';
if (e.isFile) {
var mimeType = Mimer().get(e.fileName);
var mimeGroup = mimeType.split('/')[0];
if (mimeGroup === 'text') e.icon = 'fa-file-alt';
if (mimeGroup === 'image') e.icon = 'fa-file-image';
if (mimeGroup === 'video') e.icon = 'fa-file-video';
if (mimeGroup === 'audio') e.icon = 'fa-file-audio';
if (mimeType === 'text/csv') e.icon = 'fa-file-csv';
if (mimeType === 'application/pdf') e.icon = 'fa-file-pdf';
}
});
$scope.entries = result.entries;
});
};
$scope.open = function (entry) {
if ($scope.busy) return;
var filePath = sanitize($scope.cwd + '/' + entry.fileName);
if (entry.isDirectory) {
$scope.changeDirectory(filePath);
} else if (entry.isFile) {
var mimeType = Mimer().get(entry.fileName);
var mimeGroup = mimeType.split('/')[0];
if (mimeGroup === 'video' || mimeGroup === 'image') {
$scope.mediaViewer.show(entry);
} else if (mimeType === 'application/pdf') {
Client.filesGet($scope.appId, filePath, 'open', function (error) { if (error) return Client.error(error); });
} else if (mimeGroup === 'text' || mimeGroup === 'application') {
$scope.textEditor.show(entry);
} else {
Client.filesGet($scope.appId, filePath, 'open', function (error) { if (error) return Client.error(error); });
}
} else {}
};
$scope.goDirectoryUp = function () {
$scope.changeDirectory($scope.cwd + '/..');
};
$scope.changeDirectory = function (path) {
path = sanitize(path);
if ($scope.cwd === path) return;
location.hash = path;
$scope.cwd = path;
$scope.cwdParts = path.split('/').filter(function (p) { return !!p; }).map(function (p, i) { return { name: p, path: path.split('/').slice(0, i+2).join('/') }; });
$scope.refresh();
};
$scope.uploadStatus = {
busy: false,
fileName: '',
count: 0,
countDone: 0,
size: 0,
done: 0,
percentDone: 0
};
function uploadFiles(files, targetFolder) {
if (!files || !files.length) return;
targetFolder = targetFolder || '';
// prevent it from getting closed
$('#uploadModal').modal({
backdrop: 'static',
keyboard: false
});
$scope.uploadStatus.busy = true;
$scope.uploadStatus.count = files.length;
$scope.uploadStatus.countDone = 0;
$scope.uploadStatus.size = 0;
$scope.uploadStatus.done = 0;
$scope.uploadStatus.percentDone = 0;
for (var i = 0; i < files.length; ++i) {
$scope.uploadStatus.size += files[i].size;
}
async.eachSeries(files, function (file, callback) {
var filePath = sanitize($scope.cwd + '/' + targetFolder + '/' + (file.webkitRelativePath || file.name));
$scope.uploadStatus.fileName = file.name;
Client.filesUpload($scope.appId, filePath, file, function (loaded) {
$scope.uploadStatus.percentDone = ($scope.uploadStatus.done+loaded) * 100 / $scope.uploadStatus.size;
}, function (error) {
if (error) return callback(error);
$scope.uploadStatus.done += file.size;
$scope.uploadStatus.percentDone = $scope.uploadStatus.done * 100 / $scope.uploadStatus.size;
$scope.uploadStatus.countDone++;
callback();
});
}, function (error) {
if (error) console.error(error);
$('#uploadModal').modal('hide');
$scope.uploadStatus.busy = false;
$scope.uploadStatus.fileName = '';
$scope.uploadStatus.count = 0;
$scope.uploadStatus.size = 0;
$scope.uploadStatus.done = 0;
$scope.uploadStatus.percentDone = 100;
$scope.refresh();
});
}
// file upload
$('#uploadFileInput').on('change', function (e) { uploadFiles(e.target.files || []); });
$scope.onUploadFile = function () { $('#uploadFileInput').click(); };
// folder upload
$('#uploadFolderInput').on('change', function (e ) { uploadFiles(e.target.files || []); });
$scope.onUploadFolder = function () { $('#uploadFolderInput').click(); };
$scope.newDirectory = {
busy: false,
error: null,
name: '',
show: function () {
$scope.newDirectory.error = null;
$scope.newDirectory.name = '';
$scope.newDirectory.busy = false;
$scope.newDirectoryForm.$setUntouched();
$scope.newDirectoryForm.$setPristine();
$('#newDirectoryModal').modal('show');
},
submit: function () {
$scope.newDirectory.busy = true;
$scope.newDirectory.error = null;
var filePath = sanitize($scope.cwd + '/' + $scope.newDirectory.name);
Client.filesCreateDirectory($scope.appId, filePath, function (error, result) {
$scope.newDirectory.busy = false;
if (error && error.statusCode === 409) return $scope.newDirectory.error = 'Already exists';
if (error) return Client.error(error);
$scope.refresh();
$('#newDirectoryModal').modal('hide');
});
}
};
$scope.renameEntry = {
busy: false,
error: null,
entry: null,
newName: '',
show: function (entry) {
$scope.renameEntry.error = null;
$scope.renameEntry.entry = entry;
$scope.renameEntry.newName = entry.fileName;
$scope.renameEntry.busy = false;
$('#renameEntryModal').modal('show');
},
submit: function () {
$scope.renameEntry.busy = true;
var oldFilePath = sanitize($scope.cwd + '/' + $scope.renameEntry.entry.fileName);
var newFilePath = sanitize(($scope.renameEntry.newName[0] === '/' ? '' : ($scope.cwd + '/')) + $scope.renameEntry.newName);
Client.filesRename($scope.appId, oldFilePath, newFilePath, function (error, result) {
$scope.renameEntry.busy = false;
if (error) return Client.error(error);
$scope.refresh();
$('#renameEntryModal').modal('hide');
});
}
};
$scope.mediaViewer = {
type: '',
src: '',
entry: null,
show: function (entry) {
var filePath = sanitize($scope.cwd + '/' + entry.fileName);
Client.filesGet($scope.appId, filePath, 'link', function (error, result) {
if (error) return Client.error(error);
$scope.mediaViewer.entry = entry;
$scope.mediaViewer.type = Mimer().get(entry.fileName).split('/')[0];
$scope.mediaViewer.src = result;
$('#mediaViewerModal').modal('show');
});
}
};
$scope.textEditor = {
busy: false,
entry: null,
editor: null,
unsaved: false,
show: function (entry) {
$scope.textEditor.entry = entry;
$scope.textEditor.busy = false;
$scope.textEditor.unsaved = false;
// clear model if any
if ($scope.textEditor.editor && $scope.textEditor.editor.getModel()) $scope.textEditor.editor.setModel(null);
$scope.view = 'textEditor';
// document.getElementById('textEditorModal').style['display'] = 'flex';
var filePath = sanitize($scope.cwd + '/' + entry.fileName);
var language = getLanguage(entry.fileName);
Client.filesGet($scope.appId, filePath, 'data', function (error, result) {
if (error) return Client.error(error);
if (!$scope.textEditor.editor) {
$timeout(function () {
$scope.textEditor.editor = monaco.editor.create(document.getElementById('textEditorContainer'), {
value: result,
language: language,
theme: 'vs-dark'
});
$scope.textEditor.editor.getModel().onDidChangeContent(function () { $scope.textEditor.unsaved = true; });
}, 200);
} else {
$scope.textEditor.editor.setModel(monaco.editor.createModel(result, 'javascript'));
$scope.textEditor.editor.getModel().onDidChangeContent(function () { $scope.textEditor.unsaved = true; }); // have to re-attach whenever model changes
}
});
},
save: function (callback) {
$scope.textEditor.busy = true;
var newContent = $scope.textEditor.editor.getValue();
var filePath = sanitize($scope.cwd + '/' + $scope.textEditor.entry.fileName);
var file = new File([newContent], 'file');
Client.filesUpload($scope.appId, filePath, file, function () {}, function (error) {
if (error) return Client.error(error);
$timeout(function () {
$scope.textEditor.unsaved = false;
$scope.textEditor.busy = false;
if (callback) return callback();
}, 1000);
});
},
close: function () {
$scope.view = 'fileTree';
$('#textEditorCloseModal').modal('hide');
},
saveAndClose: function () {
$scope.textEditor.save(function () {
$scope.textEditor.close();
});
},
maybeClose: function () {
if (!$scope.textEditor.unsaved) return $scope.textEditor.close();
$('#textEditorCloseModal').modal('show');
},
};
$scope.chownEntry = {
busy: false,
error: null,
entry: null,
newOwner: 0,
recursive: false,
show: function (entry) {
$scope.chownEntry.error = null;
$scope.chownEntry.entry = entry;
$scope.chownEntry.newOwner = entry.uid;
$scope.chownEntry.busy = false;
// default for directories is recursive
$scope.chownEntry.recursive = entry.isDirectory;
$('#chownEntryModal').modal('show');
},
submit: function () {
$scope.chownEntry.busy = true;
var filePath = sanitize($scope.cwd + '/' + $scope.chownEntry.entry.fileName);
Client.filesChown($scope.appId, filePath, $scope.chownEntry.newOwner, $scope.chownEntry.recursive, function (error, result) {
$scope.chownEntry.busy = false;
if (error) return Client.error(error);
$scope.refresh();
$('#chownEntryModal').modal('hide');
});
}
};
$scope.entryRemove = {
busy: false,
error: null,
entry: null,
show: function (entry) {
$scope.entryRemove.error = null;
$scope.entryRemove.entry = entry;
$('#entryRemoveModal').modal('show');
},
submit: function () {
$scope.entryRemove.busy = true;
var filePath = sanitize($scope.cwd + '/' + $scope.entryRemove.entry.fileName);
Client.filesRemove($scope.appId, filePath, function (error, result) {
$scope.entryRemove.busy = false;
if (error) return Client.error(error);
$scope.refresh();
$('#entryRemoveModal').modal('hide');
});
}
};
function init() {
Client.getStatus(function (error, status) {
if (error) return Client.initError(error, init);
if (!status.activated) {
console.log('Not activated yet, redirecting', status);
window.location.href = '/';
return;
}
// check version and force reload if needed
if (!localStorage.version) {
localStorage.version = status.version;
} else if (localStorage.version !== status.version) {
localStorage.version = status.version;
window.location.reload(true);
}
$scope.status = status;
console.log('Running filemanager version ', localStorage.version);
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
Client.refreshUserInfo(function (error) {
if (error) return Client.initError(error, init);
Client.getApp($scope.appId, function (error, result) {
if (error) {
$scope.initialized = true;
return;
}
$scope.app = result;
// now mark the Client to be ready
Client.setReady();
$scope.changeDirectory(window.location.hash.slice(1));
$scope.initialized = true;
});
});
});
}
init();
window.addEventListener('hashchange', function () {
$scope.$apply(function () {
$scope.changeDirectory(window.location.hash.slice(1));
});
});
// setup all the dialog focus handling
['newDirectoryModal', 'renameEntryModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find("[autofocus]:first").focus();
});
});
// selects filename (without extension)
['renameEntryModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
var elem = $(this).find("[autofocus]:first");
var text = elem.val();
elem[0].setSelectionRange(0, text.indexOf('.'));
});
});
}]);

View File

@@ -1,8 +1,9 @@
'use strict';
/* global angular:false */
/* global showdown:false */
/* global moment:false */
/* global $:false */
/* global ERROR,ISTATES,HSTATES,RSTATES */
// deal with accessToken in the query, this is passed for example on password reset and account setup upon invite
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; }, {});
@@ -17,23 +18,6 @@ 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 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();
});
})();
}
// 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']);
@@ -42,11 +26,24 @@ app.config(['NotificationProvider', function (NotificationProvider) {
delay: 5000,
startTop: 60,
positionX: 'left',
maxCount: 3,
templateUrl: 'notification.html'
});
}]);
// configure resourceUrlWhitelist https://code.angularjs.org/1.5.8/docs/api/ng/provider/$sceDelegateProvider#resourceUrlWhitelist
app.config(function ($sceDelegateProvider) {
$sceDelegateProvider.resourceUrlWhitelist([
// Allow same origin resource loads.
'self',
// Allow loading from our assets domain.
'https://cloudron.io/**',
'https://staging.cloudron.io/**',
'https://dev.cloudron.io/**',
// Allow local development against the appstore pages
'http://localhost:5000/**'
]);
});
// setup all major application routes
app.config(['$routeProvider', function ($routeProvider) {
$routeProvider.when('/', {
@@ -54,6 +51,9 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/users', {
controller: 'UsersController',
templateUrl: 'views/users.html?<%= revision %>'
}).when('/app/:appId/:view?', {
controller: 'AppController',
templateUrl: 'views/app.html?<%= revision %>'
}).when('/appstore', {
controller: 'AppStoreController',
templateUrl: 'views/appstore.html?<%= revision %>'
@@ -63,24 +63,30 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/apps', {
controller: 'AppsController',
templateUrl: 'views/apps.html?<%= revision %>'
}).when('/account', {
controller: 'AccountController',
templateUrl: 'views/account.html?<%= revision %>'
}).when('/profile', {
controller: 'ProfileController',
templateUrl: 'views/profile.html?<%= revision %>'
}).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 %>'
}).when('/domains', {
controller: 'DomainsController',
templateUrl: 'views/domains.html?<%= revision %>'
}).when('/email', {
controller: 'EmailController',
templateUrl: 'views/email.html?<%= revision %>'
controller: 'EmailsController',
templateUrl: 'views/emails.html?<%= revision %>'
}).when('/email/:domain', {
controller: 'EmailController',
templateUrl: 'views/email.html?<%= revision %>'
}).when('/notifications', {
controller: 'NotificationsController',
templateUrl: 'views/notifications.html?<%= revision %>'
}).when('/settings', {
controller: 'SettingsController',
templateUrl: 'views/settings.html?<%= revision %>'
@@ -90,34 +96,18 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/support', {
controller: 'SupportController',
templateUrl: 'views/support.html?<%= revision %>'
}).when('/tokens', {
controller: 'TokensController',
templateUrl: 'views/tokens.html?<%= revision %>'
}).when('/system', {
controller: 'SystemController',
templateUrl: 'views/system.html?<%= revision %>'
}).when('/services', {
controller: 'ServicesController',
templateUrl: 'views/services.html?<%= revision %>'
}).otherwise({ redirectTo: '/'});
}]);
// keep in sync with appdb.js
var ISTATES = {
PENDING_INSTALL: 'pending_install',
PENDING_CLONE: 'pending_clone',
PENDING_CONFIGURE: 'pending_configure',
PENDING_UNINSTALL: 'pending_uninstall',
PENDING_RESTORE: 'pending_restore',
PENDING_UPDATE: 'pending_update',
PENDING_FORCE_UPDATE: 'pending_force_update',
PENDING_BACKUP: 'pending_backup',
ERROR: 'error',
INSTALLED: 'installed'
};
var HSTATES = {
HEALTHY: 'healthy',
UNHEALTHY: 'unhealthy',
ERROR: 'error',
DEAD: 'dead'
};
app.filter('installError', function () {
return function (app) {
if (!app) return false;
if (app.installationState === ISTATES.ERROR) return true;
if (app.installationState === ISTATES.INSTALLED) {
// app.health can also be null to indicate insufficient data
@@ -128,87 +118,224 @@ app.filter('installError', function () {
};
});
app.filter('activeTask', function () {
return function (app) {
if (!app) return false;
return app.taskId !== null;
};
});
app.filter('installSuccess', function () {
return function (app) {
if (!app) return false;
return app.installationState === ISTATES.INSTALLED;
};
});
app.filter('activeOAuthClients', function () {
return function (clients, user) {
return clients.filter(function (c) { return user.admin || (c.activeTokens && c.activeTokens.length > 0); });
app.filter('appIsInstalledAndHealthy', function () {
return function (app) {
if (!app) return false;
return (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && app.runState === RSTATES.RUNNING);
};
});
app.filter('prettyAppMessage', function () {
return function (message) {
if (message === 'ETRYAGAIN') return 'The DNS record for this location is not setup correctly. Please verify your DNS settings and repair this app.';
if (message === 'DNS Record already exists') return 'The DNS record for this location already exists. Manually remove the DNS record and then click on repair.';
app.filter('applicationLink', function() {
return function(app) {
if (!app) return '';
if (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && app.runState === RSTATES.RUNNING && !app.pendingPostInstallConfirmation) {
return 'https://' + app.fqdn;
} else {
return '';
}
};
});
// this appears when an item in app grid is clicked
app.filter('prettyAppErrorMessage', function () {
return function (app) {
if (!app) return '';
if (app.installationState === ISTATES.INSTALLED) {
// app.health can also be null to indicate insufficient data
if (app.health === HSTATES.UNHEALTHY) return 'The app is not responding to health checks. Check the logs for any error messages.';
}
if (app.error.reason === 'Access Denied') {
if (app.error.domain) return 'The DNS record for this location is not setup correctly. Please verify your DNS settings and repair this app.';
} else if (app.error.reason === 'Already Exists') {
if (app.error.domain) return 'The DNS record for this location already exists. Cloudron does not remove existing DNS records. Manually remove the DNS record and then click on repair.';
}
return app.error.message;
};
});
// this appears as tool tip in app grid
app.filter('appProgressMessage', function () {
return function (app) {
var message = app.message || (app.error ? app.error.message : '');
return message;
};
});
app.filter('shortAppMessage', function () {
return function (message) {
if (message === 'ETRYAGAIN') return 'DNS record not setup correctly';
return message;
// 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('prettyMemory', function () {
return function (memory) {
// Adjust the default memory limit if it changes
return memory ? Math.floor(memory / 1024 / 1024) : 256;
app.filter('selectedTagFilter', function () {
return function selectedTagFilter(apps, selectedTags) {
return apps.filter(function (app) {
if (selectedTags.length === 0) return true;
if (!app.tags) return false;
for (var i = 0; i < selectedTags.length; i++) {
if (app.tags.indexOf(selectedTags[i]) === -1) return false;
}
return true;
});
};
});
app.filter('selectedDomainFilter', function () {
return function selectedDomainFilter(apps, selectedDomain) {
return apps.filter(function (app) {
if (!selectedDomain) return true;
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; });
});
};
});
app.filter('appSearchFilter', function () {
return function appSearchFilter(apps, appSearch) {
return apps.filter(function (app) {
if (!appSearch) return true;
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);
});
};
});
app.filter('prettyDomains', function () {
return function prettyDomains(domains) {
return domains.map(function (d) { return d.domain; }).join(', ');
};
});
app.filter('installationActive', function () {
return function(app) {
return function (app) {
if (app.installationState === ISTATES.ERROR) return false;
if (app.installationState === ISTATES.INSTALLED) return false;
return true;
};
});
app.filter('installationStateLabel', function() {
// for better DNS errors
function detailedError(app) {
if (app.installationProgress === 'ETRYAGAIN') return 'DNS Error';
return 'Error';
}
// this appears in the app grid
app.filter('installationStateLabel', function () {
return function(app) {
var waiting = app.progress === 0 ? ' (Pending)' : '';
if (!app) return '';
var waiting = app.progress === 0 ? ' (Queued)' : '';
switch (app.installationState) {
case ISTATES.PENDING_INSTALL:
case ISTATES.PENDING_CLONE:
return 'Installing' + waiting;
case ISTATES.PENDING_CONFIGURE: return 'Configuring' + waiting;
case ISTATES.PENDING_CLONE:
return 'Cloning' + waiting;
case ISTATES.PENDING_LOCATION_CHANGE:
case ISTATES.PENDING_CONFIGURE:
case ISTATES.PENDING_RECREATE_CONTAINER:
case ISTATES.PENDING_DEBUG:
return 'Configuring' + waiting;
case ISTATES.PENDING_RESIZE:
return 'Resizing' + waiting;
case ISTATES.PENDING_DATA_DIR_MIGRATION:
return 'Migrating data' + waiting;
case ISTATES.PENDING_UNINSTALL: return 'Uninstalling' + waiting;
case ISTATES.PENDING_RESTORE: return 'Restoring' + waiting;
case ISTATES.PENDING_UPDATE: return 'Updating' + waiting;
case ISTATES.PENDING_FORCE_UPDATE: return 'Updating' + waiting;
case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting;
case ISTATES.ERROR: return detailedError(app);
case ISTATES.PENDING_START: return 'Starting' + waiting;
case ISTATES.PENDING_STOP: return 'Stopping' + waiting;
case ISTATES.PENDING_RESTART: return 'Restarting' + waiting;
case ISTATES.ERROR: {
if (app.error && app.error.message === 'ETRYAGAIN') return 'DNS Error';
return 'Error';
}
case ISTATES.INSTALLED: {
if (app.debugMode) {
return app.debugMode.readonlyRootfs ? 'Paused (Repair)' : 'Paused (Debug)';
return 'Recovery Mode';
} else if (app.runState === 'running') {
if (!app.health) return 'Starting...'; // no data yet
if (app.health === HSTATES.HEALTHY) return 'Running';
return 'Not responding'; // dead/exit/unhealthy
} else if (app.runState === 'pending_start') return 'Starting...';
else if (app.runState === 'pending_stop') return 'Stopping...';
else if (app.runState === 'stopped') return 'Stopped';
} else if (app.runState === 'stopped') return 'Stopped';
else return app.runState;
break;
}
default: return app.installationState;
}
};
});
app.filter('taskName', function () {
return function(installationState) {
switch (installationState) {
case ISTATES.PENDING_INSTALL: return 'install';
case ISTATES.PENDING_CLONE: return 'clone';
case ISTATES.PENDING_LOCATION_CHANGE: return 'location change';
case ISTATES.PENDING_CONFIGURE: return 'configure';
case ISTATES.PENDING_RECREATE_CONTAINER: return 'create container';
case ISTATES.PENDING_DEBUG: return 'debug';
case ISTATES.PENDING_RESIZE: return 'resize';
case ISTATES.PENDING_DATA_DIR_MIGRATION: return 'data migration';
case ISTATES.PENDING_UNINSTALL: return 'uninstall';
case ISTATES.PENDING_RESTORE: return 'restore';
case ISTATES.PENDING_UPDATE: return 'update';
case ISTATES.PENDING_BACKUP: return 'backup';
case ISTATES.PENDING_START: return 'start app';
case ISTATES.PENDING_STOP: return 'stop app';
case ISTATES.PENDING_RESTART: return 'restart app';
default: return installationState || '';
}
};
});
app.filter('errorSuggestion', function () {
return function (error) {
if (!error) return '';
switch (error.reason) {
case ERROR.ACCESS_DENIED:
if (error.domain) return 'Check the DNS credentials of ' + error.domain.domain + ' in the Domains & Certs view';
return '';
case ERROR.COLLECTD_ERROR: return 'Check if collectd is running on the server';
case ERROR.DATABASE_ERROR: return 'Check if MySQL database is running on the server';
case ERROR.DOCKER_ERROR: return 'Check if docker is running on the server';
case ERROR.DNS_ERROR: return 'Check if the DNS service of the domain is running';
case ERROR.LOGROTATE_ERROR: return 'Check if logrotate is running on the server';
case ERROR.NETWORK_ERROR: return 'Check if there are any network issues on the server';
case ERROR.REVERSEPROXY_ERROR: return 'Check if nginx is running on the server';
default: return '';
}
};
});
app.filter('readyToUpdate', function () {
return function (apps) {
return apps.every(function (app) {
@@ -225,25 +352,6 @@ app.filter('inProgressApps', function () {
};
});
app.filter('ignoreAdminGroup', function () {
return function (groups) {
return groups.filter(function (group) {
if (group.id) return group.id !== 'admin';
return group !== 'admin';
});
};
});
app.filter('applicationLink', function() {
return function(app) {
if (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY) {
return 'https://' + app.fqdn;
} else {
return '';
}
};
});
app.filter('prettyHref', function () {
return function (input) {
if (!input) return input;
@@ -255,8 +363,8 @@ app.filter('prettyHref', function () {
app.filter('prettyDate', function () {
// http://ejohn.org/files/pretty.js
return function prettyDate(time) {
var date = new Date(time),
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);
@@ -278,171 +386,22 @@ app.filter('prettyDate', function () {
});
app.filter('prettyLongDate', function () {
return function prettyLongDate(time) {
return moment(time).format('MMMM Do YYYY, h:mm:ss a');
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('markdown2html', function () {
var converter = new showdown.Converter({
extensions: ['targetblank'],
simplifiedAutoLink: true,
strikethrough: true,
tables: true
});
return function (text) {
return converter.makeHtml(text);
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('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];
};
});
// keep this in sync with eventlog.js and CLI tool
var ACTION_ACTIVATE = 'cloudron.activate';
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_LOGIN = 'app.login';
var ACTION_BACKUP_FINISH = 'backup.finish';
var ACTION_BACKUP_START = 'backup.start';
var ACTION_BACKUP_CLEANUP = 'backup.cleanup';
var ACTION_CERTIFICATE_RENEWAL = 'certificate.renew';
var ACTION_START = 'cloudron.start';
var ACTION_UPDATE = 'cloudron.update';
var ACTION_USER_ADD = 'user.add';
var ACTION_USER_LOGIN = 'user.login';
var ACTION_USER_REMOVE = 'user.remove';
var ACTION_USER_UPDATE = 'user.update';
app.filter('eventLogSource', function() {
return function(eventLog) {
var source = eventLog.source;
var data = eventLog.data;
var errorMessage = data.errorMessage;
// <span ng-show="eventLog.source.ip || eventLog.source.appId"> ({{ eventLog.source.ip || eventLog.source.appId }}) </span>
var line = source.username || source.userId || source.authType || 'system';
if (source.app) line += ' - ' + source.app.fqdn;
else if (source.ip) line += ' - ' + source.ip;
else if (source.appId) line += ' - ' + source.appId;
return line;
};
});
app.filter('eventLogDetails', function() {
// NOTE: if you change this, the CLI tool (cloudron machine eventlog) probably needs fixing as well
return function(eventLog) {
var source = eventLog.source;
var data = eventLog.data;
var errorMessage = data.errorMessage;
switch (eventLog.action) {
case ACTION_ACTIVATE:
return 'Cloudron was activated';
case ACTION_APP_CONFIGURE:
return (data.app ? (data.app.manifest.title + ' was re-configured at ' + (data.app.fqdn || data.app.location)) : '');
case ACTION_APP_INSTALL:
return (data.app ? (data.app.manifest.title + ' was installed at ' + (data.app.fqdn || data.app.location)) : '');
case ACTION_APP_RESTORE:
return (data.app ? (data.app.manifest.title + ' was restored at ' + (data.app.fqdn || data.app.location)) : '');
case ACTION_APP_UNINSTALL:
return (data.app ? (data.app.manifest.title + ' was uninstalled at ' + (data.app.fqdn || data.app.location)) : '');
case ACTION_APP_UPDATE:
return (data.app ? (data.app.manifest.title + ' at ' + (data.app.fqdn || data.app.location)) : '') + ' was updated to version ' + data.toManifest.id + '@' + data.toManifest.version;
case ACTION_APP_LOGIN:
return 'App ' + data.appId + ' logged in';
case ACTION_BACKUP_START:
return 'Backup started';
case ACTION_BACKUP_FINISH:
return 'Backup finished' + (errorMessage ? (' error: ' + errorMessage) : '');
case ACTION_BACKUP_CLEANUP:
return 'Backup ' + data.backup.id + ' removed';
case ACTION_CERTIFICATE_RENEWAL:
return 'Certificate renewal for ' + data.domain + (errorMessage ? ' failed' : ' succeeded');
case ACTION_START:
return 'Cloudron started with version ' + data.version;
case ACTION_UPDATE:
return 'Cloudron was updated to version ' + data.boxUpdateInfo.version;
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_LOGIN:
return (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' logged in';
default: return eventLog.action;
}
};
});
app.filter('eventLogAction', function() {
// NOTE: if you change this, the CLI tool (cloudron machine eventlog) probably needs fixing as well
return function(eventLog) {
var source = eventLog.source;
var data = eventLog.data;
var errorMessage = data.errorMessage;
switch (eventLog.action) {
case ACTION_ACTIVATE: return 'Cloudron activated';
case ACTION_APP_CONFIGURE: return 'App configured';
case ACTION_APP_INSTALL: return 'App installed';
case ACTION_APP_RESTORE: return 'App restored';
case ACTION_APP_UNINSTALL: return 'App uninstalled';
case ACTION_APP_UPDATE: return 'App updated';
case ACTION_APP_LOGIN: return 'App login';
case ACTION_BACKUP_START: return 'Backup started';
case ACTION_BACKUP_FINISH: return 'Backup finished';
case ACTION_BACKUP_CLEANUP: return 'Backup removed';
case ACTION_CERTIFICATE_RENEWAL: return 'Certificate renewal';
case ACTION_START: return 'Cloudron started';
case ACTION_UPDATE: return 'Platform updated';
case ACTION_USER_ADD: return 'User added';
case ACTION_USER_LOGIN: return 'User login';
case ACTION_USER_REMOVE: return 'User removed';
case ACTION_USER_UPDATE: return 'User updated';
default: return eventLog.action;
}
app.filter('prettyEmailAddresses', function () {
return function prettyEmailAddresses(addresses) {
if (!addresses || addresses === '<>') return '<>';
if (Array.isArray(addresses)) return addresses.map(function (a) { return a.slice(1, -1); }).join(', ');
return addresses.slice(1, -1);
};
});
@@ -485,7 +444,7 @@ app.run(['$route', '$rootScope', '$location', function ($route, $rootScope, $loc
app.directive('ngClickSelect', function () {
return {
restrict: 'AC',
link: function (scope, element, attrs) {
link: function (scope, element/*, attrs */) {
element.bind('click', function () {
var selection = window.getSelection();
var range = document.createRange();
@@ -526,7 +485,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;
@@ -534,18 +494,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) {
@@ -558,7 +520,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;
@@ -571,6 +534,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) {
@@ -582,7 +548,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()');
}
@@ -590,9 +556,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)">&times;</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)">&times;</button>' +
'</div>' +
'<input type="text" data-ng-model="tagText" ng-blur="addTag()" placeholder="{{placeholder}}"/>' +
'</div>'

149
src/js/login.js Normal file
View File

@@ -0,0 +1,149 @@
'use strict';
/* global angular, $, showdown */
// create main application module
var app = angular.module('Application', []);
app.filter('markdown2html', function () {
var converter = new showdown.Converter({
simplifiedAutoLink: true,
strikethrough: true,
tables: true,
openLinksInNewWindow: true
});
return function (text) {
return converter.makeHtml(text);
};
});
// disable sce for footer https://code.angularjs.org/1.5.8/docs/api/ng/service/$sce
app.config(function ($sceProvider) {
$sceProvider.enabled(false);
});
app.controller('LoginController', ['$scope', '$http', function ($scope, $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; }, {});
$scope.initialized = false;
$scope.mode = '';
$scope.busy = false;
$scope.error = false;
$scope.status = null;
$scope.username = '';
$scope.password = '';
$scope.totpToken = '';
$scope.passwordResetIdentifier = '';
$scope.newPassword = '';
$scope.newPasswordRepeat = '';
var API_ORIGIN = '<%= apiOrigin %>' || window.location.origin;
$scope.onLogin = function () {
$scope.busy = true;
$scope.error = false;
var data = {
username: $scope.username,
password: $scope.password,
totpToken: $scope.totpToken
};
function error() {
$scope.busy = false;
$scope.error = true;
$scope.password = '';
$scope.loginForm.$setPristine();
setTimeout(function () { $('#inputPassword').focus(); }, 200);
}
$http.post(API_ORIGIN + '/api/v1/cloudron/login', data).success(function (data, status) {
if (status !== 200) return error();
localStorage.token = data.accessToken;
window.location.href = search.returnTo || '/';
}).error(error);
};
$scope.onPasswordReset = function () {
$scope.busy = true;
var data = {
identifier: $scope.passwordResetIdentifier
};
function done() {
$scope.busy = false;
$scope.mode = 'passwordResetDone';
}
$http.post(API_ORIGIN + '/api/v1/cloudron/password_reset_request', data).success(done).error(done);
};
$scope.onNewPassword = function () {
$scope.busy = true;
var data = {
resetToken: search.resetToken,
password: $scope.newPassword
};
function error(status) {
console.log('error', status)
$scope.busy = false;
if (status === 401) $scope.error = 'Invalid reset token';
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);
// set token to autologin
localStorage.token = data.accessToken;
$scope.mode = 'newPasswordDone';
}).error(function (data, status) {
error(status);
});
};
$scope.showPasswordReset = function () {
window.document.title = 'Password Reset';
$scope.mode = 'passwordReset';
$scope.passwordResetIdentifier = '';
setTimeout(function () { $('#inputPasswordResetIdentifier').focus(); }, 200);
};
$scope.showLogin = function () {
if ($scope.status) window.document.title = $scope.status.cloudronName + ' Login';
$scope.mode = 'login';
$scope.error = false;
setTimeout(function () { $('#inputUsername').focus(); }, 200);
};
$scope.showNewPassword = function () {
window.document.title = 'Set New Password';
$scope.mode = 'newPassword';
setTimeout(function () { $('#inputNewPassword').focus(); }, 200);
};
$http.get(API_ORIGIN + '/api/v1/cloudron/status').success(function (data, status) {
$scope.initialized = true;
if (status !== 200) return;
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();
}]);

View File

@@ -1,31 +1,22 @@
'use strict';
/* global angular */
/* global moment */
/* global $ */
// create main application module
var app = angular.module('Application', ['angular-md5', 'ui-notification']);
app.controller('LogsController', ['$scope', '$timeout', '$location', 'Client', function ($scope, $timeout, $location, Client) {
app.controller('LogsController', ['$scope', 'Client', function ($scope, Client) {
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.initialized = false;
$scope.installedApps = Client.getInstalledApps();
$scope.client = Client;
$scope.logs = [];
$scope.selected = '';
$scope.activeEventSource = null;
$scope.lines = 100;
$scope.selectedAppInfo = null;
// Add built-in log types for now
$scope.logs.push({ name: 'System (All)', type: 'platform', value: 'all', url: Client.makeURL('/api/v1/cloudron/logs?units=all') });
$scope.logs.push({ name: 'Box', type: 'platform', value: 'box', url: Client.makeURL('/api/v1/cloudron/logs?units=box') });
$scope.logs.push({ name: 'Mail', type: 'platform', value: 'mail', url: Client.makeURL('/api/v1/cloudron/logs?units=mail') });
$scope.error = function (error) {
console.error(error);
window.location.href = '/error.html';
};
$scope.selectedTaskInfo = null;
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
@@ -36,17 +27,34 @@ app.controller('LogsController', ['$scope', '$timeout', '$location', 'Client', f
logViewer.empty();
};
$scope.showTerminal = function () {
if (!$scope.selected) return;
window.open('/terminal.html?id=' + $scope.selected.value, 'Cloudron Terminal', 'width=1024,height=800');
// https://github.com/janl/mustache.js/blob/master/mustache.js#L60
var entityMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
};
function escapeHtml(string) {
return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
return entityMap[s];
});
}
function showLogs() {
if (!$scope.selected) return;
var func = $scope.selected.type === 'platform' ? Client.getPlatformLogs : Client.getAppLogs;
func($scope.selected.value, true, $scope.lines, function handleLogs(error, result) {
var func;
if ($scope.selected.type === 'platform') func = Client.getPlatformLogs;
else if ($scope.selected.type === 'service') func = Client.getServiceLogs;
else if ($scope.selected.type === 'task') func = Client.getTaskLogs;
else if ($scope.selected.type === 'app') func = Client.getAppLogs;
func($scope.selected.value, true /* follow */, $scope.lines, function handleLogs(error, result) {
if (error) return console.error(error);
$scope.activeEventSource = result;
@@ -64,8 +72,9 @@ app.controller('LogsController', ['$scope', '$timeout', '$location', 'Client', f
var autoScroll = tmp[0].scrollTop > (tmp[0].scrollHeight - tmp.innerHeight() - 24);
var logLine = $('<div class="log-line">');
var timeString = moment.utc(data.realtimeTimestamp/1000).format('MMM DD HH:mm:ss');
logLine.html('<span class="time">' + timeString + ' </span>' + window.ansiToHTML(typeof data.message === 'string' ? data.message : ab2str(data.message)));
// realtimeTimestamp is 0 if line is blank or some parse error
var timeString = data.realtimeTimestamp ? moment(data.realtimeTimestamp/1000).format('MMM DD HH:mm:ss') : '';
logLine.html('<span class="time">' + timeString + ' </span>' + window.ansiToHTML(escapeHtml(typeof data.message === 'string' ? data.message : ab2str(data.message))));
tmp.append(logLine);
if (autoScroll) tmp[0].lastChild.scrollIntoView({ behavior: 'instant', block: 'end' });
@@ -73,69 +82,109 @@ app.controller('LogsController', ['$scope', '$timeout', '$location', 'Client', f
});
}
Client.onApps(function () {
if ($scope.selected.type !== 'app') return;
function select(ids, callback) {
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') },
{ name: 'MongoDB', type: 'service', value: 'mongodb', url: Client.makeURL('/api/v1/services/mongodb/logs') },
{ name: 'MySQL', type: 'service', value: 'mysql', url: Client.makeURL('/api/v1/services/mysql/logs') },
{ 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') },
];
var appId = $scope.selected.value;
$scope.selected = BUILT_IN_LOGS.find(function (e) { return e.value === ids.id; });
callback();
} else if (ids.crashId) {
$scope.selected = {
type: 'platform',
value: 'crash-' + ids.crashId,
name: 'Crash',
url: Client.makeURL('/api/v1/cloudron/logs/crash-' + ids.crashId)
};
Client.getApp(appId, function (error, result) {
if (error) return console.error(error);
callback();
} else if (ids.appId) {
Client.getApp(ids.appId, function (error, app) {
if (error) return callback(error);
$scope.selectedAppInfo = result;
});
});
$scope.selectedAppInfo = app;
Client.getStatus(function (error, status) {
if (error) return $scope.error(error);
$scope.selected = {
type: 'app',
value: app.id,
name: app.fqdn + ' (' + app.manifest.title + ')',
url: Client.makeURL('/api/v1/apps/' + app.id + '/logs'),
addons: app.manifest.addons
};
if (!status.activated) {
console.log('Not activated yet, redirecting', status);
window.location.href = '/';
return;
callback();
});
} else if (ids.taskId) {
Client.getTask(ids.taskId, function (error, task) {
if (error) return callback(error);
$scope.selectedTaskInfo = task;
$scope.selected = {
type: 'task',
value: task.id,
name: task.type,
url: Client.makeURL('/api/v1/tasks/' + task.id + '/logs')
};
callback();
});
}
}
// check version and force reload if needed
if (!localStorage.version) {
localStorage.version = status.version;
} else if (localStorage.version !== status.version) {
localStorage.version = status.version;
window.location.reload(true);
}
function init() {
console.log('Running log version ', localStorage.version);
Client.getStatus(function (error, status) {
if (error) return Client.initError(error, init);
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
Client.refreshUserInfo(function (error) {
if (error) return $scope.error(error);
if (!status.activated) {
console.log('Not activated yet, redirecting', status);
window.location.href = '/';
return;
}
Client.refreshConfig(function (error) {
if (error) return $scope.error(error);
// check version and force reload if needed
if (!localStorage.version) {
localStorage.version = status.version;
} else if (localStorage.version !== status.version) {
localStorage.version = status.version;
window.location.reload(true);
}
Client.refreshInstalledApps(function (error) {
if (error) return $scope.error(error);
console.log('Running log version ', localStorage.version);
Client.getInstalledApps().forEach(function (app) {
$scope.logs.push({
type: 'app',
value: app.id,
name: app.fqdn + ' (' + app.manifest.title + ')',
url: Client.makeURL('/api/v1/apps/' + app.id + '/logs'),
addons: app.manifest.addons
});
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
Client.refreshUserInfo(function (error) {
if (error) return Client.initError(error, init);
Client.refreshConfig(function (error) {
if (error) return Client.initError(error, init);
select({ id: search.id, taskId: search.taskId, appId: search.appId, crashId: search.crashId }, function (error) {
if (error) return Client.initError(error, init);
// now mark the Client to be ready
Client.setReady();
$scope.initialized = true;
showLogs();
});
// activate pre-selected log from query otherwise choose the first one
$scope.selected = $scope.logs.find(function (e) { return e.value === search.id; });
if (!$scope.selected) $scope.selected = $scope.logs[0];
// now mark the Client to be ready
Client.setReady();
$scope.initialized = true;
showLogs();
});
});
});
});
}
init();
}]);

View File

@@ -1,16 +1,16 @@
'use strict';
angular.module('Application').controller('MainController', ['$scope', '$route', '$timeout', '$location', 'Client', 'AppStore', function ($scope, $route, $timeout, $location, Client, AppStore) {
$scope.initialized = false;
/* global angular */
/* global $ */
angular.module('Application').controller('MainController', ['$scope', '$route', '$timeout', '$location', 'Client', function ($scope, $route, $timeout, $location, Client) {
$scope.initialized = false; // used to animate the UI
$scope.user = Client.getUserInfo();
$scope.installedApps = Client.getInstalledApps();
$scope.config = {};
$scope.status = {};
$scope.client = Client;
$scope.appstoreConfig = {};
$scope.subscription = {};
$scope.ready = false;
$scope.notifications = [];
$scope.hideNavBarActions = $location.path() === '/logs';
$scope.isActive = function (url) {
@@ -24,182 +24,122 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
Client.logout();
};
$scope.error = function (error) {
console.error(error);
window.location.href = '/error.html';
$scope.openSubscriptionSetup = function () {
Client.openSubscriptionSetup($scope.subscription);
};
$scope.waitingForPlanSelection = false;
$('#setupSubscriptionModal').on('hide.bs.modal', function () {
$scope.waitingForPlanSelection = false;
// NOTE: this function is exported and called from the appstore.js
$scope.updateSubscriptionStatus = function () {
if (!Client.getUserInfo().isAtLeastAdmin) return;
// check for updates to stay in sync
Client.checkForUpdates(function (error) {
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);
Client.refreshConfig();
$scope.subscription = subscription;
});
});
$scope.waitForPlanSelection = function () {
if ($scope.waitingForPlanSelection) return;
$scope.waitingForPlanSelection = true;
function checkPlan() {
if (!$scope.waitingForPlanSelection) return;
AppStore.getSubscription($scope.appstoreConfig, function (error, result) {
if (error) return console.error(error);
// check again to give more immediate feedback once a subscription was setup
if (result.plan.id === 'free') {
$timeout(checkPlan, 5000);
} else {
$scope.waitingForPlanSelection = false;
$('#setupSubscriptionModal').modal('hide');
if ($scope.config.update && $scope.config.update.box) $('#updateModal').modal('show');
}
});
}
checkPlan();
};
$scope.showSubscriptionModal = function () {
$('#setupSubscriptionModal').modal('show');
};
function refreshNotifications(poll) {
Client.getNotifications(false, 1, 100, function (error, results) {
if (error) console.error(error);
else $scope.notifications = results;
function runConfigurationChecks() {
// warn user if dns config is not working (the 'configuring' flag detects if configureWebadmin is 'active')
if (!$scope.status.webadminStatus.configuring && !$scope.status.webadminStatus.dns) {
var dnsActionScope = $scope.$new(true);
dnsActionScope.action = '/#/domains';
Client.notify('Invalid Domain Config', 'Unable to update DNS. Click here to update it.', true, 'error', dnsActionScope);
}
if ($scope.config.update && $scope.config.update.box) {
var updateActionScope = $scope.$new(true);
updateActionScope.action = '/#/settings';
Client.notify('Update Available', 'Update now to version ' + $scope.config.update.box.version + '.', true, 'success', updateActionScope);
}
Client.getBackupConfig(function (error, backupConfig) {
if (error) return console.error(error);
if (backupConfig.provider === 'noop') {
var actionScope = $scope.$new(true);
actionScope.action = '/#/settings';
Client.notify('Backup Configuration', 'Cloudron backups are disabled. Ensure the server is backed up using alternate means.', false, 'info', actionScope);
}
if (poll) $timeout(refreshNotifications, 60 * 1000);
});
}
$scope.fetchAppstoreProfileAndSubscription = function (callback) {
Client.getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
if (!appstoreConfig.token) return callback();
AppStore.getProfile(appstoreConfig.token, function (error, result) {
if (error) return callback(error);
// assign late to avoid UI flicketing on update
appstoreConfig.profile = result;
$scope.appstoreConfig = appstoreConfig;
AppStore.getSubscription($scope.appstoreConfig, function (error, result) {
if (error) return callback(error);
$scope.subscription = result;
callback();
});
});
});
// update state of acknowledged notification
$scope.notificationAcknowledged = function (notificationId) {
// remove notification from list
$scope.notifications = $scope.notifications.filter(function (n) { return n.id !== notificationId; });
};
Client.getStatus(function (error, status) {
if (error) return $scope.error(error);
function init() {
Client.getStatus(function (error, status) {
if (error) return Client.initError(error, init);
// WARNING if anything about the routing is changed here test these use-cases:
//
// 1. Caas
// 2. selfhosted with --domain argument
// 3. selfhosted restore
// 4. local development with gulp develop
// WARNING if anything about the routing is changed here test these use-cases:
//
// 1. Caas
// 3. selfhosted restore
// 4. local development with gulp develop
if (!status.activated) {
console.log('Not activated yet, redirecting', status);
window.location.href = status.adminFqdn ? '/setup.html' : '/setupdns.html';
return;
}
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';
} else {
window.location.href = status.adminFqdn ? '/setup.html' : '/setupdns.html';
}
return;
}
// support local development with localhost check
if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost') {
// user is accessing by IP or by the old admin location (pre-migration)
window.location.href = '/setupdns.html';
return;
}
// support local development with localhost check
if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost') {
// user is accessing by IP or by the old admin location (pre-migration)
window.location.href = '/setupdns.html';
return;
}
$scope.status = status;
// check version and force reload if needed
if (!localStorage.version) {
localStorage.version = status.version;
} else if (localStorage.version !== status.version) {
localStorage.version = status.version;
window.location.reload(true);
}
// check version and force reload if needed
if (!localStorage.version) {
localStorage.version = status.version;
} else if (localStorage.version !== status.version) {
localStorage.version = status.version;
window.location.reload(true);
}
console.log('Running dashboard version ', localStorage.version);
console.log('Running dashboard version ', localStorage.version);
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
Client.refreshUserInfo(function (error) {
if (error) return Client.initError(error, init);
// get user profile as the first thing. this populates the "scope" and affects subsequent API calls
Client.refreshUserInfo(function (error) {
if (error) return $scope.error(error);
Client.refreshConfig(function (error) {
if (error) return Client.initError(error, init);
Client.refreshConfig(function (error) {
if (error) return $scope.error(error);
Client.refreshInstalledApps(function (error) {
if (error) return Client.initError(error, init);
Client.refreshInstalledApps(function (error) {
if (error) return $scope.error(error);
// now mark the Client to be ready
Client.setReady();
// now mark the Client to be ready
Client.setReady();
$scope.config = Client.getConfig();
$scope.config = Client.getConfig();
$scope.initialized = true;
$scope.initialized = true;
if (Client.getConfig().mandatory2FA && !Client.getUserInfo().twoFactorAuthenticationEnabled) {
$location.path('/profile').search({ setup2fa: true });
return;
}
if ($scope.user.admin) {
runConfigurationChecks();
refreshNotifications(true);
$scope.fetchAppstoreProfileAndSubscription(function (error) {
if (error) console.error(error);
$scope.ready = true;
});
}
$scope.updateSubscriptionStatus();
});
});
});
});
});
}
Client.onConfig(function (config) {
// check if we are actually updating
if (config.progress.update && config.progress.update.percent !== -1) {
window.location.href = '/update.html';
}
if (config.cloudronName) {
document.title = config.cloudronName;
}
});
Client.onReconnect(function () {
refreshNotifications(false);
});
init();
// setup all the dialog focus handling
['updateModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find("[autofocus]:first").focus();
$(this).find('[autofocus]:first').focus();
});
});
}]);

View File

@@ -1,9 +1,11 @@
'use strict';
/* global angular */
/* global tld */
/* global $ */
// create main application module
var app = angular.module('Application', ['angular-md5', 'ui-notification']);
var app = angular.module('Application', ['angular-md5', 'ui-notification', 'ui.bootstrap']);
app.filter('zoneName', function () {
return function (domain) {
@@ -11,11 +13,15 @@ app.filter('zoneName', function () {
};
});
app.controller('RestoreController', ['$scope', '$http', 'Client', function ($scope, $http, Client) {
app.controller('RestoreController', ['$scope', 'Client', function ($scope, Client) {
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.client = Client;
$scope.busy = false;
$scope.error = {};
$scope.message = ''; // progress
// variables here have to match the import config logic!
$scope.provider = '';
$scope.bucket = '';
$scope.prefix = '';
@@ -29,10 +35,35 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
$scope.instanceId = '';
$scope.acceptSelfSignedCerts = false;
$scope.format = 'tgz';
$scope.advancedVisible = false;
$scope.password = '';
$scope.encrypted = false; // only used if a backup config contains that flag
$scope.sysinfo = {
provider: 'generic',
ip: '',
ifname: ''
};
$scope.sysinfoProvider = [
{ name: 'Public IP', value: 'generic' },
{ name: 'Static IP Address', value: 'fixed' },
{ name: 'Network Interface', value: 'network-interface' }
];
$scope.prettySysinfoProviderName = function (provider) {
switch (provider) {
case 'generic': return 'Public IP';
case 'fixed': return 'Static IP Address';
case 'network-interface': return 'Network Interface';
default: return 'Unknown';
}
};
// List is from http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
$scope.s3Regions = [
{ name: 'Asia Pacific (Mumbai)', value: 'ap-south-1' },
{ name: 'Asia Pacific (Osaka-Local)', value: 'ap-northeast-3' },
{ name: 'Asia Pacific (Seoul)', value: 'ap-northeast-2' },
{ name: 'Asia Pacific (Singapore)', value: 'ap-southeast-1' },
{ name: 'Asia Pacific (Sydney)', value: 'ap-southeast-2' },
@@ -41,6 +72,8 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
{ name: 'EU (Frankfurt)', value: 'eu-central-1' },
{ name: 'EU (Ireland)', value: 'eu-west-1' },
{ name: 'EU (London)', value: 'eu-west-2' },
{ name: 'EU (Paris)', value: 'eu-west-3' },
{ name: 'EU (Stockholm)', value: 'eu-north-1' },
{ name: 'South America (São Paulo)', value: 'sa-east-1' },
{ name: 'US East (N. Virginia)', value: 'us-east-1' },
{ name: 'US East (Ohio)', value: 'us-east-2' },
@@ -50,18 +83,61 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
$scope.doSpacesRegions = [
{ name: 'AMS3', value: 'https://ams3.digitaloceanspaces.com' },
{ name: 'FRA1', value: 'https://fra1.digitaloceanspaces.com' },
{ name: 'NYC3', value: 'https://nyc3.digitaloceanspaces.com' },
{ name: 'SFO2', value: 'https://sfo2.digitaloceanspaces.com' },
{ name: 'SGP1', value: 'https://sgp1.digitaloceanspaces.com' }
];
$scope.exoscaleSosRegions = [
{ name: 'AT-VIE-1', value: 'https://sos-at-vie-1.exo.io' },
{ name: 'CH-DK-2', value: 'https://sos-ch-dk-2.exo.io' },
{ name: 'CH-GVA-2', value: 'https://sos-ch-gva-2.exo.io' },
{ name: 'DE-FRA-1', value: 'https://sos-de-fra-1.exo.io' },
];
// https://www.scaleway.com/docs/object-storage-feature/
$scope.scalewayRegions = [
{ name: 'FR-PAR', value: 'https://s3.fr-par.scw.cloud', region: 'fr-par' }, // default
{ name: 'NL-AMS', value: 'https://s3.nl-ams.scw.cloud', region: 'nl-ams' }
];
$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' },
];
$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' },
];
$scope.wasabiRegions = [
{ name: 'EU Central 1', value: 'https://s3.eu-central-1.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: 'DigitalOcean Spaces', value: 'digitalocean-spaces' },
{ name: 'Exoscale SOS', value: 'exoscale-sos' },
{ name: 'Filesystem', value: 'filesystem' },
{ name: 'Google Cloud Storage', value: 'gcs' },
{ name: 'Linode Object Storage', value: 'linode-objectstorage' },
{ name: 'Minio', value: 'minio' },
{ name: 'OVH Object Storage', value: 'ovh-objectstorage' },
{ name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' },
{ name: 'S3 API Compatible (v4)', value: 's3-v4-compat' },
{ name: 'Wasabi', value: 'wasabi' }
];
$scope.formats = [
@@ -70,7 +146,9 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
];
$scope.s3like = function (provider) {
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos' || provider === 'digitalocean-spaces';
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos'
|| provider === 'digitalocean-spaces' || provider === 'wasabi' || provider === 'scaleway-objectstorage'
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'backblaze-b2';
};
$scope.restore = function () {
@@ -79,9 +157,9 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
var backupConfig = {
provider: $scope.provider,
key: $scope.key,
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)) {
@@ -94,13 +172,26 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
if (backupConfig.provider === 's3') {
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.endpoint = 'https://sos-ch-dk-2.exo.io';
backupConfig.region = 'us-east-1';
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'wasabi') {
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;
backupConfig.signatureVersion = 'v4';
} 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 === 'digitalocean-spaces') {
backupConfig.region = 'us-east-1';
}
@@ -135,6 +226,13 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
return;
}
if ($scope.backupId.indexOf('box') === -1) {
$scope.error.generic = 'Backup id must contain "box"';
$scope.error.backupId = true;
$scope.busy = false;
return;
}
var version = $scope.backupId.match(/_v(\d+.\d+.\d+)/);
if (!version) {
$scope.error.generic = 'Backup id is missing version information';
@@ -143,11 +241,20 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
return;
}
Client.restore(backupConfig, $scope.backupId.replace(/\.tar\.gz(\.enc)?$/, ''), version ? version[1] : '', function (error) {
var sysinfoConfig = {
provider: $scope.sysinfo.provider
};
if ($scope.sysinfo.provider === 'fixed') {
sysinfoConfig.ip = $scope.sysinfo.ip;
} else if ($scope.sysinfo.provider === 'network-interface') {
sysinfoConfig.ifname = $scope.sysinfo.ifname;
}
Client.restore(backupConfig, $scope.backupId.replace(/\.tar\.gz(\.enc)?$/, ''), version ? version[1] : '', sysinfoConfig, function (error) {
$scope.busy = false;
if (error) {
if (error.statusCode === 402) {
if (error.statusCode === 424) {
$scope.error.generic = error.message;
if (error.message.indexOf('AWS Access Key Id') !== -1) {
@@ -187,16 +294,24 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
waitForRestore();
});
}
};
function waitForRestore() {
$scope.busy = true;
Client.getStatus(function (error, status) {
if (!error && !status.webadminStatus.restoring) {
window.location.href = '/';
if (!error && !status.restore.active) { // restore finished
if (status.restore.errorMessage) {
$scope.busy = false;
$scope.error.generic = status.restore.errorMessage;
} else { // restore worked, redirect to admin page
window.location.href = '/';
}
return;
}
if (!error) $scope.message = status.restore.message;
setTimeout(waitForRestore, 5000);
});
}
@@ -219,20 +334,47 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
document.getElementById('gcsKeyFileInput').onchange = readFileLocally($scope.gcsKey, 'content', 'keyFileName');
Client.getStatus(function (error, status) {
if (error) {
window.location.href = '/error.html';
return;
}
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');
if (status.restoring) return waitForRestore();
var backupConfig;
try {
backupConfig = JSON.parse(result.target.result);
} catch (e) {
console.error('Unable to parse backup config');
return;
}
if (status.activated) {
window.location.href = '/';
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]);
};
$scope.instanceId = search.instanceId;
$scope.initialized = true;
});
function init() {
Client.getStatus(function (error, status) {
if (error) return Client.initError(error, init);
if (status.restore.active) return waitForRestore();
if (status.restore.errorMessage) $scope.error.generic = status.restore.errorMessage;
if (status.activated) {
window.location.href = '/';
return;
}
$scope.status = status;
$scope.instanceId = search.instanceId;
$scope.initialized = true;
});
}
init();
}]);

View File

@@ -1,57 +1,63 @@
'use strict';
/* global angular */
/* global $ */
// create main application module
var app = angular.module('Application', ['angular-md5', 'ui-notification', 'ui.bootstrap']);
app.controller('SetupController', ['$scope', '$http', 'Client', function ($scope, $http, Client) {
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 = {
$scope.apiServerOrigin = '';
$scope.webServerOrigin = '';
$scope.owner = {
error: null,
busy: false,
email: '',
displayName: '',
requireEmail: false,
username: '',
password: ''
};
$scope.error = null;
$scope.provider = '';
$scope.apiServerOrigin = '';
$scope.setupToken = '';
password: '',
$scope.activateCloudron = function () {
$scope.busy = true;
$scope.error = null;
submit: function () {
$scope.owner.busy = true;
$scope.owner.error = null;
Client.createAdmin($scope.account.username, $scope.account.password, $scope.account.email, $scope.account.displayName, $scope.setupToken, 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;
}
Client.createAdmin($scope.owner.username, $scope.owner.password, $scope.owner.email, $scope.owner.displayName, 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;
}
window.location.href = '/';
});
setView('finished');
});
}
};
Client.getStatus(function (error, status) {
if (error) {
window.location.href = '/error.html';
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.provider !== 'caas' && status.adminFqdn && status.adminFqdn !== window.location.hostname) {
if (status.adminFqdn && status.adminFqdn !== window.location.hostname) {
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
return;
}
@@ -66,32 +72,34 @@ app.controller('SetupController', ['$scope', '$http', 'Client', function ($scope
window.location.href = '/';
return;
}
}
if (status.provider === 'caas') {
if (!search.setupToken) {
window.location.href = '/error.html?errorCode=2';
return;
}
if (!search.email) {
window.location.href = '/error.html?errorCode=3';
return;
}
$scope.setupToken = search.setupToken;
function setView(view) {
if (view === 'finished') {
$scope.view = 'finished';
} else {
$scope.view = 'owner';
}
}
$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;
function init() {
Client.getStatus(function (error, status) {
if (error) return Client.initError(error, init);
$scope.initialized = true;
redirectIfNeeded(status);
setView(search.view);
// Ensure we have a good autofocus
setTimeout(function () {
$(document).find("[autofocus]:first").focus();
}, 250);
});
$scope.apiServerOrigin = status.apiServerOrigin;
$scope.webServerOrigin = status.webServerOrigin;
$scope.initialized = true;
// Ensure we have a good autofocus
setTimeout(function () {
$(document).find("[autofocus]:first").focus();
}, 250);
});
}
init();
}]);

104
src/js/setupaccount.js Normal file
View File

@@ -0,0 +1,104 @@
'use strict';
/* global angular, $, showdown */
// create main application module
var app = angular.module('Application', []);
app.filter('markdown2html', function () {
var converter = new showdown.Converter({
simplifiedAutoLink: true,
strikethrough: true,
tables: true,
openLinksInNewWindow: true
});
return function (text) {
return converter.makeHtml(text);
};
});
// disable sce for footer https://code.angularjs.org/1.5.8/docs/api/ng/service/$sce
app.config(function ($sceProvider) {
$sceProvider.enabled(false);
});
app.controller('SetupAccountController', ['$scope', '$http', function ($scope, $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; }, {});
var API_ORIGIN = '<%= apiOrigin %>' || window.location.origin;
$scope.initialized = false;
$scope.busy = false;
$scope.error = null;
$scope.view = 'setup';
$scope.status = null;
$scope.profileLocked = !!search.profileLocked;
$scope.existingUsername = !!search.username;
$scope.username = search.username || '';
$scope.displayName = search.displayName || '';
$scope.password = '';
$scope.passwordRepeat = '';
$scope.onSubmit = function () {
$scope.busy = true;
$scope.error = null;
var data = {
resetToken: search.resetToken,
password: $scope.password
};
if (!$scope.profileLocked) {
data.username = $scope.username;
data.displayName = $scope.displayName;
}
function error(data, status) {
$scope.busy = false;
if (status === 401) {
$scope.view = 'invalidToken';
} else if (status === 409) {
$scope.error = {
username: true,
message: 'Username already taken'
};
$scope.setupAccountForm.username.$setPristine();
setTimeout(function () { $('#inputUsername').focus(); }, 200);
} else if (status === 400) {
$scope.error = {
message: data.message
};
if (data.message.indexOf('Username') === 0) {
$scope.setupAccountForm.username.$setPristine();
$scope.error.username = true;
}
} else {
$scope.error = { message: 'Unknown error. Please try again later.' };
console.error(status, data);
}
}
$http.post(API_ORIGIN + '/api/v1/cloudron/setup_account', data).success(function (data, status) {
if (status !== 201) return error(data, status);
// set token to autologin
localStorage.token = data.accessToken;
$scope.view = 'done';
}).error(error);
};
$http.get(API_ORIGIN + '/api/v1/cloudron/status').success(function (data, status) {
$scope.initialized = true;
if (status !== 200) return;
$scope.status = data;
}).error(function () {
$scope.initialized = false;
});
}]);

View File

@@ -1,6 +1,6 @@
'use strict';
/* global tld */
/* global $, tld, angular */
// create main application module
var app = angular.module('Application', ['angular-md5', 'ui-notification', 'ui.bootstrap']);
@@ -15,18 +15,50 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.state = null; // 'initialized', 'waitingForDnsSetup', 'waitingForBox'
$scope.error = null;
$scope.error = {};
$scope.provider = '';
$scope.showDNSSetup = false;
$scope.instanceId = '';
$scope.isDomain = false;
$scope.isSubdomain = false;
$scope.advancedVisible = false;
$scope.webServerOrigin = '';
$scope.clipboardDone = false;
$scope.tlsProvider = [
{ name: 'Let\'s Encrypt Prod', value: 'letsencrypt-prod' },
{ name: 'Let\'s Encrypt Prod - Wildcard', value: 'letsencrypt-prod-wildcard' },
{ name: 'Let\'s Encrypt Staging', value: 'letsencrypt-staging' },
{ name: 'Self-Signed', value: 'fallback' },
{ name: 'Let\'s Encrypt Staging - Wildcard', value: 'letsencrypt-staging-wildcard' },
{ name: 'Self-Signed', value: 'fallback' }, // this is not 'Custom' because we don't allow user to upload certs during setup phase
];
$scope.sysinfo = {
provider: 'generic',
ip: '',
ifname: ''
};
$scope.sysinfoProvider = [
{ name: 'Public IP', value: 'generic' },
{ name: 'Static IP Address', value: 'fixed' },
{ name: 'Network Interface', value: 'network-interface' }
];
$scope.prettySysinfoProviderName = function (provider) {
switch (provider) {
case 'generic': return 'Public IP';
case 'fixed': return 'Static IP Address';
case 'network-interface': return 'Network Interface';
default: return 'Unknown';
}
};
$scope.needsPort80 = function (dnsProvider, tlsProvider) {
return ((dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') &&
(tlsProvider === 'letsencrypt-prod' || tlsProvider === 'letsencrypt-staging'));
};
// If we migrate the api origin we have to poll the new location
if (search.admin_fqdn) Client.apiOrigin = 'https://' + search.admin_fqdn;
@@ -46,20 +78,19 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
// keep in sync with domains.js
$scope.dnsProvider = [
{ name: 'AWS Route53', value: 'route53' },
{ name: 'Cloudflare (DNS only)', value: 'cloudflare' },
{ name: 'Digital Ocean', value: 'digitalocean' },
{ name: 'Cloudflare', value: 'cloudflare' },
{ name: 'DigitalOcean', value: 'digitalocean' },
{ name: 'Gandi LiveDNS', value: 'gandi' },
{ name: 'GoDaddy', value: 'godaddy' },
{ name: 'Google Cloud DNS', value: 'gcdns' },
{ name: 'Name.com', value: 'namecom' },
{ name: 'Namecheap', value: 'namecheap' },
{ name: 'Wildcard', value: 'wildcard' },
{ name: 'Manual (not recommended)', value: 'manual' },
{ name: 'No-op (only for development)', value: 'noop' }
];
$scope.dnsCredentials = {
error: null,
busy: false,
advancedVisible: false,
domain: '',
accessKeyId: '',
secretAccessKey: '',
@@ -68,17 +99,32 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
gandiApiKey: '',
cloudflareEmail: '',
cloudflareToken: '',
cloudflareTokenType: 'GlobalApiKey',
godaddyApiKey: '',
godaddyApiSecret: '',
linodeToken: '',
nameComUsername: '',
nameComToken: '',
namecheapUsername: '',
namecheapApiKey: '',
provider: 'route53',
zoneName: '',
tlsConfig: {
provider: 'letsencrypt-prod'
provider: 'letsencrypt-prod-wildcard'
}
};
$scope.setDefaultTlsProvider = function () {
var dnsProvider = $scope.dnsCredentials.provider;
// wildcard LE won't work without automated DNS
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') {
$scope.dnsCredentials.tlsConfig.provider = 'letsencrypt-prod';
} else {
$scope.dnsCredentials.tlsConfig.provider = 'letsencrypt-prod-wildcard';
}
};
function readFileLocally(obj, file, fileName) {
return function (event) {
$scope.$apply(function () {
@@ -99,64 +145,91 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
$scope.setDnsCredentials = function () {
$scope.dnsCredentials.busy = true;
$scope.dnsCredentials.error = null;
$scope.error = null;
$scope.error = {};
var provider = $scope.dnsCredentials.provider;
var data = {
providerToken: $scope.instanceId
};
// special case the wildcard provider
if (provider === 'wildcard') {
provider = 'manual';
data.wildcard = true;
}
var config = {};
if (provider === 'route53') {
data.accessKeyId = $scope.dnsCredentials.accessKeyId;
data.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
config.accessKeyId = $scope.dnsCredentials.accessKeyId;
config.secretAccessKey = $scope.dnsCredentials.secretAccessKey;
} else if (provider === 'gcdns') {
try {
var serviceAccountKey = JSON.parse($scope.dnsCredentials.gcdnsKey.content);
data.projectId = serviceAccountKey.project_id;
data.credentials = {
config.projectId = serviceAccountKey.project_id;
config.credentials = {
client_email: serviceAccountKey.client_email,
private_key: serviceAccountKey.private_key
};
if (!data.projectId || !data.credentials || !data.credentials.client_email || !data.credentials.private_key) {
throw 'fields_missing';
if (!config.projectId || !config.credentials || !config.credentials.client_email || !config.credentials.private_key) {
throw new Error('One or more fields are missing in the JSON');
}
} catch(e) {
$scope.dnsCredentials.error = 'Cannot parse Google Service Account Key';
} catch (e) {
$scope.error.dnsCredentials = 'Cannot parse Google Service Account Key: ' + e.message;
$scope.dnsCredentials.busy = false;
return;
}
} else if (provider === 'digitalocean') {
data.token = $scope.dnsCredentials.digitalOceanToken;
config.token = $scope.dnsCredentials.digitalOceanToken;
} else if (provider === 'gandi') {
data.token = $scope.dnsCredentials.gandiApiKey;
config.token = $scope.dnsCredentials.gandiApiKey;
} else if (provider === 'godaddy') {
data.apiKey = $scope.dnsCredentials.godaddyApiKey;
data.apiSecret = $scope.dnsCredentials.godaddyApiSecret;
config.apiKey = $scope.dnsCredentials.godaddyApiKey;
config.apiSecret = $scope.dnsCredentials.godaddyApiSecret;
} else if (provider === 'cloudflare') {
data.email = $scope.dnsCredentials.cloudflareEmail;
data.token = $scope.dnsCredentials.cloudflareToken;
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 === 'namecom') {
data.username = $scope.dnsCredentials.nameComUsername;
data.token = $scope.dnsCredentials.nameComToken;
config.username = $scope.dnsCredentials.nameComUsername;
config.token = $scope.dnsCredentials.nameComToken;
} else if (provider === 'namecheap') {
config.token = $scope.dnsCredentials.namecheapApiKey;
config.username = $scope.dnsCredentials.namecheapUsername;
}
Client.setupDnsConfig($scope.dnsCredentials.domain, $scope.dnsCredentials.zoneName, provider, data, $scope.dnsCredentials.tlsConfig, function (error) {
if (error && error.statusCode === 403) {
var tlsConfig = {
provider: $scope.dnsCredentials.tlsConfig.provider,
wildcard: false
};
if ($scope.dnsCredentials.tlsConfig.provider.indexOf('-wildcard') !== -1) {
tlsConfig.provider = tlsConfig.provider.replace('-wildcard', '');
tlsConfig.wildcard = true;
}
var sysinfoConfig = {
provider: $scope.sysinfo.provider
};
if ($scope.sysinfo.provider === 'fixed') {
sysinfoConfig.ip = $scope.sysinfo.ip;
} else if ($scope.sysinfo.provider === 'network-interface') {
sysinfoConfig.ifname = $scope.sysinfo.ifname;
}
var data = {
dnsConfig: {
domain: $scope.dnsCredentials.domain,
zoneName: $scope.dnsCredentials.zoneName,
provider: provider,
config: config,
tlsConfig: tlsConfig
},
sysinfoConfig: sysinfoConfig,
providerToken: $scope.instanceId
};
Client.setup(data, function (error) {
if (error) {
$scope.dnsCredentials.busy = false;
$scope.error = 'Wrong instance id provided.';
return;
} else if (error) {
$scope.dnsCredentials.busy = false;
$scope.dnsCredentials.error = error.message;
if (error.statusCode === 422) {
$scope.error.ami = error.message;
} else {
$scope.error.dnsCredentials = error.message;
}
return;
}
@@ -168,12 +241,19 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
$scope.state = 'waitingForDnsSetup';
Client.getStatus(function (error, status) {
// webadminStatus.dns is intentionally not tested. it can be false if dns creds are invalid
// runConfigurationChecks() in main.js will pick the .dns and show a notification
if (!error && status.adminFqdn && status.webadminStatus.tls) {
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
if (!error && !status.setup.active) {
if (!status.adminFqdn || status.setup.errorMessage) { // setup reset or errored. start over
$scope.error.setup = status.setup.errorMessage;
$scope.state = 'initialized';
$scope.dnsCredentials.busy = false;
} else { // proceed to activation
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
}
return;
}
$scope.message = status.setup.message;
setTimeout(waitForDnsSetup, 5000);
});
}
@@ -190,19 +270,31 @@ app.controller('SetupDNSController', ['$scope', '$http', '$timeout', 'Client', f
// domain is currently like a lock flag
if (status.adminFqdn) return waitForDnsSetup();
if (status.provider === 'digitalocean') $scope.dnsCredentials.provider = 'digitalocean';
if (status.provider === 'gce') $scope.dnsCredentials.provider = 'gcdns';
if (status.provider === 'ami') {
// remove route53 on ami
$scope.dnsProvider.shift();
$scope.dnsCredentials.provider = 'wildcard';
if (status.provider === 'digitalocean' || status.provider === 'digitalocean-mp') {
$scope.dnsCredentials.provider = 'digitalocean';
// don't suggest linode by default since it takes a while for DNS to propagate
// } else if (status.provider === 'linode' || status.provider === 'linode-oneclick' || status.provider === 'linode-stackscript') {
// $scope.dnsCredentials.provider = 'linode';
} else if (status.provider === 'gce') {
$scope.dnsCredentials.provider = 'gcdns';
} else if (status.provider === 'ami') {
$scope.dnsCredentials.provider = 'route53';
}
$scope.instanceId = search.instanceId;
$scope.provider = status.provider;
$scope.webServerOrigin = status.webServerOrigin;
$scope.state = 'initialized';
setTimeout(function () { $("[autofocus]:first").focus(); }, 100);
});
}
var clipboard = new Clipboard('.clipboard');
clipboard.on('success', function () {
$scope.$apply(function () { $scope.clipboardDone = true; });
$timeout(function () { $scope.clipboardDone = false; }, 5000);
});
initialize();
}]);

View File

@@ -1,11 +1,11 @@
'use strict';
/* global Terminal */
/* global angular, $, Terminal, AttachAddon, FitAddon, ISTATES */
// create main application module
var app = angular.module('Application', ['angular-md5', 'ui-notification']);
angular.module('Application', ['angular-md5', 'ui-notification']);
app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client', function ($scope, $timeout, $location, Client) {
angular.module('Application').controller('TerminalController', ['$scope', '$timeout', '$location', 'Client', function ($scope, $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();
@@ -15,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;
@@ -28,7 +29,7 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
downloadUrl: function () {
if (!$scope.downloadFile.filePath) return '';
var filePath = $scope.downloadFile.filePath.replace(/\/*\//g, '/');
var filePath = encodeURIComponent($scope.downloadFile.filePath);
return Client.apiOrigin + '/api/v1/apps/' + $scope.selected.value + '/download?file=' + filePath + '&access_token=' + Client.getToken();
},
@@ -100,19 +101,6 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
fileUpload.click();
};
$scope.populateDropdown = function () {
Client.getInstalledApps().forEach(function (app) {
$scope.apps.push({
type: 'app',
value: app.id,
name: app.fqdn + ' (' + app.manifest.title + ')',
addons: app.manifest.addons
});
});
// $scope.selected = $scope.apps[0];
};
$scope.usesAddon = function (addon) {
if (!$scope.selected || !$scope.selected.addons) return false;
return !!Object.keys($scope.selected.addons).find(function (a) { return a === addon; });
@@ -120,7 +108,7 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
function reset() {
if ($scope.terminal) {
$scope.terminal.destroy();
$scope.terminal.dispose();
$scope.terminal = null;
}
@@ -133,92 +121,27 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
$scope.restartApp = function () {
$scope.restartAppBusy = true;
$scope.appBusy = true;
var appId = $scope.selected.value;
function waitUntilStopped(callback) {
Client.refreshInstalledApps(function (error) {
function waitUntilRestarted(callback) {
refreshApp(appId, function (error, result) {
if (error) return callback(error);
Client.getApp(appId, function (error, result) {
if (error) return callback(error);
if (result.runState === 'stopped') return callback();
setTimeout(waitUntilStopped.bind(null, callback), 2000);
});
if (result.installationState === ISTATES.INSTALLED) return callback();
setTimeout(waitUntilRestarted.bind(null, callback), 2000);
});
}
Client.stopApp(appId, function (error) {
if (error) return console.error('Failed to stop app.', error);
Client.restartApp(appId, function (error) {
if (error) console.error('Failed to restart app.', error);
waitUntilStopped(function (error) {
if (error) return console.error('Failed to get app status.', error);
waitUntilRestarted(function (error) {
if (error) console.error('Failed wait for restart.', error);
Client.startApp(appId, function (error) {
if (error) console.error('Failed to start app.', error);
$scope.restartAppBusy = false;
});
});
});
};
$scope.repairApp = function () {
$('#repairAppModal').modal('show');
};
$scope.repairAppBegin = function () {
$scope.appBusy = true;
function waitUntilInRepairState() {
Client.refreshInstalledApps(function (error) {
if (error) return console.error('Failed to refresh app status.', error);
Client.getApp($scope.selected.value, function (error, result) {
if (error) return console.error('Failed to get app status.', error);
if (result.installationState === 'installed') $scope.appBusy = false;
else setTimeout(waitUntilInRepairState, 2000);
});
});
}
Client.debugApp($scope.selected.value, true, function (error) {
if (error) return console.error(error);
Client.refreshInstalledApps(function (error) {
if (error) console.error(error);
$('#repairAppModal').modal('hide');
waitUntilInRepairState();
});
});
};
$scope.repairAppDone = function () {
$scope.appBusy = true;
function waitUntilInNormalState() {
Client.refreshInstalledApps(function (error) {
if (error) return console.error('Failed to refresh app status.', error);
Client.getApp($scope.selected.value, function (error, result) {
if (error) return console.error('Failed to get app status.', error);
if (result.installationState === 'installed') $scope.appBusy = false;
else setTimeout(waitUntilInNormalState, 2000);
});
});
}
Client.debugApp($scope.selected.value, false, function (error) {
if (error) return console.error(error);
Client.refreshInstalledApps(function (error) {
if (error) console.error(error);
waitUntilInNormalState();
$scope.restartAppBusy = false;
$scope.appBusy = false;
});
});
};
@@ -228,7 +151,7 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
// 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();
$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
@@ -242,6 +165,16 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
}
}
function refreshApp(id, callback) {
Client.getApp(id, function (error, result) {
if (error) return callback(error);
$scope.selectedAppInfo = result;
callback(null, result);
});
}
function showTerminal(retry) {
reset();
@@ -249,21 +182,19 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
var appId = $scope.selected.value;
Client.getApp(appId, function (error, result) {
refreshApp(appId, function (error) {
if (error) return console.error(error);
// we expect this to be called _after_ a reconfigure was issued
if (result.installationState === 'pending_configure') {
$scope.appBusy = true;
} else if (result.installationState === 'installed') {
$scope.appBusy = false;
}
var result = $scope.selectedAppInfo;
$scope.selectedAppInfo = result;
$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;
@@ -278,14 +209,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);
});
}
@@ -293,12 +229,34 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
$scope.terminalInject = function (addon, extra) {
if (!$scope.terminalSocket) return;
var cmd;
if (addon === 'mysql') cmd = 'mysql --user=${MYSQL_USERNAME} --password=${MYSQL_PASSWORD} --host=${MYSQL_HOST} ${MYSQL_DATABASE}';
else if (addon === 'postgresql') cmd = 'PGPASSWORD=${POSTGRESQL_PASSWORD} psql -h ${POSTGRESQL_HOST} -p ${POSTGRESQL_PORT} -U ${POSTGRESQL_USERNAME} -d ${POSTGRESQL_DATABASE}';
else if (addon === 'mongodb') cmd = 'mongo -u "${MONGODB_USERNAME}" -p "${MONGODB_PASSWORD}" ${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_DATABASE}';
else if (addon === 'redis') cmd = 'redis-cli -h "${REDIS_HOST}" -p "${REDIS_PORT}" -a "${REDIS_PASSWORD}"';
else if (addon === 'scheduler' && extra) cmd = extra.command;
var cmd, manifestVersion = $scope.selected.manifest.manifestVersion;
if (addon === 'mysql') {
if (manifestVersion === 1) {
cmd = 'mysql --user=${MYSQL_USERNAME} --password=${MYSQL_PASSWORD} --host=${MYSQL_HOST} ${MYSQL_DATABASE}';
} else {
cmd = 'mysql --user=${CLOUDRON_MYSQL_USERNAME} --password=${CLOUDRON_MYSQL_PASSWORD} --host=${CLOUDRON_MYSQL_HOST} ${CLOUDRON_MYSQL_DATABASE}';
}
} else if (addon === 'postgresql') {
if (manifestVersion === 1) {
cmd = 'PGPASSWORD=${POSTGRESQL_PASSWORD} psql -h ${POSTGRESQL_HOST} -p ${POSTGRESQL_PORT} -U ${POSTGRESQL_USERNAME} -d ${POSTGRESQL_DATABASE}';
} else {
cmd = 'PGPASSWORD=${CLOUDRON_POSTGRESQL_PASSWORD} psql -h ${CLOUDRON_POSTGRESQL_HOST} -p ${CLOUDRON_POSTGRESQL_PORT} -U ${CLOUDRON_POSTGRESQL_USERNAME} -d ${CLOUDRON_POSTGRESQL_DATABASE}';
}
} else if (addon === 'mongodb') {
if (manifestVersion === 1) {
cmd = 'mongo -u "${MONGODB_USERNAME}" -p "${MONGODB_PASSWORD}" ${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_DATABASE}';
} else {
cmd = 'mongo -u "${CLOUDRON_MONGODB_USERNAME}" -p "${CLOUDRON_MONGODB_PASSWORD}" ${CLOUDRON_MONGODB_HOST}:${CLOUDRON_MONGODB_PORT}/${CLOUDRON_MONGODB_DATABASE}';
}
} else if (addon === 'redis') {
if (manifestVersion === 1) {
cmd = 'redis-cli -h "${REDIS_HOST}" -p "${REDIS_PORT}" -a "${REDIS_PASSWORD}"';
} else {
cmd = 'redis-cli -h "${CLOUDRON_REDIS_HOST}" -p "${CLOUDRON_REDIS_PORT}" -a "${CLOUDRON_REDIS_PASSWORD}"';
}
} else if (addon === 'scheduler' && extra) {
cmd = extra.command;
}
if (!cmd) return;
@@ -308,29 +266,6 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
$scope.terminal.focus();
};
Client.onReady($scope.populateDropdown);
// Client.onApps(function () {
// console.log('onapps')
// if ($scope.$$destroyed) return;
// if ($scope.selected.type !== 'app') return $scope.appBusy = false;
// var appId = $scope.selected.value;
// Client.getApp(appId, function (error, result) {
// if (error) return console.error(error);
// // we expect this to be called _after_ a reconfigure was issued
// if (result.installationState === 'pending_configure') {
// $scope.appBusy = true;
// } else if (result.installationState === 'installed') {
// $scope.appBusy = false;
// }
// $scope.selectedAppInfo = result;
// });
// });
// terminal right click handling
$scope.terminalClear = function () {
if (!$scope.terminal) return;
@@ -348,7 +283,7 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
$scope.terminal.focus();
};
$('.contextMenuBackdrop').on('click', function (e) {
$('.contextMenuBackdrop').on('click', function () {
$('#terminalContextMenu').hide();
$('.contextMenuBackdrop').hide();
@@ -370,8 +305,8 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
return false;
});
window.addEventListener('resize', function (e) {
if ($scope.terminal) $scope.terminal.fit();
window.addEventListener('resize', function () {
if ($scope.fitAddon) $scope.fitAddon.fit();
});
Client.getStatus(function (error, status) {
@@ -401,21 +336,14 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
Client.refreshConfig(function (error) {
if (error) return $scope.error(error);
Client.refreshInstalledApps(function (error) {
if (error) return $scope.error(error);
Client.getInstalledApps().forEach(function (app) {
$scope.apps.push({
type: 'app',
value: app.id,
name: app.fqdn + ' (' + app.manifest.title + ')',
addons: app.manifest.addons
});
});
// activate pre-selected log from query otherwise choose the first one
$scope.selected = $scope.apps.find(function (e) { return e.value === search.id; });
if (!$scope.selected) $scope.selected = $scope.apps[0];
refreshApp(search.id, function (error, app) {
$scope.selected = {
type: 'app',
value: app.id,
name: app.fqdn + ' (' + app.manifest.title + ')',
addons: app.manifest.addons,
manifest: app.manifest
};
// now mark the Client to be ready
Client.setReady();

View File

@@ -1,69 +0,0 @@
'use strict';
// create main application module
var app = angular.module('Application', []);
app.controller('Controller', ['$scope', '$http', '$interval', function ($scope, $http, $interval) {
$scope.title = '';
$scope.percent = 0;
$scope.message = '';
$scope.error = false;
$scope.loadWebadmin = function () {
window.location.href = '/';
};
function fetchProgress() {
$http.get('/api/v1/cloudron/progress').success(function(data, status) {
if (status === 404) return; // just wait until we create the progress.json on the server side
if (status !== 200 || typeof data !== 'object') return console.error('Invalid response for progress', status, data);
if (!data.update && !data.migrate) return $scope.loadWebadmin();
if (data.update) {
if (data.update.percent >= 100) {
return $scope.loadWebadmin();
} else if (data.update.percent === -1) {
$scope.title = 'Update Error';
$scope.error = true;
$scope.message = data.update.message;
} else {
if (data.backup && data.backup.percent < 100) {
$scope.title = 'Backup in progress...';
$scope.percent = data.backup.percent < 0 ? 5 : (data.backup.percent / 100) * 50; // never show 0 as it looks like nothing happens
$scope.message = data.backup.message;
} else {
$scope.title = 'Update in progress...';
$scope.percent = 50 + ((data.update.percent / 100) * 50); // first half is backup
$scope.message = data.update.message;
}
}
} else { // migrating
if (data.migrate.percent === -1) {
$scope.title = 'Migration Error';
$scope.error = true;
$scope.message = data.migrate.message;
} else {
$scope.title = 'Migration in progress...';
$scope.percent = data.migrate.percent;
$scope.message = data.migrate.message;
if (!data.migrate.info) return;
// check if the new domain is available via the appstore (cannot use cloudron
// directly as we might hit NXDOMAIN)
$http.get(data.apiServerOrigin + '/api/v1/boxes/' + data.migrate.info.domain + '/status').success(function(data2, status) {
if (status === 200 && data2.status === 'ready') {
window.location = 'https://my.' + data.migrate.info.domain;
}
});
}
}
}).error(function (data, status) {
console.error('Error getting progress', status, data);
});
}
$interval(fetchProgress, 2000);
fetchProgress();
}]);

193
src/login.html Normal file
View File

@@ -0,0 +1,193 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
<meta http-equiv="Content-Security-Policy" content="default-src <%= apiOrigin %> 'unsafe-inline' 'unsafe-eval' 'self'; img-src <%= apiOrigin %> 'self'" />
<!-- this gets changed once we get the status (because angular has not loaded yet, we see template string for a flash) -->
<title>&lrm;</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">
<!-- 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>
<!-- Showdown (markdown converter) -->
<script type="text/javascript" src="/3rdparty/js/showdown-1.9.1.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-md5.min.js"></script> -->
<!-- <script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script> -->
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
<!-- Setup Application -->
<script type="text/javascript" src="/js/login.js"></script>
</head>
<body ng-app="Application" ng-controller="LoginController">
<div class="layout-root" ng-show="initialized">
<div class="layout-content" ng-show="mode === 'login'">
<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/>
<h1><small>Login to</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>
</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>
<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>
</div>
<div class="form-group">
<label class="control-label" for="inputPassword">2FA Token (if enabled)</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>
</form>
<a ng-href="" class="hand" ng-click="showPasswordReset()">Reset password</a>
</div>
</div>
</div>
</div>
<div class="layout-content" ng-show="mode === 'passwordReset'">
<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>Password reset</h2>
</div>
</div>
<br/>
<div class="row">
<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>
<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>
</form>
<a ng-href="" class="hand" ng-click="showLogin()">Back to login</a>
</div>
</div>
</div>
</div>
<div class="layout-content" ng-show="mode === 'passwordResetDone'">
<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>Password reset email sent</h2>
<br/>
<button class="btn btn-primary" ng-click="showLogin()">Back to login</button>
</div>
</div>
</div>
</div>
<div class="layout-content" ng-show="mode === 'newPassword'">
<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>Set new password</h2>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<h4 class="has-error" ng-show="error">{{ error }}</h4>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<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>
<div class="control-label" ng-show="newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid">
<small ng-show="newPasswordForm.newPassword.$dirty && newPasswordForm.newPassword.$invalid">Password must be at least 8 and at most 265 characters</small>
</div>
<input type="password" class="form-control" id="inputNewPassword" ng-model="newPassword" name="newPassword" ng-minlength="8" ng-maxlength="256" autofocus required>
</div>
<div class="form-group" ng-class="{ 'has-error': newPasswordForm.newPasswordRepeat.$dirty && (newPassword !== newPasswordRepeat) }">
<label class="control-label" for="inputNewPasswordRepeat">Repeat Password</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>
</div>
<input type="password" class="form-control" id="inputNewPasswordRepeat" ng-model="newPasswordRepeat" name="newPasswordRepeat" required>
</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>
</form>
<a ng-href="" class="hand" ng-click="showLogin()">Back to login</a>
</div>
</div>
</div>
</div>
<div class="layout-content" ng-show="mode === 'newPasswordDone'">
<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>Password changed</h2>
<br/>
<a href="/" class="btn btn-primary">Open Dashboard</a>
</div>
</div>
</div>
</div>
<footer class="text-center ng-cloak">
<span class="text-muted" ng-bind-html="status.footer | markdown2html"></span>
</footer>
</div>
</body>
</html>

View File

@@ -1,34 +0,0 @@
<html>
<head>
<title> Cloudron OAuth Callback </title>
<script>
'use strict';
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
if (!search.token) {
console.error('No token found');
} else if (!search.state || !window.localStorage.oauth2State || search.state !== window.localStorage.oauth2State ) {
console.error('OAuth2 state error');
} else {
// the actual app picks up the access token from localStorage
localStorage.token = search.token;
// clear oauth2 state
delete window.localStorage.oauth2State;
var returnTo = window.localStorage.returnTo;
delete window.localStorage.returnTo;
if (returnTo) window.location.href = returnTo;
else window.location.href = '/';
}
</script>
</head>
<body>
</body>
</html>

View File

@@ -1,68 +1,76 @@
<!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 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.min.css"/>
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- CSS -->
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css"/>
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- Custom Fonts -->
<link type="text/css" rel="stylesheet" href="/3rdparty/css/font-awesome.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"></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"></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.min.js"></script>
<!-- Bootstrap Core JavaScript -->
<script type="text/javascript" src="/3rdparty/js/bootstrap.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>
<!-- 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>
<!-- 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"></script>
<!-- moment -->
<script type="text/javascript" src="/3rdparty/js/moment.min.js"></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/logs.js"></script>
<!-- colors -->
<script type="text/javascript" src="/3rdparty/js/colors.js"></script>
<!-- moment -->
<script type="text/javascript" src="/3rdparty/js/moment.min.js"></script>
<!-- Main Application -->
<script type="text/javascript" src="/js/logs.js"></script>
</head>
<body class="logs">
<div class="animateMe ng-hide layout-root" ng-show="initialized">
<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="logs-controls">
<h3 style="display: inline-block;">{{ selected.name }} Logs</h3>
<!-- logs actions -->
<div class="pull-right">
<a class="btn btn-primary" ng-click="showTerminal()" 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=800"><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</a>
<a class="btn btn-primary" ng-href="{{ '/filemanager.html?appId=' + selected.value }}" target="_blank" ng-show="selected.type === 'app'"><i class="fas fa-folder"></i> File Manager</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>
</body>
</html>

View File

@@ -11,12 +11,15 @@
<!-- Theme CSS -->
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- Custom Fonts -->
<link type="text/css" rel="stylesheet" href="/3rdparty/css/font-awesome.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>
@@ -24,12 +27,18 @@
<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.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
<!-- Angular directives for tldjs -->
<script type="text/javascript" src="/3rdparty/js/tld.js"></script>
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
<!-- 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>
@@ -37,11 +46,13 @@
<body class="setup" ng-app="Application" ng-controller="RestoreController">
<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-o-notch fa-spin fa-5x"></i><br/>
<h3>Downloading backup</h3>
<i class="fa fa-circle-notch fa-spin fa-5x"></i><br/>
<h3>{{ message }} ...</h3>
</div>
</div>
</div>
@@ -55,16 +66,23 @@
<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/" 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>
@@ -75,9 +93,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 === '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 === 'backblaze-b2' || provider === 's3-v4-compat'">
</div>
<div class="checkbox" ng-show="provider === 'minio' || provider === 's3-v4-compat'" >
@@ -103,11 +121,41 @@
<select class="form-control" name="region" id="inputConfigureBackupS3Region" ng-model="region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="busy" ng-required="provider === 's3'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 's3-v4-compat'">
<label class="control-label" for="inputConfigureBackupS3V4CompatRegion">Region</label>
<input class="form-control" type="text" name="region" id="inputConfigureBackupS3V4CompatRegion" ng-model="region" ng-disabled="busy" placeholder="Leave empty to use us-east-1 as default"></input>
</div>
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'digitalocean-spaces'">
<label class="control-label" for="inputConfigureBackupDORegion">Region</label>
<select class="form-control" name="region" id="inputConfigureBackupDORegion" ng-model="endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="busy" ng-required="provider === 'digitalocean-spaces'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'exoscale-sos'">
<label class="control-label" for="inputConfigureBackupExoscaleRegion">Region</label>
<select class="form-control" name="region" id="inputConfigureBackupExoscaleRegion" ng-model="endpoint" ng-options="a.value as a.name for a in exoscaleSosRegions" ng-disabled="busy" ng-required="provider === 'exoscale-sos'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'wasabi'">
<label class="control-label" for="inputConfigureBackupWasabiRegion">Region</label>
<select class="form-control" name="region" id="inputConfigureBackupWasabiRegion" ng-model="endpoint" ng-options="a.value as a.name for a in wasabiRegions" ng-disabled="busy" ng-required="provider === 'wasabi'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'scaleway-objectstorage'">
<label class="control-label" for="inputConfigureBackupScalewayRegion">Region</label>
<select class="form-control" name="region" id="inputConfigureBackupScalewayRegion" ng-model="endpoint" ng-options="a.value as a.name for a in scalewayRegions" ng-disabled="busy" ng-required="provider === 'scaleway-objectstorage'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'linode-objectstorage'">
<label class="control-label" for="inputConfigureBackupLinodeRegion">Region</label>
<select class="form-control" name="region" id="inputConfigureBackupLinodeRegion" ng-model="endpoint" ng-options="a.value as a.name for a in linodeRegions" ng-disabled="busy" ng-required="provider === 'linode-objectstorage'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': error.region }" ng-show="provider === 'ovh-objectstorage'">
<label class="control-label" for="inputConfigureBackupOvhRegion">Region</label>
<select class="form-control" name="region" id="inputConfigureBackupOvhRegion" ng-model="endpoint" ng-options="a.value as a.name for a in ovhRegions" ng-disabled="busy" ng-required="provider === 'ovh-objectstorage'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': error.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)">
@@ -135,25 +183,52 @@
<select class="form-control" id="storageFormat" ng-change="key = ''" ng-model="format" ng-options="a.value as a.name for a in formats"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': error.key }" ng-show="provider !== 'noop' && format === 'tgz'">
<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">
</div>
<div class="form-group" ng-class="{ 'has-error': error.backupId }">
<label class="control-label" for="inputConfigureBackupId">Backup ID</label>
<input type="text" class="form-control" ng-model="backupId" name="inputConfigureBackupId" placeholder="Backup Id" required ng-disabled="busy">
</div>
<div class="form-group" ng-class="{ 'has-error': error.key }" ng-show="provider !== 'noop'">
<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>
<input class="ng-hide" type="submit" ng-disabled="configureBackupForm.$invalid"/>
<div uib-collapse="!advancedVisible">
<div class="form-group">
<label class="control-label">IP Configuration <sup><a ng-href="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<select class="form-control" ng-model="sysinfo.provider" ng-options="a.value as a.name for a in sysinfoProvider"></select>
</div>
<!-- Fixed -->
<div class="form-group" ng-show="sysinfo.provider === 'fixed'" ng-class="{ 'has-error': error.ip }">
<label class="control-label">IP Address</label>
<input type="text" class="form-control" ng-model="sysinfo.ip" name="ip" ng-disabled="sysinfo.busy" ng-required="sysinfo.provider === 'fixed'">
<p class="has-error" ng-show="error.ip">{{ error.ip }}</p>
</div>
<!-- Network Interface -->
<div class="form-group" ng-show="sysinfo.provider === 'network-interface'" ng-class="{ 'has-error': error.ifname }">
<label class="control-label">Interface Name</label>
<input type="text" class="form-control" ng-model="sysinfo.ifname" name="ifname" ng-disabled="sysinfo.busy" ng-required="sysinfo.provider === 'network-interface'">
<p class="has-error" ng-show="error.ifname">{{ error.ifname }}</p>
</div>
</div>
<div class="text-center">
<a href="" ng-click="advancedVisible = true" ng-hide="advancedVisible">Advanced settings...</a>
<a href="" ng-click="advancedVisible = false" ng-show="advancedVisible">Hide Advanced settings</a>
</div>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12 text-center">
<button type="submit" class="btn btn-primary" ng-disabled="configureBackupForm.$invalid"/><i class="fa fa-circle-o-notch fa-spin" ng-show="busy"></i> Restore</button>
<button type="submit" class="btn btn-primary" ng-disabled="configureBackupForm.$invalid"/><i class="fa fa-circle-notch fa-spin" ng-show="busy"></i> Restore</button>
</div>
</div>
@@ -164,8 +239,7 @@
</div>
<footer class="text-center">
<span class="text-muted">&copy;2018 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted">&copy;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>

View File

@@ -4,19 +4,22 @@
<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/css/font-awesome.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>
@@ -24,12 +27,15 @@
<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.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
<!-- 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,82 +43,104 @@
<body class="setup" ng-app="Application" ng-controller="SetupController">
<div class="main-container ng-cloak text-center" ng-show="provider === 'caas' && !setupToken">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h1> <i class="fa fa-frown-o fa-fw text-danger"></i> No setup token provided. </h1>
Please use the setup link for this cloudron.
</div>
</div>
</div>
<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-o-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>Setup Admin Account</h3>
</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 href="https://cloudron.io/documentation/installation/#administrator-setup" class="help" target="_blank"><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-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="3" 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,30}$/" 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>
<br/>
<div class="row">
<div class="col-md-12 text-center"><small>Looking to <a href="/restore.html">restore?</a></small></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">
<small><span ng-show="ownerForm.password.$dirty && ownerForm.password.$invalid">Password must be at least 8 characters</span> &nbsp;</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">&copy;2018 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></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>
&nbsp; &nbsp; 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 on 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 &amp; 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">&copy;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>
</body>
</html>

143
src/setupaccount.html Normal file
View File

@@ -0,0 +1,143 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
<meta http-equiv="Content-Security-Policy" content="default-src <%= apiOrigin %> 'unsafe-inline' 'unsafe-eval' 'self'; img-src <%= apiOrigin %> 'self'" />
<title>Cloudron Account Setup</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">
<!-- 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-md5.min.js"></script> -->
<!-- <script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script> -->
<script type="text/javascript" src="/3rdparty/js/autofill-event.js"></script>
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
<script type="text/javascript" src="/3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
<!-- 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/setupaccount.js"></script>
</head>
<body ng-app="Application" ng-controller="SetupAccountController">
<div class="layout-root" ng-show="initialized">
<div class="layout-content" ng-show="view === 'setup'">
<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/>
<h1><small>Welcome to</small> {{ status.cloudronName || 'Cloudron' }}</h1>
<h3>Please setup your account</h3>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<h4 class="has-error" ng-show="error">{{ error.message }}</h4>
</div>
</div>
<div class="row">
<div class="col-md-12">
<form name="setupAccountForm" ng-submit="onSubmit()" autocomplete="off">
<input type="password" style="display: none;">
<div class="form-group" ng-class="{ 'has-error': ((setupAccountForm.username.$dirty && setupAccountForm.username.$invalid) || (!setupAccountForm.username.$dirty && error.username))}">
<label class="control-label">Username</label>
<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>
</div>
<input type="text" class="form-control" ng-model="username" name="username" id="inputUsername" ng-disabled="profileLocked || existingUsername" 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" 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>
<div class="control-label" ng-show="setupAccountForm.password.$dirty && setupAccountForm.password.$invalid">
<small ng-show="setupAccountForm.password.$dirty && setupAccountForm.password.$invalid">Password must be at least 8 characters</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>
<div class="control-label" ng-show="setupAccountForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
<small ng-show="setupAccountForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</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>
</form>
</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>
<br/>
<p>Contact your server admin to get a new invite link.</p>
</div>
</div>
</div>
</div>
<div class="layout-content" ng-show="view === 'done'">
<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>Your Account is ready</h2>
<br/>
<a href="/" class="btn btn-primary">Open Dashboard</a>
</div>
</div>
</div>
</div>
<footer class="text-center ng-cloak">
<span class="text-muted" ng-bind-html="status.footer | markdown2html"></span>
</footer>
</div>
</body>
</html>

View File

@@ -11,12 +11,15 @@
<!-- Theme CSS -->
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- Custom Fonts -->
<link type="text/css" rel="stylesheet" href="/3rdparty/css/font-awesome.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>
@@ -24,15 +27,19 @@
<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.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>
<!-- 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 +47,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-o-notch fa-spin fa-5x"></i><br/>
<h3>Waiting for domain and certificate setup</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}}.
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,32 +74,30 @@
<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>Cloudron Setup</h1>
<h3>Provide a domain for your Cloudron</h3>
<p>Apps will be installed on subdomains of this domain</p>
</div>
<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-8 col-md-offset-2">
<br/>
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.domain.$dirty && dnsCredentialsForm.domain.$invalid }">
<label class="control-label">Primary Domain</label>
<input type="text" class="form-control" ng-model="dnsCredentials.domain" name="domain" placeholder="example.com" required autofocus ng-disabled="dnsCredentials.busy">
</div>
<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>
<br/>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<h3 class="text-center">Domain Configuration</h3>
<p class="has-error text-center" ng-show="dnsCredentials.error">{{ dnsCredentials.error }}</p>
<div class="col-md-10 col-md-offset-1">
<h3 class="text-center">Domain Configuration <sup><a ng-href="https://docs.cloudron.io/domains/#dns-providers" class="help" target="_blank" tabindex="-1"><i class="fa fa-question-circle"></i></a></sup> </h3>
<p class="has-error text-center" ng-show="error.dnsCredentials">{{ error.dnsCredentials }}</p>
<br/>
<div class="form-group">
<label class="control-label">Domain Provider</label>
<select class="form-control" ng-model="dnsCredentials.provider" ng-options="a.value as a.name for a in dnsProvider" ng-disabled="dnsCredentials.busy"></select>
<label class="control-label">DNS Provider</label>
<select class="form-control" ng-model="dnsCredentials.provider" ng-options="a.value as a.name for a in dnsProvider" ng-disabled="dnsCredentials.busy" ng-change="setDefaultTlsProvider()"></select>
</div>
<!-- Route53 -->
@@ -107,19 +124,19 @@
<!-- DigitalOcean -->
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.digitalOceanToken.$dirty && dnsCredentialsForm.digitalOceanToken.$invalid }" ng-show="dnsCredentials.provider === 'digitalocean'">
<label class="control-label">DigitalOcean Token <sup><a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label class="control-label">DigitalOcean Token</label>
<input type="text" class="form-control" ng-model="dnsCredentials.digitalOceanToken" name="digitalOceanToken" placeholder="API Token" ng-required="dnsCredentials.provider === 'digitalocean'" ng-disabled="dnsCredentials.busy">
</div>
<!-- Gandi -->
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.gandiApiKey.$dirty && dnsCredentialsForm.gandiApiKey.$invalid }" ng-show="dnsCredentials.provider === 'gandi'">
<label class="control-label">Gandi API Key <sup><a href="http://doc.livedns.gandi.net/" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label class="control-label">Gandi API Key</label>
<input type="text" class="form-control" ng-model="dnsCredentials.gandiApiKey" name="gandiApiKey" placeholder="API Key" ng-required="dnsCredentials.provider === 'gandi'" ng-disabled="dnsCredentials.busy">
</div>
<!-- GoDaddy -->
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.godaddyApiKey.$dirty && dnsCredentialsForm.godaddyApiKey.$invalid }" ng-show="dnsCredentials.provider === 'godaddy'">
<label class="control-label">API Key <sup><a href="https://developer.godaddy.com/keys" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label class="control-label">API Key</label>
<input type="text" class="form-control" ng-model="dnsCredentials.godaddyApiKey" name="godaddyApiKey" placeholder="API Key" ng-minlength="1" ng-required="dnsCredentials.provider === 'godaddy'" ng-disabled="dnsCredentials.busy">
</div>
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.godaddyApiSecret.$dirty && dnsCredentialsForm.godaddyApiSecret.$invalid }" ng-show="dnsCredentials.provider === 'godaddy'">
@@ -129,12 +146,21 @@
<!-- Cloudflare -->
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareToken.$dirty && dnsCredentialsForm.cloudflareToken.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare'">
<label class="control-label">Global API Key</label>
<input type="text" class="form-control" ng-model="dnsCredentials.cloudflareToken" name="cloudflareToken" placeholder="Global API Key" ng-required="dnsCredentials.provider === 'cloudflare'" ng-disabled="dnsCredentials.busy">
<label class="control-label">Token Type</label>
<select class="form-control" ng-model="dnsCredentials.cloudflareTokenType">
<option value="GlobalApiKey">Global API Key</option>
<option value="ApiToken">API Token</option>
</select>
</div>
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareToken.$dirty && dnsCredentialsForm.cloudflareToken.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare'">
<label class="control-label" ng-show="dnsCredentials.cloudflareTokenType === 'GlobalApiKey'">Global API Key</label>
<label class="control-label" ng-show="dnsCredentials.cloudflareTokenType === 'ApiToken'">Api Token</label>
<input type="text" class="form-control" ng-model="dnsCredentials.cloudflareToken" name="cloudflareToken" placeholder="API Key/Token" ng-required="dnsCredentials.provider === 'cloudflare'" ng-disabled="dnsCredentials.busy">
</div>
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareEmail.$dirty && dnsCredentialsForm.cloudflareEmail.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare'">
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.cloudflareEmail.$dirty && dnsCredentialsForm.cloudflareEmail.$invalid }" ng-show="dnsCredentials.provider === 'cloudflare' && dnsCredentials.cloudflareTokenType === 'GlobalApiKey'">
<label class="control-label">Cloudflare Email</label>
<input type="email" class="form-control" ng-model="dnsCredentials.cloudflareEmail" name="cloudflareEmail" placeholder="Cloudflare Account Email" ng-required="dnsCredentials.provider === 'cloudflare'" ng-disabled="dnsCredentials.busy">
<input type="email" class="form-control" ng-model="dnsCredentials.cloudflareEmail" name="cloudflareEmail" placeholder="Cloudflare Account Email" ng-required="dnsCredentials.provider === 'cloudflare' && dnsCredentials.cloudflareTokenType === 'GlobalApiKey'" ng-disabled="dnsCredentials.busy">
</div>
<!-- Name.com -->
@@ -143,63 +169,97 @@
<input type="text" class="form-control" ng-model="dnsCredentials.nameComUsername" name="nameComUsername" placeholder="Name.com Username" ng-required="dnsCredentials.provider === 'namecom'" ng-disabled="dnsCredentials.busy">
</div>
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.nameComToken.$dirty && dnsCredentialsForm.nameComToken.$invalid }" ng-show="dnsCredentials.provider === 'namecom'">
<label class="control-label">API Token <sup><a href="https://www.name.com/account/settings/api" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<label class="control-label">API Token</label>
<input type="text" class="form-control" ng-model="dnsCredentials.nameComToken" name="nameComToken" placeholder="Name.com API Token" ng-required="dnsCredentials.provider === 'namecom'" ng-disabled="dnsCredentials.busy">
</div>
<!-- Namecheap -->
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.namecheapUsername.$dirty && dnsCredentialsForm.namecheapUsername.$invalid }" ng-show="dnsCredentials.provider === 'namecheap'">
<label class="control-label">Namecheap Username</label>
<input type="text" class="form-control" ng-model="dnsCredentials.namecheapUsername" name="namecheapUsername" placeholder="Namecheap Username" ng-required="dnsCredentials.provider === 'namecheap'" ng-disabled="dnsCredentials.busy">
</div>
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.namecheapApiKey.$dirty && dnsCredentialsForm.namecheapApiKey.$invalid }" ng-show="dnsCredentials.provider === 'namecheap'">
<label class="control-label">API Key</label>
<p class="small text-info" ng-show="dnsCredentials.provider === 'namecheap'"><b>The server IP needs to be whitelisted for this API Key.</b></p>
<input type="text" class="form-control" ng-model="dnsCredentials.namecheapApiKey" name="namecheapApiKey" placeholder="Namecheap API Key" ng-required="dnsCredentials.provider === 'namecheap'" ng-disabled="dnsCredentials.busy">
</div>
<!-- Linode -->
<p class="small text-warning" ng-show="dnsCredentials.provider === 'linode'">
<b>Linode DNS average <a target="_blank" ng-href="https://docs.cloudron.io/domains/#linode-dns">propagation time</a> is 30 minutes. Cloudron setup &amp; installing apps will take a while.</b>
</p>
<!-- Wildcard -->
<p ng-show="dnsCredentials.provider === '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>
</p>
<!-- Manual -->
<p 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>
<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>
</p>
<p class="small text-info" ng-show="needsPort80(dnsCredentials.provider, dnsCredentials.tlsConfig.provider)">Let's Encrypt requires your server to be reachable on port 80</p>
<div ng-show="provider === 'ami'">
<br/>
<h3>Owner verification</h3>
<p>Provide the EC2 instance id to verify you have access to this server.</p>
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.instanceId.$dirty && (dnsCredentialsForm.instanceId.$invalid || error) }">
<input type="text" class="form-control" ng-model="instanceId" id="inputInstanceId" name="instanceId" placeholder="AWS EC2 instance id" ng-maxlength="20" ng-minlength="10" ng-required="provider === 'ami'" autocomplete="off">
<h3 class="text-center">Owner verification</h3>
<p class="has-error text-center" ng-show="error.ami">{{ error.ami }}</p>
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.instanceId.$dirty && (dnsCredentialsForm.instanceId.$invalid || error.ami) }">
<label class="control-label">EC2 Instance Id</label>
<input type="text" class="form-control" ng-model="instanceId" id="inputInstanceId" name="instanceId" placeholder="i-0123456789abcdefg" ng-minlength="1" ng-required="provider === 'ami'" autocomplete="off">
</div>
<p>&nbsp;<span ng-show="error" class="text-danger">{{ error }}</span></p>
<p style="margin-top: 5px; font-size: 13px;">Provide the EC2 instance id to verify you have access to this server.</p>
</div>
<br/>
<div class="text-center">
<a href="" ng-click="dnsCredentials.advancedVisible = true" ng-hide="dnsCredentials.advancedVisible">Advanced settings...</a>
</div>
<div uib-collapse="!dnsCredentials.advancedVisible">
<div uib-collapse="!advancedVisible">
<div class="form-group">
<label class="control-label">Zone Name (Optional)</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</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="https://docs.cloudron.io/networking/#ip-configuration" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<select class="form-control" ng-model="sysinfo.provider" ng-options="a.value as a.name for a in sysinfoProvider"></select>
</div>
<!-- Fixed -->
<div class="form-group" ng-show="sysinfo.provider === 'fixed'" ng-class="{ 'has-error': error.ip }">
<label class="control-label">IP Address</label>
<input type="text" class="form-control" ng-model="sysinfo.ip" name="ip" ng-disabled="sysinfo.busy" ng-required="sysinfo.provider === 'fixed'">
<p class="has-error" ng-show="error.ip">{{ error.ip }}</p>
</div>
<!-- Network Interface -->
<div class="form-group" ng-show="sysinfo.provider === 'network-interface'" ng-class="{ 'has-error': error.ifname }">
<label class="control-label">Interface Name</label>
<input type="text" class="form-control" ng-model="sysinfo.ifname" name="ifname" ng-disabled="sysinfo.busy" ng-required="sysinfo.provider === 'network-interface'">
<p class="has-error" ng-show="error.ifname">{{ error.ifname }}</p>
</div>
</div>
<div class="text-center">
<a href="" ng-click="advancedVisible = true" ng-hide="advancedVisible">Advanced settings...</a>
<a href="" ng-click="advancedVisible = false" ng-show="advancedVisible">Hide Advanced settings</a>
</div>
</div>
</div>
<br/>
<br/>
<div class="row">
<div class="col-md-12 text-center">
<button type="submit" class="btn btn-primary" ng-disabled="dnsCredentialsForm.$invalid"/><i class="fa fa-circle-o-notch fa-spin" ng-show="dnsCredentials.busy"></i> Next</button>
<button type="submit" class="btn btn-primary" ng-disabled="dnsCredentialsForm.$invalid"><i class="fa fa-circle-notch fa-spin" ng-show="dnsCredentials.busy"></i> Next</button>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12 text-center">
<small>You can setup a new Cloudron or restore from a backup in the next step</small>
</div>
<div class="col-md-12 text-center"><small>Looking to <a href="/restore.html">restore?</a></small></div>
</div>
</form>
</div>
@@ -208,8 +268,7 @@
</div>
<footer class="text-center">
<span class="text-muted">&copy;2018 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted">&copy;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>

60
src/splash.html Normal file
View File

@@ -0,0 +1,60 @@
<!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</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>
</head>
<body>
<div class="content">
<p>You found a <a href="https://cloudron.io">Cloudron</a> out in the wild!</p>
</div>
</body>
</html>

View File

@@ -4,22 +4,25 @@
<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>
<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.min.css"/>
<link type="text/css" rel="stylesheet" href="/3rdparty/xterm/xterm.css">
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.css"/>
<link type="text/css" rel="stylesheet" href="/theme.css">
<!-- Custom Fonts -->
<link type="text/css" rel="stylesheet" href="/3rdparty/css/font-awesome.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>
@@ -30,7 +33,7 @@
<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.min.js"></script>
<script type="text/javascript" src="/3rdparty/js/angular-ui-notification.js"></script>
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
@@ -40,9 +43,13 @@
<script type="text/javascript" src="/3rdparty/js/clipboard.min.js"></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>
<link type="text/css" rel="stylesheet" href="/3rdparty/xterm/css/xterm.css" />
<script src="/3rdparty/xterm/lib/xterm.js"></script>
<script src="/3rdparty/xterm-addon-attach/lib/xterm-addon-attach.js"></script>
<script src="/3rdparty/xterm-addon-fit/lib/xterm-addon-fit.js"></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"></script>
@@ -74,7 +81,7 @@
<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-o-notch fa-spin" ng-show="downloadFile.busy"></i> Download</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>
</div>
@@ -88,7 +95,7 @@
<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>
<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>
@@ -99,25 +106,6 @@
</div>
</div>
<!-- Modal repair info -->
<div class="modal fade" id="repairAppModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Repair app {{ selected.name }} ?</h4>
</div>
<div class="modal-body">
<p>This will restart the app in repair mode. The app will start in a paused state and can be used to fix broken plugins or database content.</p>
<p class="text-danger text-bold">The app will not be reachable in repair mode!</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="repairAppBegin()">Repair</button>
</div>
</div>
</div>
</div>
<div class="animateMe ng-hide layout-root terminal-view" ng-show="initialized">
<div class="terminal-controls">
@@ -127,7 +115,7 @@
<div class="pull-right">
<div class="btn-group" ng-show="usesAddon('scheduler')">
<button type="button" class="btn btn-success dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<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">
@@ -136,27 +124,25 @@
</div>
<!-- addon actions -->
<a class="btn btn-success" ng-click="terminalInject('mysql')" ng-show="usesAddon('mysql')">MySQL</a>
<a class="btn btn-success" ng-click="terminalInject('postgresql')" ng-show="usesAddon('postgresql')">Postgres</a>
<a class="btn btn-success" ng-click="terminalInject('mongodb')" ng-show="usesAddon('mongodb')">MongoDB</a>
<a class="btn btn-success" ng-click="terminalInject('redis')" ng-show="usesAddon('redis')">Redis</a>
<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 -->
<a class="btn btn-primary" ng-click="restartApp()" ng-show="selected.type === 'app'" ng-disabled="restartAppBusy"><i class="fa fa-refresh" ng-class="{ 'fa-spin': restartAppBusy }"></i> Restart</a>
<a class="btn btn-primary" ng-click="uploadFile()" ng-show="selected.type === 'app' && !uploadProgress.busy"><i class="fa fa-upload"></i> Upload to /tmp</a>
<a class="btn btn-primary" ng-click="uploadProgress.show()" ng-show="uploadProgress.busy"><i class="fa fa-circle-o-notch fa-spin"></i> Uploading...</a>
<a class="btn btn-primary" ng-click="downloadFile.show()" ng-show="selected.type === 'app'"><i class="fa fa-download"></i> Download</a>
<a class="btn btn-primary" ng-click="repairApp()" ng-show="selected.type === 'app' && !selectedAppInfo.debugMode && !appBusy"><i class="fa fa-wrench"></i> Repair</a>
<a class="btn btn-danger" ng-click="repairAppDone()" ng-show="selectedAppInfo.debugMode && !appBusy"><i class="fa fa-wrench"></i> Repair Done</a>
<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>&nbsp;
<span ng-show="selectedAppInfo.installationState === 'pending_configure' && selectedAppInfo.debugMode">Restarting app for repair...</span>
<span ng-show="selectedAppInfo.installationState === 'pending_configure' && !selectedAppInfo.debugMode ">App is being reconfigured...</span>
<span ng-show="selectedAppInfo.installationState === 'installed' && !selectedAppInfo.debugMode">Waiting for app to start...</span>
<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>

File diff suppressed because it is too large Load Diff

View File

@@ -1,68 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height"/>
<title> Cloudron Update </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/css/font-awesome.min.css">
<!-- jQuery-->
<script type="text/javascript" src="/3rdparty/js/jquery.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>
<!-- Update Application -->
<script type="text/javascript" src="/js/update.js"></script>
</head>
<body ng-app="Application" ng-controller="Controller" style="background-color: #7F7F7F">
<div class="modal show" id="updateProgressModal" tabindex="-1" role="dialog" aria-labelledby="updateProgressModalLabel" aria-hidden="true" data-keyboard="false" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{title}}</h4>
</div>
<div class="modal-body" ng-show="!error">
<div class="progress progress-striped active">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{percent}}%"></div>
</div>
<span>{{message}}</span>
</div>
<div class="modal-body" ng-show="error">
<span>{{message}}</span>
</div>
<div class="modal-footer" ng-show="error">
<button type="button" class="btn btn-primary" ng-click="loadWebadmin()">OK</button>
</div>
</div>
</div>
</div>
<div class="layout-root">
<div class="layout-content"></div>
<footer class="text-center">
<span class="text-muted">&copy;2018 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></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>
</body>
</html>

View File

@@ -1,350 +0,0 @@
'use strict';
/* global asyncForEach:false */
angular.module('Application').controller('AccountController', ['$scope', 'Client', function ($scope, Client) {
$scope.user = Client.getUserInfo();
$scope.config = Client.getConfig();
$scope.activeTokens = 0;
$scope.activeClients = [];
$scope.webadminClient = {};
$scope.twoFactorAuthentication = {
busy: false,
error: null,
password: '',
totpToken: '',
secret: '',
qrcode: '',
reset: function () {
$scope.twoFactorAuthentication.busy = false;
$scope.twoFactorAuthentication.error = null;
$scope.twoFactorAuthentication.password = '';
$scope.twoFactorAuthentication.totpToken = '';
$scope.twoFactorAuthentication.secret = '';
$scope.twoFactorAuthentication.qrcode = '';
$scope.twoFactorAuthenticationEnableForm.$setUntouched();
$scope.twoFactorAuthenticationEnableForm.$setPristine();
$scope.twoFactorAuthenticationDisableForm.$setUntouched();
$scope.twoFactorAuthenticationDisableForm.$setPristine();
},
show: function () {
$scope.twoFactorAuthentication.reset();
if ($scope.user.twoFactorAuthenticationEnabled) {
$('#twoFactorAuthenticationDisableModal').modal('show');
} 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;
});
}
},
enable: function() {
$scope.twoFactorAuthentication.busy = true;
Client.enableTwoFactorAuthentication($scope.twoFactorAuthentication.totpToken, function (error) {
$scope.twoFactorAuthentication.busy = false;
if (error) {
$scope.twoFactorAuthentication.error = error.message;
$scope.twoFactorAuthentication.totpToken = '';
$scope.twoFactorAuthenticationEnableForm.totpToken.$setPristine();
$('#twoFactorAuthenticationTotpTokenInput').focus();
return;
}
Client.refreshUserInfo();
$('#twoFactorAuthenticationEnableModal').modal('hide');
});
},
disable: function () {
$scope.twoFactorAuthentication.busy = true;
Client.disableTwoFactorAuthentication($scope.twoFactorAuthentication.password, function (error) {
$scope.twoFactorAuthentication.busy = false;
if (error) {
$scope.twoFactorAuthentication.error = error.message;
$scope.twoFactorAuthentication.password = '';
$scope.twoFactorAuthenticationDisableForm.password.$setPristine();
$('#twoFactorAuthenticationPasswordInput').focus();
return;
}
Client.refreshUserInfo();
$('#twoFactorAuthenticationDisableModal').modal('hide');
});
}
};
$scope.passwordchange = {
busy: false,
error: {},
password: '',
newPassword: '',
newPasswordRepeat: '',
reset: function () {
$scope.passwordchange.error.password = null;
$scope.passwordchange.error.newPassword = null;
$scope.passwordchange.error.newPasswordRepeat = null;
$scope.passwordchange.password = '';
$scope.passwordchange.newPassword = '';
$scope.passwordchange.newPasswordRepeat = '';
$scope.passwordChangeForm.$setUntouched();
$scope.passwordChangeForm.$setPristine();
},
show: function () {
$scope.passwordchange.reset();
$('#passwordChangeModal').modal('show');
},
submit: function () {
$scope.passwordchange.error.password = null;
$scope.passwordchange.error.newPassword = null;
$scope.passwordchange.error.newPasswordRepeat = null;
$scope.passwordchange.busy = true;
Client.changePassword($scope.passwordchange.password, $scope.passwordchange.newPassword, function (error) {
$scope.passwordchange.busy = false;
if (error) {
if (error.statusCode === 403) {
$scope.passwordchange.error.password = true;
$scope.passwordchange.password = '';
$('#inputPasswordChangePassword').focus();
$scope.passwordChangeForm.password.$setPristine();
} else if (error.statusCode === 400) {
$scope.passwordchange.error.newPassword = error.message;
$scope.passwordchange.newPassword = '';
$scope.passwordchange.newPasswordRepeat = '';
$scope.passwordChangeForm.newPassword.$setPristine();
$scope.passwordChangeForm.newPasswordRepeat.$setPristine();
$('#inputPasswordChangeNewPassword').focus();
} else {
console.error('Unable to change password.', error);
}
return;
}
$scope.passwordchange.reset();
$('#passwordChangeModal').modal('hide');
});
}
};
$scope.emailchange = {
busy: false,
error: {},
email: '',
reset: function () {
$scope.emailchange.busy = false;
$scope.emailchange.error.email = null;
$scope.emailchange.email = '';
$scope.emailChangeForm.$setUntouched();
$scope.emailChangeForm.$setPristine();
},
show: function () {
$scope.emailchange.reset();
$('#emailChangeModal').modal('show');
},
submit: function () {
$scope.emailchange.error.email = null;
$scope.emailchange.busy = true;
var data = {
email: $scope.emailchange.email
};
Client.updateProfile(data, function (error) {
$scope.emailchange.busy = false;
if (error) {
if (error.statusCode === 409) {
$scope.emailchange.error.email = 'Email already taken';
$scope.emailChangeForm.email.$setPristine();
$('#inputEmailChangeEmail').focus();
} else {
console.error('Unable to change email.', error);
}
return;
}
// update user info in the background
Client.refreshUserInfo();
$scope.emailchange.reset();
$('#emailChangeModal').modal('hide');
});
}
};
$scope.fallbackEmailChange = {
busy: false,
error: {},
email: '',
reset: function () {
$scope.fallbackEmailChange.busy = false;
$scope.fallbackEmailChange.error.email = null;
$scope.fallbackEmailChange.email = '';
$scope.fallbackEmailChangeForm.$setUntouched();
$scope.fallbackEmailChangeForm.$setPristine();
},
show: function () {
$scope.fallbackEmailChange.reset();
$('#fallbackEmailChangeModal').modal('show');
},
submit: function () {
$scope.fallbackEmailChange.error.email = null;
$scope.fallbackEmailChange.busy = true;
var data = {
fallbackEmail: $scope.fallbackEmailChange.email
};
Client.updateProfile(data, function (error) {
$scope.fallbackEmailChange.busy = false;
if (error) return console.error('Unable to change fallback email.', error);
// update user info in the background
Client.refreshUserInfo();
$scope.fallbackEmailChange.reset();
$('#fallbackEmailChangeModal').modal('hide');
});
}
};
$scope.displayNameChange = {
busy: false,
error: {},
displayName: '',
reset: function () {
$scope.displayNameChange.busy = false;
$scope.displayNameChange.error.displayName = null;
$scope.displayNameChange.displayName = '';
$scope.displayNameChangeForm.$setUntouched();
$scope.displayNameChangeForm.$setPristine();
},
show: function () {
$scope.displayNameChange.reset();
$scope.displayNameChange.displayName = $scope.user.displayName;
$('#displayNameChangeModal').modal('show');
},
submit: function () {
$scope.displayNameChange.error.displayName = null;
$scope.displayNameChange.busy = true;
var user = {
displayName: $scope.displayNameChange.displayName
};
Client.updateProfile(user, function (error) {
$scope.displayNameChange.busy = false;
if (error) {
if (error.statusCode === 400) {
$scope.displayNameChange.error.displayName = 'Invalid display name';
$scope.displayNameChangeForm.email.$setPristine();
$('#inputDisplayNameChangeDisplayName').focus();
} else {
console.error('Unable to change email.', error);
}
return;
}
// update user info in the background
Client.refreshUserInfo();
$scope.displayNameChange.reset();
$('#displayNameChangeModal').modal('hide');
});
}
};
function revokeTokensByClient(client, callback) {
Client.delTokensByClientId(client.id, function (error) {
if (error) console.error(error);
callback();
});
}
$scope.revokeTokens = function () {
asyncForEach($scope.activeClients, revokeTokensByClient, function () {
// now kill this session if exists
if (!$scope.webadminClient || !$scope.webadminClient.id) return;
revokeTokensByClient($scope.webadminClient, function () {
// we should be logged out by now
});
});
};
function refreshClientTokens(client, callback) {
Client.getTokensByClientId(client.id, function (error, result) {
if (error) console.error(error);
client.activeTokens = result || [];
callback();
});
}
Client.onReady(function () {
Client.getOAuthClients(function (error, activeClients) {
if (error) return console.error(error);
asyncForEach(activeClients, refreshClientTokens, function () {
activeClients = activeClients.filter(function (c) { return c.activeTokens.length > 0; });
$scope.activeClients = activeClients.filter(function (c) { return c.id !== 'cid-sdk' && c.id !== 'cid-webadmin'; });
$scope.webadminClient = activeClients.filter(function (c) { return c.id === 'cid-webadmin'; })[0];
$scope.activeTokenCount = $scope.activeClients.reduce(function (prev, cur) { return prev + cur.activeTokens.length; }, 0);
$scope.activeTokenCount += $scope.webadminClient ? $scope.webadminClient.activeTokens.length : 0;
});
});
});
// setup all the dialog focus handling
['passwordChangeModal', 'emailChangeModal', 'fallbackEmailChangeModal', 'displayNameChangeModal', 'twoFactorAuthenticationEnableModal', 'twoFactorAuthenticationDisableModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find("[autofocus]:first").focus();
});
});
$('.modal-backdrop').remove();
}]);

View File

@@ -1,15 +1,15 @@
<div>
<div class="col-md-10 col-md-offset-1">
<h1>Activity Log</h1>
<h1>Event Log</h1>
</div>
</div>
<div>
<div class="col-md-10 col-md-offset-1">
<div class="filter">
<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="Search"/>
<multiselect ng-model="selectedActions" ms-header="All Events" options="a.name for a in actions" data-multiple="true" ng-change="updateFilter(true)"></multiselect>
<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>
<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>
@@ -17,34 +17,36 @@
</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 > eventLogs.length">next <i class="fa fa-angle-double-right"></i></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>
</div>
</div>
</div>
<div>
<div class="col-md-10 col-md-offset-1">
<div class="card card-block" style="max-width: 100%">
<center ng-show="busy"><h2><i class="fa fa-circle-o-notch fa-spin"></i></h2></center>
<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>
</tr>
</thead>
<tbody ng-repeat="eventLog in eventLogs">
<tr ng-click="showEventLogDetails(eventLog)" class="hand">
<td><span uib-tooltip="{{ eventLog.creationTime | prettyLongDate }}" class="arrow">{{ eventLog.creationTime | prettyDate }}</span></td>
<td>{{ eventLog | eventLogSource }}</td>
<td ng-bind-html="eventLog | eventLogDetails"></td>
</tr>
<tr ng-show="activeEventLog === eventLog">
<td colspan="4"><pre class="eventlog-details">{{ eventLog.data | json }}</pre></td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-10 col-md-offset-1">
<div class="card card-block" style="max-width: 100%">
<div>
<center ng-show="busy"><h2><i class="fa fa-circle-notch fa-spin"></i></h2></center>
<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>
</tr>
</thead>
<tbody ng-repeat="eventLog in eventLogs">
<tr ng-click="showEventLogDetails(eventLog)" class="hand">
<td><span uib-tooltip="{{ eventLog.raw.creationTime | prettyLongDate }}" class="arrow">{{ eventLog.raw.creationTime | prettyDate }}</span></td>
<td>{{ eventLog.source }}</td>
<td ng-bind-html="eventLog.details"></td>
</tr>
<tr ng-show="activeEventLog === eventLog">
<td colspan="4"><pre class="eventlog-details">{{ eventLog.raw.data | json }}</pre></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>

View File

@@ -1,7 +1,10 @@
'use strict';
/* global angular:false */
/* global $:false */
angular.module('Application').controller('ActivityController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
Client.onReady(function () { if (!Client.hasScope('cloudron')) $location.path('/'); });
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
$scope.config = Client.getConfig();
@@ -18,19 +21,48 @@ angular.module('Application').controller('ActivityController', ['$scope', '$loca
{ 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: 'backup.cleanup', value: 'backup.cleanup' },
{ 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: 'settings.climode', value: 'settings.climode' },
{ 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: '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.update', value: 'user.update' }
{ name: 'user.transfer', value: 'user.transfer' },
{ name: 'user.update', value: 'user.update' },
{ name: 'System Crash', value: 'system.crash' }
];
$scope.pageItemCount = [
@@ -45,6 +77,337 @@ angular.module('Application').controller('ActivityController', ['$scope', '$loca
$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_APP_START = 'app.start';
var ACTION_APP_STOP = 'app.stop';
var ACTION_APP_RESTART = 'app.restart';
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_LOCATION = 'mail.location';
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, app;
function appName(app) {
return (app.label || app.fqdn || app.location) + ' (' + app.manifest.title + ')';
}
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 '';
app = data.app;
var q = function (x) {
return '"' + x + '"';
};
if ('accessRestriction' in data) { // since it can be null
return 'Access restriction of ' + appName(app) + ' was changed';
} else if (data.label) {
return 'Label of ' + appName(app) + ' was set to ' + q(data.label);
} else if (data.tags) {
return 'Tags of ' + appName(app) + ' was set to ' + q(data.tags.join(','));
} else if (data.icon) {
return 'Icon of ' + appName(app) + ' was changed';
} else if (data.memoryLimit) {
return 'Memory limit of ' + appName(app) + ' was set to ' + data.memoryLimit;
} else if (data.cpuShares) {
return 'CPU shares of ' + appName(app) + ' was set to ' + Math.round((data.cpuShares * 100)/1024) + '%';
} else if (data.env) {
return 'Env vars of ' + appName(app) + ' was changed';
} else if ('debugMode' in data) { // since it can be null
if (data.debugMode) {
return appName(app) + ' was placed in repair mode';
} else {
return appName(app) + ' was taken out of repair mode';
}
} else if ('enableBackup' in data) {
return 'Automatic backups of ' + appName(app) + ' was ' + (data.enableBackup ? 'enabled' : 'disabled');
} else if ('enableAutomaticUpdate' in data) {
return 'Automatic updates of ' + appName(app) + ' was ' + (data.enableAutomaticUpdate ? 'enabled' : 'disabled');
} else if ('reverseProxyConfig' in data) {
return 'Reverse proxy configuration of ' + appName(app) + ' was updated';
} else if ('cert' in data) {
if (data.cert) {
return 'Custom certificate was set for ' + appName(app);
} else {
return 'Certificate of ' + appName(app) + ' was reset';
}
} else if (data.location) {
if (data.fqdn !== data.app.fqdn) {
return 'Location of ' + appName(app) + ' 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 ' + appName(app) + ' was ' + (altFqdns.length ? 'set to ' + altFqdns.join(', ') : 'reset');
} else if (!angular.equals(data.portBindings, data.app.portBindings)) {
return 'Port bindings of ' + appName(app) + ' was changed';
}
} else if ('dataDir' in data) {
if (data.dataDir) {
return 'Data directory of ' + appName(app) + ' was set ' + data.dataDir;
} else {
return 'Data directory of ' + appName(app) + ' was reset';
}
} else if ('icon' in data) {
if (data.icon) {
return 'Icon of ' + appName(app) + ' was set';
} else {
return 'Icon of ' + appName(app) + ' was reset';
}
} else if (('mailboxName' in data) && data.mailboxName !== data.app.mailboxName) {
if (data.mailboxName) {
return 'Mailbox of ' + appName(app) + ' was set to ' + q(data.mailboxName);
} else {
return 'Mailbox of ' + appName(app) + ' was reset';
}
}
return appName(app) + ' was re-configured';
}
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 ' + appName(data.app) + ' was re-configured'; // re-configure of email apps is more common?
case ACTION_APP_LOGIN: {
app = Client.getCachedAppSync(data.appId);
if (!app) return '';
return 'App ' + app.fqdn + ' logged in';
}
case ACTION_APP_OOM:
if (!data.app) return '';
return appName(data.app) + ' ran out of memory';
case ACTION_APP_DOWN:
if (!data.app) return '';
return appName(data.app) + ' is down';
case ACTION_APP_UP:
if (!data.app) return '';
return appName(data.app) + ' is back online';
case ACTION_APP_START:
if (!data.app) return '';
return appName(data.app) + ' was started';
case ACTION_APP_STOP:
if (!data.app) return '';
return appName(data.app) + ' was stopped';
case ACTION_APP_RESTART:
if (!data.app) return '';
return appName(data.app) + ' was restarted';
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 ? data.removedBoxBackups.length : '0') + ' 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_LOCATION:
return 'Mail server location was changed to ' + data.subdomain + (data.subdomain ? '.' : '') + data.domain;
case ACTION_MAIL_ENABLED:
return 'Mail was enabled for domain ' + data.domain;
case ACTION_MAIL_DISABLED:
return '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:
if (data.errorMessage) {
return 'Cloudron update errored. Error: ' + data.errorMessage;
} else {
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(', ');
@@ -54,7 +417,10 @@ angular.module('Application').controller('ActivityController', ['$scope', '$loca
if (error) return console.error(error);
$scope.eventLogs = result;
$scope.eventLogs = [];
result.forEach(function (e) {
$scope.eventLogs.push({ raw: e, details: eventLogDetails(e), source: eventLogSource(e) });
});
});
}

1164
src/views/app.html Normal file

File diff suppressed because it is too large Load Diff

1623
src/views/app.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,398 +1,91 @@
<!-- Modal configure/repair app -->
<div class="modal fade" id="appConfigureModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" ng-show="(appConfigure.app | installError)">Repair {{ appConfigure.app.fqdn }}</h4>
<h4 class="modal-title" ng-hide="(appConfigure.app | installError)">Configure {{ appConfigure.app.fqdn }}</h4>
</div>
<div class="modal-body" style="padding: 0 15px">
<fieldset>
<form role="form" name="appConfigureForm" ng-submit="appConfigure.submit()" autocomplete="off">
<uib-tabset>
<uib-tab index="0" heading="General">
<br/>
<div class="has-error text-center" ng-show="appConfigure.error.other">{{ appConfigure.error.other }}</div>
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.location.$dirty && appConfigureForm.location.$invalid) || (!appConfigureForm.location.$dirty && appConfigure.error.location) }">
<label class="control-label" for="appConfigureLocationInput">Location {{ appConfigure.error.location }} </label>
<div class="input-group form-inline">
<input type="text" class="form-control" ng-model="appConfigure.location" id="appConfigureLocationInput" name="location" placeholder="{{ 'Leave empty to use bare domain' }}" autofocus>
<!-- Modal postinstall confirm -->
<div class="modal fade" id="appPostInstallConfirmModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<img ng-src="{{appPostInstallConfirm.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-info-icon"/>
<h5 class="app-info-title">
{{ appPostInstallConfirm.app.manifest.title }}
<span class="app-info-meta text-small">Package <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>
<br/>
</h5>
</div>
<div class="modal-body">
<p ng-show="appPostInstallConfirm.customAuth && !appPostInstallConfirm.app.manifest.addons.email"></p>
<p ng-show="appPostInstallConfirm.app.manifest.addons.email">This app is set up to allow all users with a mailbox on this Cloudron. Login with the email and Cloudron password to access the mailbox.</p>
<p ng-show="!appPostInstallConfirm.customAuth && !appPostInstallConfirm.app.manifest.addons.email"> This app is set up to authenticate with the Cloudron User Directory. Cloudron users can login and use {{ appPostInstallConfirm.app.manifest.title }}.</p>
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
{{ (!appConfigure.location ? '' : (appConfigure.domain.provider !== 'caas' ? '.' : '-')) + appConfigure.domain.domain }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li ng-repeat="domain in domains">
<a href="" ng-click="appConfigure.domain = domain">{{ domain.domain }}</a>
</li>
</ul>
</div>
</div>
</div>
<p class="text-center" ng-show="appConfigure.location && appConfigure.domain.provider === 'manual' && !appConfigure.domain.config.wildcard">
<b>Add an A record manually for {{ appConfigure.location }} to this Cloudron's public IP</b>
<br>
</p>
<div class="has-error text-center" ng-show="appConfigure.error.port">{{ appConfigure.error.port }}</div>
<div ng-repeat="(env, info) in appConfigure.portBindingsInfo">
<ng-form name="portInfo_form">
<div class="form-group" ng-class="{ 'has-error': (!appConfigureForm.itemName{{$index}}.$dirty && appConfigure.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
<label class="control-label" for="appConfigurePortInput{{env}}"><input type="checkbox" ng-model="appConfigure.portBindingsEnabled[env]"> {{ info.description }} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})</label>
<input type="number" class="form-control" ng-model="appConfigure.portBindings[env]" ng-disabled="!appConfigure.portBindingsEnabled[env]" id="appConfigurePortInput{{env}}" later-name="itemName{{$index}}" min="{{HOST_PORT_MIN}}" max="{{HOST_PORT_MAX}}" required>
</div>
</ng-form>
</div>
<div class="form-group">
<label ng-show="appConfigure.ssoAuth" class="control-label">User management</label>
<label ng-show="!appConfigure.ssoAuth" class="control-label">Dashboard visibility</label>
<p ng-show="!appConfigure.ssoAuth && !appConfigure.app.manifest.addons.email" class="text-small">
This app has it's own user management.
</p>
<p ng-show="!appConfigure.ssoAuth && appConfigure.app.manifest.addons.email">
This app is pre-configured for use with <a href="https://cloudron.io/documentation/email/" target="_blank">Cloudron Email</a>.
</p>
<div class="radio">
<label>
<input type="radio" ng-model="appConfigure.accessRestrictionOption" value="any">
<span ng-show="appConfigure.ssoAuth">Allow all users on this Cloudron</span>
<span ng-show="!appConfigure.ssoAuth">Visible to all users on this Cloudron</span>
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="appConfigure.accessRestrictionOption" value="groups">
<span ng-show="appConfigure.ssoAuth">Only allow the following users and groups</span>
<span ng-show="!appConfigure.ssoAuth">Only visible to the following users and groups</span>
<span class="label label-danger" ng-show="appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid()">Select at least one user or group</span>
</label>
</div>
<div>
<div style="margin-left: 20px;">
<div class="col-md-5">
Users:
<multiselect class="input-sm stretch" ng-model="appConfigure.accessRestriction.users" ng-disabled="appConfigure.accessRestrictionOption !== 'groups'" options="user.display for user in users" data-multiple="true"></multiselect>
</div>
<div class="col-md-5">
Groups:
<multiselect class="input-sm stretch" ng-model="appConfigure.accessRestriction.groups" ng-disabled="appConfigure.accessRestrictionOption !== 'groups'" options="group.name for group in (groups | ignoreAdminGroup)" data-multiple="true"></multiselect>
</div>
</div>
</div>
<br/>
<br/>
</div>
</uib-tab>
<uib-tab index="1" heading="Advanced">
<br/>
<div class="form-group">
<label class="control-label" for="memoryLimit">Memory Limit <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#increasing-the-memory-limit-of-an-app" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> : <b>{{ appConfigure.memoryLimit ? appConfigure.memoryLimit / 1024 / 1024 + 'MB' : 'Default (256 MB)' }}</b></label>
<br/>
<div style="padding: 0 10px;">
<slider id="memoryLimit" ng-model="appConfigure.memoryLimit" step="134217728" tooltip="hide" ticks="appConfigure.memoryTicks" ticks-snap-bounds="67108864"></slider>
</div>
</div>
<div class="form-group" ng-show="appConfigure.app.manifest.addons.sendmail" ng-class="{ 'has-error': !appConfigureForm.mailboxName.$dirty && appConfigure.error.mailboxName }">
<label class="control-label">Mailbox Name</label>
<div class="control-label" ng-show="appConfigure.error.mailboxName">{{appConfigure.error.mailboxName}}</div>
<div class="input-group form-inline">
<input type="text" class="form-control" id="appConfigureMailboxNameInput" name="mailboxName" ng-model="appConfigure.mailboxName" uib-tooltip="App FROM email address">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
@{{ appConfigure.domain.domain }}
</button>
</div>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.xFrameOptions.$dirty && appConfigure.error.xFrameOptions }">
<label class="control-label">Allow embedding from the following site</label>
<div class="control-label" ng-show="appConfigure.error.xFrameOptions">{{appConfigure.error.xFrameOptions}}</div>
<input type="text" class="form-control" id="appConfigureXFrameOptionsInput" name="xFrameOptions" placeholder="https://example.com" ng-model="appConfigure.xFrameOptions" uib-tooltip="Leave blank to not allow embedding">
</div>
<div class="form-group">
<label class="control-label">Specify robots.txt file content</label>
<textarea ng-model="appConfigure.robotsTxt" placeholder="Leave empty to allow all bots to index this app." class="form-control" rows="3"></textarea>
</div>
<div class="form-group">
<input type="checkbox" id="appConfigureEnableBackup" ng-model="appConfigure.enableBackup">
<label class="control-label" for="appConfigureEnableBackup">Enable automatic daily backups</label>
</div>
<div class="hide">
<label class="control-label" for="appConfigureCertificateInput" ng-show="appConfigure.domain.provider !== 'caas'">Certificate (optional)</label>
<div class="has-error text-center" ng-show="appConfigure.error.cert && appConfigure.domain.provider !== 'caas'">{{ appConfigure.error.cert }}</div>
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.certificate.$dirty && appConfigure.error.cert }" ng-show="appConfigure.domain.provider !== 'caas'">
<div class="input-group">
<input type="file" id="appConfigureCertificateFileInput" onchange="readCertificate()" style="display:none"/>
<input type="text" class="form-control" placeholder="Certificate" ng-model="appConfigure.certificateFileName" id="appConfigureCertificateInput" name="certificate" onclick="getElementById('appConfigureCertificateFileInput').click();" style="cursor: pointer;" ng-required="appConfigure.keyFileName">
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('appConfigureCertificateFileInput').click();"></i>
</span>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.key.$dirty && appConfigure.error.cert }" ng-show="appConfigure.domain.provider !== 'caas'">
<div class="input-group">
<input type="file" id="appConfigureKeyFileInput" onchange="readKey()" style="display:none"/>
<input type="text" class="form-control" placeholder="Key" ng-model="appConfigure.keyFileName" id="appConfigureKeyInput" name="key" onclick="getElementById('appConfigureKeyFileInput').click();" style="cursor: pointer;" ng-required="appConfigure.certificateFileName">
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('appConfigureKeyFileInput').click();"></i>
</span>
</div>
</div>
</div>
</uib-tab>
</uib-tabset>
<input class="ng-hide" type="submit" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid())"/>
</form>
</fieldset>
</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="appConfigure.submit()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid())"><i class="fa fa-circle-o-notch fa-spin" ng-show="appConfigure.busy"></i> Configure</button>
</div>
<div ng-bind-html="appPostInstallConfirm.app.manifest.postInstallMessage | 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>
</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>
</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>
</div>
</div>
</div>
</div>
<!-- Modal restore app -->
<div class="modal fade" id="appRestoreModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Backups - {{ appRestore.app.fqdn }}</h4>
</div>
<div class="modal-body" style="padding: 0 15px">
<p class="text-center" ng-show="appRestore.busyFetching"><i class="fa fa-circle-o-notch fa-spin"></i> Fetching backups</p>
<button type="button" class="btn btn-primary pull-right" ng-click="appRestore.createBackup()" ng-hide="appRestore.busyFetching" ng-disabled="appRestore.app.installationState === 'pending_backup'"><i class="fa fa-circle-o-notch fa-spin" ng-show="appRestore.app.installationState === 'pending_backup'"></i> Create Backup</button>
<uib-tabset active="appRestore.action" ng-show="!appRestore.busyFetching">
<!-- restore -->
<uib-tab index="'restore'" heading="Restore">
<br/>
<p class="text-danger" ng-hide="appRestore.backups.length">This app has no backups to restore or clone from yet.</p>
<div ng-show="appRestore.backups.length">
<p>Restoring the app will lose all content generated since the backup.</p>
<label class="control-label">Select Backup</label>
<div class="dropdown">
<button type="button" class="btn btn-default" data-toggle="dropdown">{{ appRestore.selectedBackup.creationTime | prettyDate }} - v{{appRestore.selectedBackup.version}} ({{ appRestore.selectedBackup.creationTime | prettyLongDate }}) <span class="caret"></span></button>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="backup in appRestore.backups | orderBy:'-creationTime'">
<a href="" ng-click="appRestore.selectBackup(backup)">{{ backup.creationTime | prettyDate }} - v{{backup.version}} ({{ backup.creationTime | prettyLongDate }})</a>
</li>
</ul>
</div>
<br/>
<fieldset>
<form role="form" ng-submit="appRestore.restore()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': appRestore.error.password }">
<label class="control-label" for="appRestorePasswordInput">Provide your password to confirm this action</label>
<div ng-show="appRestore.error.password"><small>Wrong password</small></div>
<input type="password" class="form-control" ng-model="appRestore.password" id="appRestorePasswordInput" name="password" required autofocus>
</div>
<input class="ng-hide" type="submit" ng-disabled="!appRestore.password || appRestore.busy || !appRestore.selectedBackup"/>
</form>
</fieldset>
</div>
</uib-tab>
<!-- clone -->
<uib-tab index="'clone'" heading="Clone">
<br/>
<p class="text-danger" ng-hide="appRestore.backups.length">This app has no backups to restore or clone from yet.</p>
<div ng-show="appRestore.backups.length">
<label class="control-label">Select Backup</label>
<div class="dropdown">
<button type="button" class="btn btn-default" data-toggle="dropdown">{{ appRestore.selectedBackup.creationTime | prettyDate }} - v{{appRestore.selectedBackup.version}} ({{ appRestore.selectedBackup.creationTime | prettyLongDate }}) <span class="caret"></span></button>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="backup in appRestore.backups | orderBy:'-creationTime'">
<a href="" ng-click="appRestore.selectBackup(backup)">{{ backup.creationTime | prettyDate }} - v{{backup.version}} ({{ backup.creationTime | prettyLongDate }})</a>
</li>
</ul>
</div>
<br/>
<fieldset>
<form role="form" name="appCloneForm" ng-submit="appRestore.clone()" autocomplete="off">
<div class="has-error text-center" ng-show="appRestore.error.other" ng-bind-html="appRestore.error.other"></div>
<div class="form-group" ng-class="{ 'has-error': (appCloneForm.location.$dirty && appCloneForm.location.$invalid) || (!appCloneForm.location.$dirty && appRestore.error.location) }">
<label class="control-label" for="appRestoreLocationInput">Location {{ appRestore.error.location }} </label>
<div class="input-group form-inline">
<input type="text" class="form-control" ng-model="appRestore.location" id="appRestoreLocationInput" name="location" placeholder="Leave empty to use bare domain" autofocus>
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
{{ (appRestore.location ? (appRestore.domain.provider !== 'caas' ? '.' : '-') : '') + appRestore.domain.domain }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li ng-repeat="domain in domains">
<a href="" ng-click="appRestore.domain = domain">{{ domain.domain }}</a>
</li>
</ul>
</div>
</div>
</div>
<p class="text-center" ng-show="appRestore.location && appRestore.domain.provider === 'manual' && !appRestore.domain.config.wildcard">
<b>Add an A record manually for {{ appRestore.location }} to this Cloudron's public IP</b>
<br>
</p>
<div class="has-error text-center" ng-show="appRestore.error.port">{{ appRestore.error.port }}</div>
<div ng-repeat="(env, info) in appRestore.portBindingsInfo">
<ng-form name="portInfo_form">
<div class="form-group" ng-class="{ 'has-error': (!appRestore.itemName{{$index}}.$dirty && appRestore.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appRestore.portBindingsEnabled[env]"> {{ info.description }} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})</label>
<input type="number" class="form-control" ng-model="appRestore.portBindings[env]" ng-disabled="!appRestore.portBindingsEnabled[env]" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{hostPortMin}}" max="{{hostPortMax}}" required>
</div>
</ng-form>
</div>
</form>
</fieldset>
</div>
</uib-tab>
</uib-tabset>
</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="appRestore.clone()" ng-show="appRestore.action === 'clone' && appRestore.backups.length !== 0" ng-disabled="appCloneForm.$invalid || appRestore.busy || !appRestore.selectedBackup"><i class="fa fa-circle-o-notch fa-spin" ng-show="appRestore.busy"></i> Clone</button>
<button type="button" class="btn btn-danger" ng-click="appRestore.restore()" ng-show="appRestore.action === 'restore' && appRestore.backups.length !== 0" ng-disabled="!appRestore.password || appRestore.busy || !appRestore.selectedBackup"><i class="fa fa-circle-o-notch fa-spin" ng-show="appRestore.busy"></i> Restore</button>
</div>
</div>
</div>
</div>
<!-- Modal information of app -->
<!-- Modal app info -->
<div class="modal fade" id="appInfoModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<img ng-src="{{appInfo.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-info-icon"/>
<h5 class="app-info-title">
{{ appInfo.app.manifest.title }}
<span class="app-info-meta text-small">Package <a ng-href="/#/appstore/{{appInfo.app.manifest.id}}?version={{appInfo.app.manifest.version}}">v{{ appInfo.app.manifest.version }}</a> </span>
</h5>
<br/>
<span class="app-info-meta" ng-show="appInfo.app.manifest.documentationUrl"><a target="_blank" ng-href="{{appInfo.app.manifest.documentationUrl}}">Documentation</a> </span>
<br/>
<span class="app-info-meta">Last updated {{ appInfo.app.updateTime | prettyDate }}</span>
</div>
<div class="modal-body">
<div class="app-postinstall-message" ng-hide="appInfo.app.manifest && appInfo.app.manifest.postInstallMessage">
This package has no special usage information.
</div>
<div class="app-postinstall-message" ng-show="appInfo.app.manifest && appInfo.app.manifest.postInstallMessage">
<div ng-bind-html="appInfo.message | postInstallMessage:appInfo.app | markdown2html"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" autofocus>Got it</button>
</div>
</div>
</div>
</div>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<img ng-src="{{appInfo.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-info-icon" style="padding-right: 10px;"/>
<h4 style="margin-top: 0;">
{{ appInfo.app.manifest.title }}
<br/>
<span class="text-small text-muted">Package <a ng-href="/#/appstore/{{appInfo.app.manifest.id}}?version={{appInfo.app.manifest.version}}">v{{ appInfo.app.manifest.version }}</a></span>
<br/>
<span class="text-small text-muted" ng-show="appInfo.app.upstreamVersion">App v{{ appInfo.app.upstreamVersion }}</a></span>
</h5>
</div>
<div class="modal-body">
<p ng-hide="appInfo.app.appStoreId">
This is a custom app and not installed from the App Store and will not receive updates. See the <a target="_blank" ng-href="https://docs.cloudron.io/custom-apps/tutorial/">documentation</a>
on how to update a custom app.
</p>
<p ng-show="appInfo.app.manifest.documentationUrl">
Please see the <a target="_blank" ng-href="{{appInfo.app.manifest.documentationUrl}}">{{ appInfo.app.manifest.title }} documentation</a> for helpful information and common topics on this app.
If you need further help, refer to Cloudron's <a target="_blank" ng-href="{{ appInfo.app.manifest.forumUrl || 'https://forum.cloudron.io' }}">{{ appInfo.app.manifest.title }} forum section</a>.
</p>
<!-- Modal error app -->
<div class="modal fade" id="appErrorModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Error for {{ appError.app.fqdn }}</h4>
</div>
<div class="modal-body">
<p>{{ appError.app.message | prettyAppMessage }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default pull-left" ng-click="appConfigure.show(appError.app)" autofocus>Repair</button>
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
<p ng-show="appInfo.customAuth && !appInstall.app.manifest.addons.email"></p>
<p ng-show="appInfo.app.manifest.addons.email">This app is set up to allow all users with a mailbox on this Cloudron. Login with the email and Cloudron password to access the mailbox.</p>
<p ng-show="!appInfo.customAuth && !appInfo.app.manifest.addons.email"> This app is set up to authenticate with the Cloudron User Directory. Cloudron users can login and use {{ appInfo.app.manifest.title }}.</p>
<!-- Modal uninstall app -->
<div class="modal fade" id="appUninstallModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Really uninstall {{ appUninstall.app.fqdn }} ?</h4>
</div>
<div class="modal-body">
<p>Deleting the app will also remove all content generated within this app!</p>
<fieldset>
<form role="form" name="appUninstallForm" ng-submit="doUninstall()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': (appUninstallForm.password.$dirty && appUninstallForm.password.$invalid) || (!appUninstallForm.password.$dirty && appUninstall.error.password) }">
<label class="control-label" for="appUninstallPasswordInput">Provide your password to confirm this action</label>
<div class="control-label" ng-show="(appUninstallForm.password.$dirty && appUninstallForm.password.$invalid) || (!appUninstallForm.password.$dirty && appUninstall.error.password)">
<small ng-show=" appUninstallForm.password.$dirty && appUninstallForm.password.$invalid">Password required</small>
<small ng-show="!appUninstallForm.password.$dirty && appUninstall.error.password">Wrong password</small>
</div>
<input type="password" class="form-control" ng-model="appUninstall.password" id="appUninstallPasswordInput" name="password" required autofocus>
</div>
<a ng-show="appInfo.app.manifest.postInstallMessage" href="" data-toggle="collapse" data-parent="#accordion" data-target="#appinfoPostinstallMessage">
<i class="fa fa-angle-right"></i>
First time setup instructions
</a>
<input class="ng-hide" type="submit" ng-disabled="appUninstallForm.$invalid || busy"/>
</form>
</fieldset>
</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="doUninstall()" ng-disabled="appUninstallForm.$invalid || appUninstall.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="appUninstall.busy"></i> Uninstall</button>
</div>
<div id="appinfoPostinstallMessage" class="panel-collapse collapse">
<br/>
<div ng-bind-html="appInfo.app.manifest.postInstallMessage | markdown2html"></div>
</div>
</div>
</div>
<!-- Modal update app -->
<div class="modal fade" id="appUpdateModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Update {{ appUpdate.app.fqdn }}</h4>
</div>
<div class="modal-body">
<p>Recent Changes for new version <b>{{ appUpdate.manifest.version}}</b>:</p>
<div ng-bind-html="appUpdate.manifest.changelog | markdown2html"></div>
</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="doUpdate()" ng-disabled="appUpdate.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="appUpdate.busy"></i> Update</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
function imageErrorHandler(elem) {
'use strict';
var appstoreIconUrl = elem.getAttribute('appstore-icon');
var fallbackIconUrl = elem.getAttribute('fallback-icon');
if (elem.src === appstoreIconUrl) {
elem.src = fallbackIconUrl;
elem.onerror = null; // avoid retry after default icon cannot be loaded
} else {
elem.src = appstoreIconUrl;
}
elem.src = elem.getAttribute('fallback-icon');
elem.onerror = null; // avoid retry after default icon cannot be loaded
}
</script>
@@ -401,7 +94,7 @@
<!-- Workaround for select-all issue, see commit message -->
<div style="font-size: 1px;">&nbsp;</div>
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && user.admin">
<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>
@@ -410,108 +103,71 @@
</div>
</div>
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && !user.admin">
<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 on this Cloudron yet!</h1>
<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>
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
<div class="col-md-12">
<h1>Your Apps</h1>
<h1 class="view-header">
My Apps
<div class="pull-right">
<form class="form-inline">
<input type="text" class="form-control" ng-show="installedApps.length > 8" placeholder="Search Apps" id="appSearch" ng-model="appSearch"/>
<multiselect ng-model="selectedState" ng-show="installedApps.length > 1" ms-header="All States" ms-selected="{{ selectedState }}" options="state.label for state in states" data-multiple="false"></multiselect>
<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>
<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>
</h1>
</div>
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
<div class="col-sm-1 grid-item" ng-repeat="app in installedApps | orderBy:'location'">
<div style="background-color: white;" class="highlight grid-item-content" uib-tooltip="{{ app.fqdn }}">
<a ng-href="{{ app | applicationLink }}" ng-click="(app | installError) === true && showError(app)" target="_blank" ng-class="{ 'hand': !(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" appstore-icon="{{ app.iconUrlStore }}" 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.location || app.fqdn }}</div>
<div class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden" uib-tooltip="{{ app.message | shortAppMessage }}">
{{ app | installationStateLabel }}
</div>
<div class="status" ng-style="{ 'visibility': (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="app-grid">
<div class="grid-item" ng-repeat="app in installedApps | selectedStateFilter:selectedState | selectedTagFilter:selectedTags | selectedDomainFilter:selectedDomain | appSearchFilter:appSearch | orderBy:'fqdn'">
<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 class="highlight grid-item-content" uib-tooltip="{{ app.fqdn }}" tooltip-class="long nowrap">
<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 }}
</div>
<div class="grid-item-bottom-mobile" ng-show="user.admin">
<div class="row">
<div class="col-xs-4 text-left">
<a href="" ng-click="appRestore.show(app)" ng-show="backupConfig.provider !== 'noop'">
<i class="fa fa-undo scale"></i>
</a>
<a href="" ng-click="appConfigure.show(app)" ng-show="app.installationState === 'installed' || app.installationState === 'pending_configure' || (app | installError)">
<i ng-hide="(app | installError)" class="fa fa-pencil scale"></i>
<i ng-show="(app | installError)" class="fa fa-wrench scale"></i>
</a>
</div>
<div class="col-xs-4 text-center"></div>
<div class="col-xs-4 text-right">
<a href="" ng-click="showUninstall(app)">
<i class="fa fa-remove scale"></i>
</a>
</div>
</div>
<div class="status" ng-style="{ 'visibility': user.isAtLeastAdmin && (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 class="grid-item-bottom" ng-show="user.admin">
<div>
<a href="" ng-click="showUninstall(app)" uib-tooltip="Uninstall" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-remove scale"></i></a>
</div>
</div>
</div>
</div>
<div>
<a href="" ng-click="appRestore.show(app)" ng-show="backupConfig.provider !== 'noop'" uib-tooltip="Backups" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-archive scale"></i></a>
</div>
<div class="grid-item-actions" ng-show="user.isAtLeastAdmin">
<a ng-href="#/app/{{ app.id}}/display" uib-tooltip="Config" tooltip-placement="right" tooltip-class="grid-items-action-tooltip"><i class="fas fa-cogs"></i></a>
<a ng-href="{{ '/logs.html?appId=' + app.id }}" target="_blank" uib-tooltip="Logs" tooltip-placement="right" tooltip-class="grid-items-action-tooltip"><i class="fas fa-align-left"></i></a>
<a ng-href="" class="hand" ng-click="appInfo.show(app)" uib-tooltip="Info" tooltip-placement="right" tooltip-class="grid-items-action-tooltip"><i class="fas fa-info-circle"></i></a>
<a ng-href="{{ app | applicationLink }}{{ app.manifest.configurePath }}" target="_blank" ng-show="app.manifest.configurePath && (app | applicationLink)" uib-tooltip="Admin Page" tooltip-placement="right" tooltip-class="grid-items-action-tooltip"><i class="fas fa-external-link-alt"></i></a>
</div>
<div ng-show="(app.installationState === 'installed' || app.installationState === 'pending_configure') && !(app | installError)">
<a href="" ng-click="appConfigure.show(app)" uib-tooltip="Configure" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-pencil scale"></i></a>
</div>
<div ng-show="app | installError">
<a href="" ng-click="appConfigure.show(app)" uib-tooltip="Repair" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-wrench scale"></i></a>
</div>
<div>
<a href="" ng-click="showTerminal(app)" uib-tooltip="Terminal" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-terminal scale"></i></a>
</div>
<div>
<a href="" ng-click="showLogs(app)" uib-tooltip="Logs" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-file-text scale"></i></a>
</div>
<div>
<a href="" ng-click="showInformation(app)" uib-tooltip="Information" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-info-circle scale"></i></a>
</div>
</div>
<!-- we check the version here because the box updater does not know when an app gets updated -->
<div class="app-update-badge" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
<a href="" ng-click="showUpdate(app, config.update.apps[app.id].manifest)" title="Update Available">
<span class="fa-stack fa-lg scale-small">
<i class="fa fa-circle fa-stack-2x text-success"></i>
<i class="fa fa-refresh fa-stack-1x fa-inverse"></i>
</span>
</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>
</div>
</a>
</div>
</div>
</div>
</div>

View File

@@ -1,587 +1,122 @@
'use strict';
/* global angular:false */
/* global $:false */
angular.module('Application').controller('AppsController', ['$scope', '$timeout', '$interval', '$location', 'Client', function ($scope, $timeout, $interval, $location, Client) {
var ALL_DOMAINS_DOMAIN = { _alldomains: true, domain: 'All Domains' }; // dummy record for the single select filter
angular.module('Application').controller('AppsController', ['$scope', '$location', '$timeout', '$interval', 'Client', 'ngTld', 'AppStore', function ($scope, $location, $timeout, $interval, Client, ngTld, AppStore) {
$scope.HOST_PORT_MIN = 1024;
$scope.HOST_PORT_MAX = 65535;
$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.selectedDomain = ALL_DOMAINS_DOMAIN;
$scope.filterDomains = [ ALL_DOMAINS_DOMAIN ];
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
$scope.domains = [];
$scope.groups = [];
$scope.users = [];
$scope.backupConfig = {};
$scope.appSearch = '';
$scope.appConfigure = {
busy: false,
error: {},
$scope.$watch('selectedTags', function (newVal, oldVal) {
if (newVal === oldVal) return;
localStorage.selectedTags = newVal.join(',');
});
$scope.$watch('selectedDomain', function (newVal, oldVal) {
if (newVal === oldVal) return;
if (newVal._alldomains) localStorage.removeItem('selectedDomain');
else localStorage.selectedDomain = newVal.domain;
});
$scope.appPostInstallConfirm = {
app: {},
domain: '',
location: '',
advancedVisible: false,
portBindings: {},
portBindingsEnabled: {},
portBindingsInfo: {},
robotsTxt: '',
certificateFile: null,
certificateFileName: '',
keyFile: null,
keyFileName: '',
memoryLimit: 0,
memoryTicks: [],
mailboxName: '',
accessRestrictionOption: 'any',
accessRestriction: { users: [], groups: [] },
xFrameOptions: '',
ssoAuth: false,
isAccessRestrictionValid: function () {
var tmp = $scope.appConfigure.accessRestriction;
return !!(tmp.users.length || tmp.groups.length);
},
message: '',
confirmed: false,
customAuth: false,
show: function (app) {
$scope.reset();
$scope.appPostInstallConfirm.app = app;
$scope.appPostInstallConfirm.message = app.manifest.postInstallMessage;
$scope.appPostInstallConfirm.confirmed = false;
$scope.appPostInstallConfirm.customAuth = !(app.manifest.addons['ldap'] || app.manifest.addons['oauth']);
// fill relevant info from the app
$scope.appConfigure.app = app;
$scope.appConfigure.location = app.location;
$scope.appConfigure.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0];
$scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information
$scope. Option = app.accessRestriction ? 'groups' : 'any';
$scope.appConfigure.memoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
$scope.appConfigure.xFrameOptions = app.xFrameOptions.indexOf('ALLOW-FROM') === 0 ? app.xFrameOptions.split(' ')[1] : '';
$scope.appConfigure.robotsTxt = app.robotsTxt;
$scope.appConfigure.enableBackup = app.enableBackup;
$scope.appConfigure.mailboxName = app.mailboxName || '';
$scope.appConfigure.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['oauth']) && app.sso;
// 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.appConfigure.memoryTicks = [ ];
var npow2 = Math.pow(2, Math.ceil(Math.log($scope.config.memory)/Math.log(2)));
for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) {
if (i >= (app.manifest.memoryLimit/1024/1024 || 0)) $scope.appConfigure.memoryTicks.push(i * 1024 * 1024);
}
if (app.manifest.memoryLimit && $scope.appConfigure.memoryTicks[0] !== app.manifest.memoryLimit) {
$scope.appConfigure.memoryTicks.unshift(app.manifest.memoryLimit);
}
$scope.appConfigure.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any';
$scope.appConfigure.accessRestriction = { users: [], groups: [] };
if (app.accessRestriction) {
var userSet = { };
app.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; });
$scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.appConfigure.accessRestriction.users.push(u); });
var groupSet = { };
app.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; });
$scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.appConfigure.accessRestriction.groups.push(g); });
}
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
for (var env in $scope.appConfigure.portBindingsInfo) {
if (app.portBindings && app.portBindings[env]) {
$scope.appConfigure.portBindings[env] = app.portBindings[env];
$scope.appConfigure.portBindingsEnabled[env] = true;
} else {
$scope.appConfigure.portBindings[env] = $scope.appConfigure.portBindingsInfo[env].defaultValue || 0;
$scope.appConfigure.portBindingsEnabled[env] = false;
}
}
$('#appConfigureModal').modal('show');
},
submit: function () {
$scope.appConfigure.busy = true;
$scope.appConfigure.error.other = null;
$scope.appConfigure.error.location = null;
$scope.appConfigure.error.xFrameOptions = null;
$scope.appConfigure.error.mailboxName = null;
// only use enabled ports from portBindings
var finalPortBindings = {};
for (var env in $scope.appConfigure.portBindings) {
if ($scope.appConfigure.portBindingsEnabled[env]) {
finalPortBindings[env] = $scope.appConfigure.portBindings[env];
}
}
var finalAccessRestriction = null;
if ($scope.appConfigure.accessRestrictionOption === 'groups') {
finalAccessRestriction = { users: [], groups: [] };
finalAccessRestriction.users = $scope.appConfigure.accessRestriction.users.map(function (u) { return u.id; });
finalAccessRestriction.groups = $scope.appConfigure.accessRestriction.groups.map(function (g) { return g.id; });
}
var data = {
location: $scope.appConfigure.location,
domain: $scope.appConfigure.domain.domain,
portBindings: finalPortBindings,
accessRestriction: finalAccessRestriction,
cert: $scope.appConfigure.certificateFile,
key: $scope.appConfigure.keyFile,
xFrameOptions: $scope.appConfigure.xFrameOptions ? ('ALLOW-FROM ' + $scope.appConfigure.xFrameOptions) : 'SAMEORIGIN',
memoryLimit: $scope.appConfigure.memoryLimit === $scope.appConfigure.memoryTicks[0] ? 0 : $scope.appConfigure.memoryLimit,
robotsTxt: $scope.appConfigure.robotsTxt,
enableBackup: $scope.appConfigure.enableBackup
};
if ($scope.appConfigure.mailboxName !== $scope.appConfigure.app.mailboxName) data.mailboxName = $scope.appConfigure.mailboxName;
Client.configureApp($scope.appConfigure.app.id, data, function (error) {
if (error) {
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
$scope.appConfigure.error.port = error.message;
} else if (error.statusCode === 409 && error.message.indexOf('mailbox') !== -1 ) {
$scope.appConfigure.error.mailboxName = error.message;
$scope.appConfigureForm.mailboxName.$setPristine();
$('#appConfigureMailboxNameInput').focus();
} else if (error.statusCode === 409) {
$scope.appConfigure.error.location = 'This name is already taken.';
$scope.appConfigureForm.location.$setPristine();
$('#appConfigureLocationInput').focus();
} else if (error.statusCode === 400 && error.message.indexOf('cert') !== -1 ) {
$scope.appConfigure.error.cert = error.message;
$scope.appConfigure.certificateFileName = '';
$scope.appConfigure.certificateFile = null;
$scope.appConfigure.keyFileName = '';
$scope.appConfigure.keyFile = null;
} else if (error.statusCode === 400 && error.message.indexOf('xFrameOptions') !== -1 ) {
$scope.appConfigure.error.xFrameOptions = error.message;
$scope.appConfigureForm.xFrameOptions.$setPristine();
$('#appConfigureXFrameOptionsInput').focus();
} else {
$scope.appConfigure.error.other = error.message;
}
$scope.appConfigure.busy = false;
return;
}
$scope.appConfigure.busy = false;
Client.refreshInstalledApps(); // reflect the new app state immediately
$('#appConfigureModal').modal('hide');
$scope.reset();
});
}
};
$scope.appUninstall = {
busy: false,
error: {},
app: {},
password: ''
};
$scope.appRestore = {
busy: false,
busyFetching: false,
error: {},
app: {},
password: '',
backups: [ ],
selectedBackup: null,
// from clone
location: '',
domain: null,
portBindings: {},
portBindingsInfo: {},
portBindingsEnabled: {},
action: 'restore',
selectBackup: function (backup) {
$scope.appRestore.selectedBackup = backup;
},
createBackup: function () {
Client.backupApp($scope.appRestore.app.id, function (error) {
if (error) Client.error(error);
function waitForBackupFinish() {
if ($scope.appRestore.app.installationState === 'pending_backup') return $timeout(waitForBackupFinish, 1000);
// we are done, refresh the backup list
Client.getAppBackups($scope.appRestore.app.id, function (error, backups) {
if (error) return Client.error(error);
$scope.appRestore.backups = backups;
if (backups.length) $scope.appRestore.selectedBackup = backups[0]; // pre-select first backup
});
}
// reflect the new app state immediately
Client.refreshInstalledApps(waitForBackupFinish);
});
},
clone: function () {
$scope.appRestore.busy = true;
var data = {
location: $scope.appRestore.location,
domain: $scope.appRestore.domain.domain,
portBindings: $scope.appRestore.portBindings,
backupId: $scope.appRestore.selectedBackup.id
};
Client.cloneApp($scope.appRestore.app.id, data, function (error) {
$scope.appRestore.busy = false;
if (error) {
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
$scope.appRestore.error.port = error.message;
} else if (error.statusCode === 409) {
$scope.appRestore.error.location = 'This name is already taken.';
$scope.appCloneForm.location.$setPristine();
$('#appRestoreLocationInput').focus();
} else {
Client.error(error);
}
} else {
$('#appRestoreModal').modal('hide');
}
Client.refreshInstalledApps(); // reflect the new app state immediately
});
},
show: function (app) {
$scope.reset();
$scope.appRestore.app = app;
$scope.appRestore.busyFetching = true;
$scope.appRestore.domain = $scope.domains.find(function (d) { return app.domain === d.domain; }); // pre-select the app's domain
$scope.appRestore.portBindingsInfo = $scope.appRestore.app.manifest.tcpPorts || {}; // Portbinding map only for information
// set default ports
for (var env in $scope.appRestore.app.manifest.tcpPorts) {
$scope.appRestore.portBindings[env] = $scope.appRestore.app.manifest.tcpPorts[env].defaultValue || 0;
$scope.appRestore.portBindingsEnabled[env] = true;
}
$scope.appRestore.action = 'restore';
$('#appRestoreModal').modal('show');
Client.getAppBackups(app.id, function (error, backups) {
if (error) {
Client.error(error);
} else {
$scope.appRestore.backups = backups;
if (backups.length) $scope.appRestore.selectedBackup = backups[0]; // pre-select first backup
$scope.appRestore.busyFetching = false;
}
});
$('#appPostInstallConfirmModal').modal('show');
return false; // prevent propagation and default
},
restore: function () {
$scope.appRestore.busy = true;
$scope.appRestore.error.password = null;
submit: function () {
if (!$scope.appPostInstallConfirm.confirmed) return;
Client.restoreApp($scope.appRestore.app.id, $scope.appRestore.selectedBackup.id, $scope.appRestore.password, function (error) {
if (error && error.statusCode === 403) {
$scope.appRestore.password = '';
$scope.appRestore.error.password = true;
$('#appRestorePasswordInput').focus();
} else if (error) {
Client.error(error);
} else {
$('#appRestoreModal').modal('hide');
}
$scope.appPostInstallConfirm.app.pendingPostInstallConfirmation = false;
delete localStorage['confirmPostInstall_' + $scope.appPostInstallConfirm.app.id];
$scope.appRestore.busy = false;
Client.refreshInstalledApps(); // reflect the new app state immediately
});
$('#appPostInstallConfirmModal').modal('hide');
}
};
$scope.appInfo = {
app: {},
message: ''
message: '',
customAuth: false,
show: function (app) {
$scope.appInfo.app = app;
$scope.appInfo.message = app.manifest.postInstallMessage;
$scope.appInfo.customAuth = !(app.manifest.addons['ldap'] || app.manifest.addons['oauth']);
$('#appinfoPostinstallMessage').collapse('hide');
$('#appInfoModal').modal('show');
return false; // prevent propagation and default
}
};
$scope.appError = {
app: {}
$scope.showAppConfigure = function (app, view) {
$location.path('/app/' + app.id + '/' + view);
};
$scope.appUpdate = {
busy: false,
error: {},
app: {},
manifest: {},
portBindings: {}
};
$scope.reset = function () {
// close all dialogs
$('#appErrorModal').modal('hide');
$('#appConfigureModal').modal('hide');
$('#appRestoreModal').modal('hide');
$('#appUpdateModal').modal('hide');
$('#appInfoModal').modal('hide');
$('#appUninstallModal').modal('hide');
// reset configure dialog
$scope.appConfigure.error = {};
$scope.appConfigure.app = {};
$scope.appConfigure.domain = null;
$scope.appConfigure.location = '';
$scope.appConfigure.advancedVisible = false;
$scope.appConfigure.portBindings = {}; // This is the actual model holding the env:port pair
$scope.appConfigure.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
$scope.appConfigure.certificateFile = null;
$scope.appConfigure.certificateFileName = '';
$scope.appConfigure.keyFile = null;
$scope.appConfigure.keyFileName = '';
$scope.appConfigure.memoryLimit = 0;
$scope.appConfigure.memoryTicks = [];
$scope.appConfigure.accessRestrictionOption = 'any';
$scope.appConfigure.accessRestriction = { users: [], groups: [] };
$scope.appConfigure.xFrameOptions = '';
$scope.appConfigure.ssoAuth = false;
$scope.appConfigure.robotsTxt = '';
$scope.appConfigure.enableBackup = true;
$scope.appConfigureForm.$setPristine();
$scope.appConfigureForm.$setUntouched();
// reset uninstall dialog
$scope.appUninstall.app = {};
$scope.appUninstall.error = {};
$scope.appUninstall.password = '';
$scope.appUninstallForm.$setPristine();
$scope.appUninstallForm.$setUntouched();
// reset update dialog
$scope.appUpdate.error = {};
$scope.appUpdate.app = {};
$scope.appUpdate.manifest = {};
// reset restore dialog
$scope.appRestore.error = {};
$scope.appRestore.app = {};
$scope.appRestore.password = '';
$scope.appRestore.selectedBackup = null;
$scope.appRestore.backups = [];
$scope.appRestore.location = '';
$scope.appRestore.domain = null;
$scope.appRestore.portBindings = {};
$scope.appRestore.portBindingsInfo = {};
$scope.appRestore.portBindingsEnabled = {};
$scope.appRestore.action = 'restore';
};
$scope.readCertificate = function (event) {
$scope.$apply(function () {
$scope.appConfigure.certificateFile = null;
$scope.appConfigure.certificateFileName = event.target.files[0].name;
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read local file');
$scope.appConfigure.certificateFile = result.target.result;
};
reader.readAsText(event.target.files[0]);
});
};
$scope.readKey = function (event) {
$scope.$apply(function () {
$scope.appConfigure.keyFile = null;
$scope.appConfigure.keyFileName = event.target.files[0].name;
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read local file');
$scope.appConfigure.keyFile = result.target.result;
};
reader.readAsText(event.target.files[0]);
});
};
$scope.showInformation = function (app) {
$scope.reset();
$scope.appInfo.app = app;
$scope.appInfo.message = app.manifest.postInstallMessage;
$('#appInfoModal').modal('show');
return false; // prevent propagation and default
};
$scope.showLogs = function (app) {
window.open('/logs.html?id=' + app.id, 'Logs', 'width=1024,height=800').focus();
};
$scope.showTerminal = function (app) {
window.open('/terminal.html?id=' + app.id, 'Terminal', 'width=1024,height=800').focus();
};
$scope.showError = function (app) {
$scope.reset();
$scope.appError.app = app;
$('#appErrorModal').modal('show');
return false; // prevent propagation and default
};
$scope.showUninstall = function (app) {
$scope.reset();
$scope.appUninstall.app = app;
$('#appUninstallModal').modal('show');
};
$scope.doUninstall = function () {
$scope.appUninstall.busy = true;
$scope.appUninstall.error.password = null;
Client.uninstallApp($scope.appUninstall.app.id, $scope.appUninstall.password, function (error) {
if (error && error.statusCode === 403) {
$scope.appUninstall.password = '';
$scope.appUninstall.error.password = true;
$scope.appUninstallForm.password.$setPristine();
$('#appUninstallPasswordInput').focus();
} else if (error && error.statusCode === 402) { // unpurchase failed
Client.error('Relogin to Cloudron App Store');
} else if (error) {
Client.error(error);
} else {
$('#appUninstallModal').modal('hide');
$scope.reset();
}
$scope.appUninstall.busy = false;
Client.refreshInstalledApps(); // reflect the new app state immediately
});
};
$scope.showUpdate = function (app, updateManifest) {
$scope.reset();
$scope.appUpdate.app = app;
$scope.appUpdate.manifest = angular.copy(updateManifest);
$('#appUpdateModal').modal('show');
};
$scope.doUpdate = function () {
$scope.appUpdate.busy = true;
Client.updateApp($scope.appUpdate.app.id, $scope.appUpdate.manifest, function (error) {
if (error) {
Client.error(error);
} else {
$scope.appUpdate.app = {};
$('#appUpdateModal').modal('hide');
}
$scope.appUpdate.busy = false;
Client.refreshInstalledApps(); // reflect the new app state immediately
});
};
$scope.renderAccessRestrictionUser = function (userId) {
var user = $scope.users.filter(function (u) { return u.id === userId; })[0];
// user not found
if (!user) return userId;
return user.username ? user.username : user.email;
};
$scope.cancel = function () {
window.history.back();
};
function fetchUsers() {
Client.getUsers(function (error, users) {
if (error) {
console.error(error);
return $timeout(fetchUsers, 5000);
}
// ensure we have something to work with in the access restriction dropdowns
users.forEach(function (user) { user.display = user.username || user.email; });
$scope.users = users;
});
}
function fetchGroups() {
Client.getGroups(function (error, groups) {
if (error) {
console.error(error);
return $timeout(fetchUsers, 5000);
}
$scope.groups = groups;
});
}
function getDomains() {
Client.getDomains(function (error, result) {
if (error) {
console.error(error);
return $timeout(getDomains, 5000);
}
$scope.domains = result;
});
}
function getBackupConfig() {
Client.getBackupConfig(function (error, backupConfig) {
if (error) return console.error(error);
$scope.backupConfig = backupConfig;
});
}
Client.onReady(function () {
Client.refreshInstalledApps(function (error) {
if (error) return console.error(error);
if ($scope.user.admin) {
fetchUsers();
fetchGroups();
getDomains();
getBackupConfig();
}
var refreshAppsTimer = $interval(Client.refreshInstalledApps.bind(Client), 5000);
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;
// load local settings and apply tag filter
if (localStorage.selectedTags) {
if (!$scope.tags.length) localStorage.removeItem('selectedTags');
else $scope.selectedTags = localStorage.selectedTags.split(',');
}
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;
});
});
// setup all the dialog focus handling
['appConfigureModal', 'appUninstallModal', 'appUpdateModal', 'appRestoreModal', 'appInfoModal', 'appErrorModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find("[autofocus]:first").focus();
});
$('.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();

View File

@@ -1,172 +1,171 @@
<!-- Modal install app -->
<div class="modal fade appstore-install" id="appInstallModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel" aria-hidden="true">
<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>
<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>
<br/>
<span class="appstore-install-meta hand">Requires atleast {{ appInstall.app.manifest.memoryLimit | prettyMemory }}MB memory</span>
<div class="modal fade appstore-install" id="appInstallModal" tabindex="-1" role="dialog">
<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>
<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>
<br/>
<span class="appstore-install-meta">Requires at least {{ appInstall.app.manifest.memoryLimit | prettyByteSize:'256 MB' }} memory</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>
<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>
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span>{{ (appInstall.location ? '.' : '') + appInstall.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.domain = domain">{{ domain.domain }}</a>
</li>
</ul>
</div>
</div>
</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 {{ appInstall.error.location }} </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>
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
{{ (appInstall.location ? (appInstall.domain.provider !== 'caas' ? '.' : '-') : '') + appInstall.domain.domain }}
<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.domain = domain">{{ domain.domain }}</a>
</li>
</ul>
</div>
</div>
</div>
<p class="text-center" ng-show="appInstall.location && appInstall.domain.provider === 'manual' && !appInstall.domain.config.wildcard">
<b>Add an A record manually for {{ appInstall.location }} to this Cloudron's public IP</b>
<br>
</p>
<p class="small text-center text-warning" ng-show="appInstall.domain.provider === 'linode'">
<b>Linode DNS average <a target="_blank" ng-href="https://docs.cloudron.io/domains/#linode-dns">propagation time</a> is 30 minutes.
Installing the app will take a while.</b>
<br>
</p>
<div class="has-error text-center" ng-show="appInstall.error.port">{{ appInstall.error.port }}</div>
<div ng-repeat="(env, info) in appInstall.portBindingsInfo">
<ng-form name="portInfo_form">
<div class="form-group" ng-class="{ 'has-error': (!appInstallForm.itemName{{$index}}.$dirty && appInstall.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appInstall.portBindingsEnabled[env]"> {{ info.description }} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})</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>
</div>
</ng-form>
</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>
<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>
</div>
<div class="form-group" ng-hide="appInstall.customAuth || appInstall.app.manifest.addons.email">
<label class="control-label">User management</label>
<div class="radio" ng-show="appInstall.optionalSso">
<label>
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="nosso">
Leave user management to the app
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="any">
Allow all users from this Cloudron
</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>
</label>
</div>
<div>
<div style="margin-left: 20px;">
<div class="col-md-5">
Users: &nbsp;
<multiselect ng-model="appInstall.accessRestriction.users" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" options="user.username for user in users" data-multiple="true"></multiselect>
</div>
<div class="col-md-5">
Groups: &nbsp;
<multiselect ng-model="appInstall.accessRestriction.groups" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" options="group.name for group in (groups | ignoreAdminGroup)" data-multiple="true"></multiselect>
</div>
</div>
</div>
<br/>
<br/>
</div>
<p ng-show="appInstall.app.manifest.addons.email" class="text-info">
This app is pre-configured for use with <a href="https://cloudron.io/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'">
<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">
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('appInstallCertificateFileInput').click();"></i>
</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="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">
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('appInstallKeyFileInput').click();"></i>
</span>
</div>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="appInstallForm.$invalid || busy"/>
</form>
<div class="has-error text-center" ng-show="appInstall.error.port">{{ appInstall.error.port }}</div>
<div ng-repeat="(env, info) in appInstall.portBindingsInfo">
<ng-form name="portInfo_form">
<div class="form-group" ng-class="{ 'has-error': (!appInstallForm.itemName{{$index}}.$dirty && appInstall.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appInstall.portBindingsEnabled[env]">
{{ info.title }}
<sup>
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})"><i class="fa fa-question-circle"></i></a>
</sup>
</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>
</div>
<div class="collapse" id="collapseMediaLinksCarousel" data-toggle="false">
<div ng-repeat="mediaLink in appInstall.mediaLinks" class="slick-item" style="background-image: url('{{mediaLink}}');" ng-show="appInstall.mediaLinks.length == 1"></div>
<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>
<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 ng-show="config.provider === 'caas'">Please upgrade to a bigger plan. Alternately, free up resources by uninstalling unused applications.</p>
<p ng-hide="config.provider === 'caas'">Please upgrade to a server instance with more memory. Alternately, free up resources by uninstalling unused applications.</p>
</div>
<div class="collapse" id="collapseAppLimitReached" data-toggle="false">
<h4 class="text-danger">Subscription required to install more apps.</h4>
<p>The free plan only contains 2 apps, signing up for a Cloudron subscription allows up to 20 apps. All apps will be automatically kept up-to-date via the appstore.</p>
</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>
</div>
<div class="form-group" ng-show="!appInstall.customAuth && !appInstall.app.manifest.addons.email">
<label class="control-label">User management</label>
<div class="radio" ng-show="appInstall.optionalSso">
<label>
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="nosso">
Leave user management to the app
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="appInstall.accessRestrictionOption" value="any">
Allow all users from this Cloudron
</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>
</label>
</div>
<div>
<div style="margin-left: 20px;">
<div class="col-md-5">
Users: &nbsp;
<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="collapse" id="postInstallMessage" data-toggle="false">
<div class="appstore-install-description">
<div ng-bind-html="appInstall.app.manifest.postInstallMessage | postInstallMessage:appInstall.app | markdown2html"></div>
</div>
<div class="col-md-5">
Groups: &nbsp;
<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>
<div class="modal-footer">
<button type="button" class="btn btn-default" ng-show="appInstall.state !== 'postInstall'" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-default" ng-show="appInstall.state === 'postInstall'" data-dismiss="modal" ng-click="appInstall.switchToAppsView()">Got it</button>
<button type="button" class="btn btn-success" ng-show="config.provider === 'caas' && user.admin && appInstall.state === 'resourceConstraint'" ng-click="showView('/settings')">Upgrade Cloudron</button>
<button type="button" class="btn btn-danger" ng-show="config.provider !== 'caas' && user.admin && appInstall.state === 'resourceConstraint'" ng-click="appInstall.showForm(true)">Install anyway</button>
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'appInfo' && user.admin" ng-click="appInstall.showForm()">Install</button>
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'installForm' && user.admin" ng-click="appInstall.submit()" ng-disabled="appInstallForm.$invalid || appInstall.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="appInstall.busy"></i> Install</button>
<a class="btn btn-success" ng-show="appInstall.state === 'appLimitReached'" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.emailEncoded + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank">Setup Subscription</a>
</div>
<br/>
<br/>
</div>
<p ng-show="appInstall.app.manifest.addons.email" class="text-info">
This app is pre-configured for use with <a ng-href="https://docs.cloudron.io/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'">
<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">
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('appInstallCertificateFileInput').click();"></i>
</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="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">
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('appInstallKeyFileInput').click();"></i>
</span>
</div>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="(appInstall.accessRestrictionOption === 'groups' && !appInstall.isAccessRestrictionValid()) || appInstallForm.$invalid || busy"/>
</form>
</div>
<div class="collapse" id="collapseMediaLinksCarousel" data-toggle="false">
<div ng-repeat="mediaLink in appInstall.mediaLinks" class="slick-item" style="background-image: url('{{mediaLink}}');" ng-show="appInstall.mediaLinks.length == 1"></div>
<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>
<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>
</div>
<div class="collapse" id="collapseSubscriptionRequired" data-toggle="false">
<p>To install more apps, a paid subscription is required.</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success pull-left" ng-click="openSubscriptionSetup()" ng-show="appInstall.state === 'subscriptionRequired'">Setup Subscription</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</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>
</div>
</div>
</div>
</div>
<!-- Modal app not found -->
@@ -187,11 +186,11 @@
</div>
<div ng-show="!ready" class="loading-banner">
<h1><i class="fa fa-circle-o-notch fa-spin"></i></h1>
<h1><i class="fa fa-circle-notch fa-spin"></i></h1>
</div>
<!-- appstore login -->
<div ng-show="ready && !validAppstoreAccount" class="container card card-small appstore-login ng-cloak">
<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>
@@ -232,6 +231,20 @@
</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="msp">Managed Service Provider</option>
<option value="paas">PaaS - Develop &amp; 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>
@@ -242,7 +255,7 @@
<center>
<button type="submit" class="btn btn-lg btn-success" ng-disabled="appstoreLoginForm.$invalid || appstoreLogin.busy || !appstoreLogin.termsAccepted">
<i class="fa fa-circle-o-notch fa-spin" ng-show="appstoreLogin.busy"></i> <span ng-hide="appstoreLogin.register">Login</span><span ng-show="appstoreLogin.register">Create Account</span>
<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/>
@@ -257,64 +270,55 @@
</div>
<!-- give more vertical spacing so the login form does not appear clipped -->
<div ng-show="ready && !validAppstoreAccount">
<br/>
<br/>
<div ng-show="ready && !validSubscription">
<br/>
<br/>
</div>
<div ng-show="ready && validAppstoreAccount" 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 === '' }" 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-bar-chart"></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-o"></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-fork"></i> Code Hosting</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'CRM' }" category="crm"><i class="fa fa-connectdevelop"></i> CRM</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'email' }" category="email"><i class="fa fa-envelope-o"></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-refresh"></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-dollar"></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-picture-o"></i> Gallery</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-o"></i> Notes</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'project' }" category="project"><i class="fa fa-line-chart"></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-bars"></i> Web Hosting</a>
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'wiki' }" category="wiki"><i class="fa 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.publishState === 'testing' || app.publishState === 'pending_approval') }">
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.publishState === 'testing'">Testing</span>
<span class="badge badge-warning appstore-item-badge-testing" ng-show="app.publishState === 'pending_approval'">Pending Approval</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 }">All</button>
<button class="btn" type="button" ng-click="showCategory('featured');" ng-class="{ 'btn-primary': 'featured' === category }">Popular</button>
<button class="btn" type="button" ng-click="showCategory('new');" ng-class="{ 'btn-primary': 'new' === category }">New Apps</button>
<div class="dropdown">
<button class="btn dropdown-toggle" type="button" data-toggle="dropdown" ng-class="{ 'btn-primary': '' !== category && 'recent' !== category && 'featured' !== category && 'new' !== category }">
{{ categoryButtonLabel(category) }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li ng-repeat="category in categories"><a href="" ng-click="showCategory(category.id);"><i class="{{ category.icon }}"></i> {{ category.label }}</a></li>
</ul>
</div>
<input type="text" id="appstoreSearch" class="form-control" placeholder="Search for alternatives like Github, Dropbox, Slack, Trello, ..." 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">No apps found.</h3>
<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-12" ng-show="apps.length">
<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'">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>
</div>

Some files were not shown because too many files have changed in this diff Show More