Compare commits
1485 Commits
dashboard-
...
v5.6.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d3c27ec30 | ||
|
|
b6d3c18944 | ||
|
|
31282af701 | ||
|
|
45c8b83d54 | ||
|
|
29d23c9dcc | ||
|
|
589f19f370 | ||
|
|
8e20db664f | ||
|
|
fdcd457ce1 | ||
|
|
95516a2383 | ||
|
|
ba92b1e667 | ||
|
|
f3a159823a | ||
|
|
8388491e58 | ||
|
|
e87d206dda | ||
|
|
db4c8d92da | ||
|
|
daab4a95c2 | ||
|
|
22b8b9b9bd | ||
|
|
c87f3a8cb4 | ||
|
|
72118a0b66 | ||
|
|
68573ceb18 | ||
|
|
510b88cd68 | ||
|
|
490720e6a7 | ||
|
|
990f75dddc | ||
|
|
a3c6b82283 | ||
|
|
f5e0ff51f2 | ||
|
|
f114a629f3 | ||
|
|
5fca372ddf | ||
|
|
d9d1f13bf9 | ||
|
|
63b212bea5 | ||
|
|
5a1e09936f | ||
|
|
e21a504c35 | ||
|
|
3ba6c387e9 | ||
|
|
2c7238b2c9 | ||
|
|
92b9fc02fa | ||
|
|
576281990b | ||
|
|
6b7570df4e | ||
|
|
b141db4776 | ||
|
|
4cffcfff03 | ||
|
|
59ea292263 | ||
|
|
e0ca52b1da | ||
|
|
0c9ea1e0f0 | ||
|
|
c02cf0f5dc | ||
|
|
d0e2df5166 | ||
|
|
b9cda71413 | ||
|
|
e008e44566 | ||
|
|
c100539736 | ||
|
|
32aa3febf9 | ||
|
|
1249b3b3e8 | ||
|
|
18ba66afcc | ||
|
|
1000d88508 | ||
|
|
e13cb1debd | ||
|
|
2c3c8f8c4a | ||
|
|
b81196fa87 | ||
|
|
c7291af970 | ||
|
|
92c3237552 | ||
|
|
848e446b93 | ||
|
|
2f96f565eb | ||
|
|
8fa58eb108 | ||
|
|
31947127d9 | ||
|
|
2c7cfa1a93 | ||
|
|
b856c4f995 | ||
|
|
497be710a7 | ||
|
|
d7287b5c3c | ||
|
|
854010b823 | ||
|
|
39f7a5be70 | ||
|
|
dbc53b8d09 | ||
|
|
c4fe362a08 | ||
|
|
f55ec5de9b | ||
|
|
b2279c9acc | ||
|
|
b420d054ae | ||
|
|
566f0f7783 | ||
|
|
ae24c1d968 | ||
|
|
8ca344e3bf | ||
|
|
0458d2cb90 | ||
|
|
7c2322e6e0 | ||
|
|
08abe4bff2 | ||
|
|
eb69c365fc | ||
|
|
f6fef21bf7 | ||
|
|
4a1f8457cf | ||
|
|
5eb5b952d5 | ||
|
|
8a375c6363 | ||
|
|
ac23b610bc | ||
|
|
5f8b141f62 | ||
|
|
517db50712 | ||
|
|
6310a431dd | ||
|
|
9996e9a6d7 | ||
|
|
ddc211a8ea | ||
|
|
32f4f88b88 | ||
|
|
45b3062ac6 | ||
|
|
03296b3195 | ||
|
|
97df39a16f | ||
|
|
59cd6f6e93 | ||
|
|
d4312507e2 | ||
|
|
76950bdada | ||
|
|
01b7bc96fa | ||
|
|
efde15b848 | ||
|
|
941e0ba6c8 | ||
|
|
3b818855dc | ||
|
|
f73c8b00d4 | ||
|
|
08f116486a | ||
|
|
f6f5ae8578 | ||
|
|
d82dde4b7f | ||
|
|
91d4d95cb4 | ||
|
|
b9973d69c3 | ||
|
|
8c8e363abc | ||
|
|
aa240e8ee3 | ||
|
|
cdaf9e1876 | ||
|
|
1c8352ec56 | ||
|
|
43ef7f088d | ||
|
|
28b4f66f86 | ||
|
|
4fb94ea162 | ||
|
|
d24340f221 | ||
|
|
482cd123c0 | ||
|
|
ab3abe7e5e | ||
|
|
31fbffb435 | ||
|
|
9a7f8bd861 | ||
|
|
29c20cfcc4 | ||
|
|
b5c25bcaaa | ||
|
|
8abe0a174a | ||
|
|
692abcd6de | ||
|
|
03bdcc786e | ||
|
|
6df2985e2a | ||
|
|
05de8b54ec | ||
|
|
c0dad4f5a0 | ||
|
|
7ad425e399 | ||
|
|
836a3784cb | ||
|
|
06d4aec850 | ||
|
|
614674563a | ||
|
|
349633c8da | ||
|
|
7d4f617757 | ||
|
|
e82f17ab06 | ||
|
|
cb14592705 | ||
|
|
77300d6858 | ||
|
|
38682e48d4 | ||
|
|
1e5d28e2a2 | ||
|
|
ad86b4b1eb | ||
|
|
99927df991 | ||
|
|
6661f21e2f | ||
|
|
4ef963fe54 | ||
|
|
c87ddd5116 | ||
|
|
4f4df7d9fe | ||
|
|
0043b3690a | ||
|
|
be6c34386d | ||
|
|
a8e9a71489 | ||
|
|
90f42fe6cd | ||
|
|
6dd414fe7e | ||
|
|
4cb5e66ccb | ||
|
|
1fd4d772e4 | ||
|
|
3abdbdc7c9 | ||
|
|
6d6fba873f | ||
|
|
6aa8602b96 | ||
|
|
240272f7ce | ||
|
|
3d17a33c43 | ||
|
|
6956cfa32d | ||
|
|
3a54e662c2 | ||
|
|
823cfca3c3 | ||
|
|
9da2484bab | ||
|
|
0b50d62ef3 | ||
|
|
343e8e90ba | ||
|
|
02dcb013ef | ||
|
|
e77d3f4fcc | ||
|
|
7aff747b1c | ||
|
|
e97f3032cc | ||
|
|
ebabe29d8e | ||
|
|
b690c9bc95 | ||
|
|
fd3034bacc | ||
|
|
3bcef3d9c3 | ||
|
|
da54699815 | ||
|
|
6b64dd52b9 | ||
|
|
fb07dc2294 | ||
|
|
779c3ba75b | ||
|
|
4564e501d3 | ||
|
|
d271d2db57 | ||
|
|
46ed0ab49e | ||
|
|
35dfea03da | ||
|
|
ff5036a55b | ||
|
|
799892c220 | ||
|
|
8b160cbbfd | ||
|
|
48983879ab | ||
|
|
2cecdd7f01 | ||
|
|
4ebaa674c3 | ||
|
|
fb637f61f3 | ||
|
|
805e07e65f | ||
|
|
049a488e08 | ||
|
|
afc90817cf | ||
|
|
38f3e39258 | ||
|
|
c674d679bd | ||
|
|
7c2ab4e5bd | ||
|
|
b86dff8601 | ||
|
|
a725fc7a0b | ||
|
|
fbe3545153 | ||
|
|
50b528260c | ||
|
|
d2ece2b7f9 | ||
|
|
f71e47aac7 | ||
|
|
8d9c4b0476 | ||
|
|
ea1a62c3ef | ||
|
|
2e5e459094 | ||
|
|
f51eccdef7 | ||
|
|
a9a9af9ef7 | ||
|
|
200122deee | ||
|
|
4170be7f34 | ||
|
|
0be5a292c4 | ||
|
|
4555586254 | ||
|
|
173531b767 | ||
|
|
412082d3ef | ||
|
|
3b51b84308 | ||
|
|
d6d1ad98e4 | ||
|
|
e8560e6905 | ||
|
|
ccaabd6f06 | ||
|
|
9ba79cfb32 | ||
|
|
62e0e34e12 | ||
|
|
2d50ae4b00 | ||
|
|
11b567391c | ||
|
|
e50e488c8a | ||
|
|
2a9d32309e | ||
|
|
de0370011c | ||
|
|
4a844e582e | ||
|
|
d36aad4adc | ||
|
|
11240b6bbb | ||
|
|
b52d3231e4 | ||
|
|
c9ba4ba50a | ||
|
|
4db07b5254 | ||
|
|
83688f9fd8 | ||
|
|
7a384846f8 | ||
|
|
923f7f3aa8 | ||
|
|
8e0cfcda88 | ||
|
|
cd90af35a1 | ||
|
|
d2ac8536b3 | ||
|
|
5100a28ff1 | ||
|
|
0830e9293d | ||
|
|
4a981cd2e2 | ||
|
|
b1d956f7bf | ||
|
|
75b2c7236a | ||
|
|
c8278e7b24 | ||
|
|
dbf6520860 | ||
|
|
e593e48d40 | ||
|
|
39bccea953 | ||
|
|
98f62eba9d | ||
|
|
4e65728979 | ||
|
|
b58ca1506e | ||
|
|
e0334b3ac8 | ||
|
|
0fa230527c | ||
|
|
13c5085cb1 | ||
|
|
300a3919ab | ||
|
|
e65d946633 | ||
|
|
412bd1c1f4 | ||
|
|
1d15fd3178 | ||
|
|
cb94737519 | ||
|
|
01683e9383 | ||
|
|
1960969325 | ||
|
|
b49721f514 | ||
|
|
6876e82d64 | ||
|
|
15a7beae57 | ||
|
|
297a635613 | ||
|
|
e0778c52e8 | ||
|
|
e09b9964be | ||
|
|
1d27926220 | ||
|
|
7427d549cc | ||
|
|
37aeb3f713 | ||
|
|
7bf06da9f8 | ||
|
|
b6157d58c8 | ||
|
|
4767fe5515 | ||
|
|
750acdbcd7 | ||
|
|
29543fbc85 | ||
|
|
05913d0ae0 | ||
|
|
a31617fcb0 | ||
|
|
ec71b622fc | ||
|
|
3dd659639d | ||
|
|
4aca2b64b9 | ||
|
|
4c2c27c686 | ||
|
|
429f45a09a | ||
|
|
886c668107 | ||
|
|
c0df62cd5b | ||
|
|
a8e6d727fa | ||
|
|
ccf1c78cbb | ||
|
|
4e25688dd9 | ||
|
|
3378bf7a1e | ||
|
|
2bbafb5604 | ||
|
|
1e82774460 | ||
|
|
dce865c3cb | ||
|
|
81bf84b50a | ||
|
|
94b6f5bffd | ||
|
|
5440a3b62b | ||
|
|
24737382f9 | ||
|
|
5fa3215a4d | ||
|
|
105141be53 | ||
|
|
e19edcb67a | ||
|
|
be0b61a628 | ||
|
|
8d79244068 | ||
|
|
8ee66d3abf | ||
|
|
fb94416b1b | ||
|
|
70a925b416 | ||
|
|
959f245ce4 | ||
|
|
b3eb650315 | ||
|
|
bdf7da6ef6 | ||
|
|
36d49b8217 | ||
|
|
18ac61e8ab | ||
|
|
b524da23d5 | ||
|
|
eeac846f5a | ||
|
|
0410ba51ca | ||
|
|
ca3bf6fe5c | ||
|
|
4353a05350 | ||
|
|
d2a3bb7339 | ||
|
|
589ee2d0c5 | ||
|
|
2178dcc963 | ||
|
|
f18fdd4a46 | ||
|
|
4352d9c698 | ||
|
|
494884595c | ||
|
|
b17db02f9d | ||
|
|
0f33a6b34b | ||
|
|
231dfe70d0 | ||
|
|
79eecd8b3e | ||
|
|
ca09f64c12 | ||
|
|
dea1f01998 | ||
|
|
8cfae92c24 | ||
|
|
989a5ba685 | ||
|
|
a9e49d98fd | ||
|
|
f66d4e34d6 | ||
|
|
989820183c | ||
|
|
53f0e6c7d3 | ||
|
|
1608faecea | ||
|
|
4260082726 | ||
|
|
ca573dec91 | ||
|
|
3e252e1fd8 | ||
|
|
7adc153e57 | ||
|
|
ae105d9f83 | ||
|
|
87c895bd76 | ||
|
|
034b2b2ddd | ||
|
|
fb5a789f55 | ||
|
|
2b36a2f8cb | ||
|
|
d2a81ce907 | ||
|
|
1f0b0d7bd1 | ||
|
|
735527a0f0 | ||
|
|
4dc034dd5e | ||
|
|
4bfe4079cc | ||
|
|
66eff3a020 | ||
|
|
401c561238 | ||
|
|
606fe87ca0 | ||
|
|
f4775cc17c | ||
|
|
a2e941970a | ||
|
|
c2ed909818 | ||
|
|
c38c440e63 | ||
|
|
29b0785594 | ||
|
|
e15dcd41db | ||
|
|
6528461873 | ||
|
|
a8f5b5d4e4 | ||
|
|
be489744c9 | ||
|
|
cd0b7ed3d2 | ||
|
|
3ebc5c6b9d | ||
|
|
66ada600b7 | ||
|
|
4871d5df9d | ||
|
|
7088e6682b | ||
|
|
babe0adffb | ||
|
|
8f0a76ecef | ||
|
|
23607c303c | ||
|
|
884b7062c9 | ||
|
|
07650d424a | ||
|
|
218ec9c678 | ||
|
|
8b7c3308b3 | ||
|
|
ca9528fa4e | ||
|
|
aef625ba31 | ||
|
|
e5c8f2caec | ||
|
|
5c06305f85 | ||
|
|
428893d5c5 | ||
|
|
fc7277a542 | ||
|
|
c8c6b15285 | ||
|
|
0a987bdec9 | ||
|
|
ecc4fee84e | ||
|
|
4802ecfc29 | ||
|
|
436f415d9f | ||
|
|
164480834a | ||
|
|
68642e056c | ||
|
|
9033c6e1d4 | ||
|
|
89fc6feb5f | ||
|
|
80dc9568ce | ||
|
|
5774a7893f | ||
|
|
abd9ea9ec5 | ||
|
|
8799882f09 | ||
|
|
f85a4878de | ||
|
|
ae87213105 | ||
|
|
33bd86a2c7 | ||
|
|
2092ae22dc | ||
|
|
aa9317069a | ||
|
|
a31ea92649 | ||
|
|
b8f18bdec2 | ||
|
|
704977d5f6 | ||
|
|
0757c20d59 | ||
|
|
fa08847d6d | ||
|
|
f91f08628a | ||
|
|
9ebf6b06dd | ||
|
|
357d5e46a3 | ||
|
|
c0f5526801 | ||
|
|
861204e442 | ||
|
|
eb90b614ea | ||
|
|
d087ed2349 | ||
|
|
6ee7e75465 | ||
|
|
c2b80d7aba | ||
|
|
a95e8633cd | ||
|
|
e3adbbe000 | ||
|
|
eef360673b | ||
|
|
36e298c758 | ||
|
|
275157f27b | ||
|
|
e776deaa3f | ||
|
|
4fc8e9b45e | ||
|
|
fe41eec7c5 | ||
|
|
d1d1d22734 | ||
|
|
da8b76957a | ||
|
|
305f9fd1cf | ||
|
|
cd2a94ddb8 | ||
|
|
a2df4db504 | ||
|
|
b7740a4758 | ||
|
|
62c24de5c4 | ||
|
|
5ed3e67b76 | ||
|
|
c7f2314a15 | ||
|
|
420c7ebd67 | ||
|
|
b93b1a6eec | ||
|
|
7d52be6e99 | ||
|
|
9b1f0e394a | ||
|
|
1b0cb5d455 | ||
|
|
9b79d59d93 | ||
|
|
3e12316ea1 | ||
|
|
1b38c0111f | ||
|
|
5542393eb5 | ||
|
|
ad48bc0ee8 | ||
|
|
ba0e5d0b59 | ||
|
|
1c5ff88e3c | ||
|
|
bf7d4a550e | ||
|
|
324bc763fc | ||
|
|
f9fb2ca3a1 | ||
|
|
b5eac7c91b | ||
|
|
3c858ca0fd | ||
|
|
da9d634b83 | ||
|
|
128704400f | ||
|
|
a3594322bd | ||
|
|
fe4b3d5f1d | ||
|
|
da08da2b54 | ||
|
|
5deb5f79bd | ||
|
|
9f0d694f0a | ||
|
|
4153fb7d1e | ||
|
|
6994ec0f03 | ||
|
|
e1af60cfa9 | ||
|
|
7bcec61e6d | ||
|
|
dde287f05d | ||
|
|
27fc37e55c | ||
|
|
ad901760f6 | ||
|
|
973029865e | ||
|
|
52e4fedd16 | ||
|
|
b81ba49370 | ||
|
|
39a0f93f69 | ||
|
|
53cb83eacc | ||
|
|
b307d278b0 | ||
|
|
14348eba38 | ||
|
|
cead5b74ae | ||
|
|
2e2a945f7c | ||
|
|
0e3ae2b450 | ||
|
|
19e2df65ca | ||
|
|
565d715a66 | ||
|
|
abe6f55aa6 | ||
|
|
c278d0c5d4 | ||
|
|
a7e2c74158 | ||
|
|
d84900d601 | ||
|
|
fdda28d67f | ||
|
|
e00dccaa7c | ||
|
|
08c1a33362 | ||
|
|
31e3c8da30 | ||
|
|
62a6095ed7 | ||
|
|
4c2c0e2b95 | ||
|
|
d36e4937d4 | ||
|
|
fea48e8220 | ||
|
|
637a59136b | ||
|
|
385d275f59 | ||
|
|
e240ac1fa5 | ||
|
|
4f020c1ec7 | ||
|
|
237decb81e | ||
|
|
1c98cba36d | ||
|
|
21fb815adc | ||
|
|
6b729bd9b5 | ||
|
|
a7ee869c8e | ||
|
|
bbbe3cc92f | ||
|
|
c1770c8d90 | ||
|
|
71b7e68937 | ||
|
|
dbca88829a | ||
|
|
ebd365a156 | ||
|
|
698a20396c | ||
|
|
ffc2507362 | ||
|
|
038d6fe2c3 | ||
|
|
34c8baa744 | ||
|
|
52a8081d0f | ||
|
|
23813aa346 | ||
|
|
7d034a4b0b | ||
|
|
7e41f2ef35 | ||
|
|
f49dd31804 | ||
|
|
cf9e116388 | ||
|
|
2a8d6f37c4 | ||
|
|
d5930fd859 | ||
|
|
f1e0167e1b | ||
|
|
2d74c62054 | ||
|
|
9249f28e68 | ||
|
|
1273dbde76 | ||
|
|
966960c64b | ||
|
|
19e2919d5b | ||
|
|
1555b143a9 | ||
|
|
f1c2679137 | ||
|
|
49e9bd3ca6 | ||
|
|
6d9fe0410d | ||
|
|
11419365ca | ||
|
|
a9767ac29a | ||
|
|
efbf78ed00 | ||
|
|
1f0965fdf6 | ||
|
|
0ea2f48d94 | ||
|
|
547b351f40 | ||
|
|
c9f0166c3d | ||
|
|
0de106f23d | ||
|
|
854281417d | ||
|
|
4d35fde8ba | ||
|
|
754c9eff9e | ||
|
|
aca5a876d8 | ||
|
|
7a817319ba | ||
|
|
f01ed81472 | ||
|
|
942c755a5b | ||
|
|
392da50f2c | ||
|
|
dffaaf067d | ||
|
|
d663930d66 | ||
|
|
9291b6a489 | ||
|
|
53abc1171e | ||
|
|
7f95e11af1 | ||
|
|
256676cb9d | ||
|
|
6180c0dc69 | ||
|
|
e44ae4a0a0 | ||
|
|
8221e6a148 | ||
|
|
6603c48fd9 | ||
|
|
3665d7cab7 | ||
|
|
27da68dc4b | ||
|
|
5695d555e5 | ||
|
|
08a6ad8bd3 | ||
|
|
66a95fb130 | ||
|
|
6eca1dfb83 | ||
|
|
fb5e2ef671 | ||
|
|
3cb15f0097 | ||
|
|
7367932f2c | ||
|
|
fef854580d | ||
|
|
102a0a40a6 | ||
|
|
0515b650ca | ||
|
|
3c7e28c768 | ||
|
|
e528cf5692 | ||
|
|
dfe2eee0b9 | ||
|
|
60f42e342b | ||
|
|
958f738820 | ||
|
|
2d2989a425 | ||
|
|
3c3370b929 | ||
|
|
90b22196b1 | ||
|
|
e7b9c2d294 | ||
|
|
a3830d23e8 | ||
|
|
d83eb32b6e | ||
|
|
303c55dbba | ||
|
|
afde058a85 | ||
|
|
14397cab96 | ||
|
|
a78eec79a8 | ||
|
|
7ce4effc2d | ||
|
|
2674160acc | ||
|
|
f1c951c997 | ||
|
|
a17d810fea | ||
|
|
4b1cb76eaf | ||
|
|
6b89b2be5e | ||
|
|
92bfda9028 | ||
|
|
728d50461f | ||
|
|
3f92204de5 | ||
|
|
0e6c9177f0 | ||
|
|
3c0e674ee5 | ||
|
|
15c9052912 | ||
|
|
7061880104 | ||
|
|
6f12cde2e8 | ||
|
|
52d454276d | ||
|
|
81fb4ab435 | ||
|
|
af8bb1f0e8 | ||
|
|
5fd575a217 | ||
|
|
1ef5fd1a0f | ||
|
|
932de7dba7 | ||
|
|
84310336bd | ||
|
|
aba233c74a | ||
|
|
016e2b375d | ||
|
|
732b1ae0de | ||
|
|
ae0c0f957e | ||
|
|
4283046e76 | ||
|
|
0a126a15ba | ||
|
|
21e7190b72 | ||
|
|
9fcd049bdc | ||
|
|
bc31ea5eb7 | ||
|
|
35dd92f54e | ||
|
|
0a29f92384 | ||
|
|
a13414ddb9 | ||
|
|
3dd0566f48 | ||
|
|
8b3bc28120 | ||
|
|
8051d6ba48 | ||
|
|
43b49ef4c9 | ||
|
|
8ca51f1877 | ||
|
|
59c5f22dbd | ||
|
|
a92dc1ad73 | ||
|
|
fd72a00cfb | ||
|
|
e9d10d6f2f | ||
|
|
fa630a6cb5 | ||
|
|
135548a03b | ||
|
|
304c930f95 | ||
|
|
48991e22b1 | ||
|
|
132a375347 | ||
|
|
1392abe2c0 | ||
|
|
fde07fda55 | ||
|
|
b9c4928949 | ||
|
|
d2025d5ddf | ||
|
|
113ae1cfa5 | ||
|
|
a0f2039fd4 | ||
|
|
8442f80641 | ||
|
|
f10fafd038 | ||
|
|
c85f48a9e9 | ||
|
|
a1c487b29d | ||
|
|
62562f051c | ||
|
|
467edb6b32 | ||
|
|
1970641001 | ||
|
|
72b9384902 | ||
|
|
8600019079 | ||
|
|
49975a521b | ||
|
|
b289db6879 | ||
|
|
8ae6bf832a | ||
|
|
3efe7eb85d | ||
|
|
46635e1992 | ||
|
|
e8abe35bc5 | ||
|
|
7596881464 | ||
|
|
ba36f05182 | ||
|
|
be1874839e | ||
|
|
5996ea1ba3 | ||
|
|
9283537efc | ||
|
|
9dbdad324a | ||
|
|
39faf2e55c | ||
|
|
19b5253708 | ||
|
|
3f2b59e67f | ||
|
|
4bc1d5cd4a | ||
|
|
689349887d | ||
|
|
e7a1b5d40b | ||
|
|
8f997ee724 | ||
|
|
99bce50b4d | ||
|
|
db166c4f29 | ||
|
|
4a8a52a0c7 | ||
|
|
0762b337b8 | ||
|
|
a50fa9bcf4 | ||
|
|
1b0c5f8771 | ||
|
|
92be875a2f | ||
|
|
d5e4453f15 | ||
|
|
c9e43ed295 | ||
|
|
63610f04ec | ||
|
|
e0db4fce6e | ||
|
|
1f4b6f4a42 | ||
|
|
300ab191fe | ||
|
|
0315ae511b | ||
|
|
e336c4405d | ||
|
|
2a4d9c0ba6 | ||
|
|
4a29fa93c5 | ||
|
|
c5d14195d6 | ||
|
|
09d34f5843 | ||
|
|
7432610629 | ||
|
|
d50d555372 | ||
|
|
f0859291fc | ||
|
|
1eeba899f0 | ||
|
|
36653c10dc | ||
|
|
008ac68ecb | ||
|
|
36b8b0e6a1 | ||
|
|
42066e20ed | ||
|
|
57aa93eb84 | ||
|
|
c6c51bd319 | ||
|
|
2fa9c89246 | ||
|
|
840b326187 | ||
|
|
1fef7130fb | ||
|
|
2328dd1d58 | ||
|
|
dbba99eee5 | ||
|
|
f07e0bf967 | ||
|
|
c548f572df | ||
|
|
129f67c9f8 | ||
|
|
bc75a8e7b8 | ||
|
|
e4f16ae520 | ||
|
|
4df34c724e | ||
|
|
1960fc8606 | ||
|
|
aaae8a84f6 | ||
|
|
1dca3c17a4 | ||
|
|
dfd31722fc | ||
|
|
f5fd75f4fa | ||
|
|
24d1c2d63a | ||
|
|
da191d62cc | ||
|
|
e8bc6e564d | ||
|
|
95f3158bb4 | ||
|
|
f7cc5be173 | ||
|
|
adc078a5cb | ||
|
|
3f3ec9ef9a | ||
|
|
7d70060962 | ||
|
|
4507496d3d | ||
|
|
445dcc24df | ||
|
|
78205c9a13 | ||
|
|
3a0f7e0602 | ||
|
|
fab23ee595 | ||
|
|
d285c5a679 | ||
|
|
6d079b9349 | ||
|
|
c291f744e7 | ||
|
|
f0730f595f | ||
|
|
d175d06b35 | ||
|
|
119969634e | ||
|
|
b67c09a4c1 | ||
|
|
8a850ecc5b | ||
|
|
f752ed3927 | ||
|
|
866a7480bc | ||
|
|
55ae8404cd | ||
|
|
71e9d7c4af | ||
|
|
580556cab6 | ||
|
|
bcb6182be3 | ||
|
|
c865aaed6f | ||
|
|
37c23fa187 | ||
|
|
2eeb99e869 | ||
|
|
fd528edfed | ||
|
|
df6a645600 | ||
|
|
d1515f8f64 | ||
|
|
9fb85311b9 | ||
|
|
aed949e221 | ||
|
|
5b29f48bfd | ||
|
|
5bfb48b863 | ||
|
|
9536fafd53 | ||
|
|
4434d59b09 | ||
|
|
060fe39f2e | ||
|
|
a5b14e8d68 | ||
|
|
165ad229e2 | ||
|
|
beb3117bfc | ||
|
|
e1d462aa42 | ||
|
|
bd15ef7768 | ||
|
|
646f669c14 | ||
|
|
682eb8d6e5 | ||
|
|
00b0a21c78 | ||
|
|
c7a5d295ec | ||
|
|
bcb055ed05 | ||
|
|
983b1e3656 | ||
|
|
3c4dbe2558 | ||
|
|
80b931ca9e | ||
|
|
200a234469 | ||
|
|
24ef877bfe | ||
|
|
602244b53f | ||
|
|
97b2a6eea0 | ||
|
|
e20d09cfee | ||
|
|
7e2ae8e87c | ||
|
|
a868766a65 | ||
|
|
7f1c505303 | ||
|
|
f679746e63 | ||
|
|
40b75c6ac8 | ||
|
|
c5fc4db980 | ||
|
|
f6b88518a2 | ||
|
|
3fecb777e8 | ||
|
|
3d2914da94 | ||
|
|
52e1ce5237 | ||
|
|
b6b5875786 | ||
|
|
97782d29cc | ||
|
|
0c5930d5cf | ||
|
|
836a3659b6 | ||
|
|
2e6e320bd9 | ||
|
|
c26597cf02 | ||
|
|
9a0cc4a717 | ||
|
|
81aa94c8df | ||
|
|
ff30d6d23a | ||
|
|
30769b5992 | ||
|
|
c6d2e6cda3 | ||
|
|
3a0c29988e | ||
|
|
85f1c3816b | ||
|
|
7040bb01f4 | ||
|
|
71f1304606 | ||
|
|
90d242b784 | ||
|
|
b520b6dc13 | ||
|
|
82b2b0b334 | ||
|
|
6463b84952 | ||
|
|
34cedbdadc | ||
|
|
44cf25b447 | ||
|
|
94c7638c96 | ||
|
|
6cf0727bd5 | ||
|
|
561301bd28 | ||
|
|
9039be8e39 | ||
|
|
c42292d546 | ||
|
|
e21d17f6b8 | ||
|
|
ca2eacdd82 | ||
|
|
af9f2794be | ||
|
|
ff84149623 | ||
|
|
99aea3ed60 | ||
|
|
9528db700a | ||
|
|
106187e2f4 | ||
|
|
e412aa9a3d | ||
|
|
0a8fa40b6b | ||
|
|
8a84fa5cdd | ||
|
|
1e8fb61abf | ||
|
|
ee4e90deb5 | ||
|
|
e2124bac5a | ||
|
|
c1b95547d7 | ||
|
|
28025cfb44 | ||
|
|
7c978f6c1c | ||
|
|
470936476e | ||
|
|
9c418e110f | ||
|
|
499cb76492 | ||
|
|
bb00327e81 | ||
|
|
e79dec3c2b | ||
|
|
ab23882c27 | ||
|
|
a22602f6d1 | ||
|
|
c1ba1014c3 | ||
|
|
f393f58bce | ||
|
|
b06d1fd293 | ||
|
|
ac32e76eec | ||
|
|
420fe0df0d | ||
|
|
b035030867 | ||
|
|
a641fec3ae | ||
|
|
13c3624025 | ||
|
|
16728ab51c | ||
|
|
247eea1a0c | ||
|
|
bf454816ea | ||
|
|
36028632ac | ||
|
|
0e386d33b0 | ||
|
|
4f9d8915fb | ||
|
|
0d94e4290b | ||
|
|
a1426bc81b | ||
|
|
d98d36d97b | ||
|
|
bf930d2ae0 | ||
|
|
631730bf3a | ||
|
|
8d34a4c5a1 | ||
|
|
c8f50fc117 | ||
|
|
cfdb7b32fc | ||
|
|
cc833f0b73 | ||
|
|
47282afa22 | ||
|
|
417640cfbe | ||
|
|
8df1690d5d | ||
|
|
461d1bcd5b | ||
|
|
8b5e164291 | ||
|
|
2644f56755 | ||
|
|
437e6baeba | ||
|
|
6788d37b6f | ||
|
|
b90550d6ba | ||
|
|
c068deb47e | ||
|
|
2ce5b28048 | ||
|
|
57f1751309 | ||
|
|
2d8dc36f28 | ||
|
|
077a717525 | ||
|
|
8795493462 | ||
|
|
af532bae8f | ||
|
|
c07b3e2d3c | ||
|
|
6b1a9fa837 | ||
|
|
24888dfad5 | ||
|
|
4d4c8638ca | ||
|
|
9fd983abfb | ||
|
|
637839ee14 | ||
|
|
9b2578665f | ||
|
|
ee05e109c8 | ||
|
|
a905e32cde | ||
|
|
fb5cec0d38 | ||
|
|
1a4f490fb5 | ||
|
|
4518c2c4c0 | ||
|
|
17771ccecd | ||
|
|
f3c2a3c025 | ||
|
|
2aa919b444 | ||
|
|
408987ee30 | ||
|
|
fe04ad9940 | ||
|
|
1b03e750a2 | ||
|
|
3ff781139e | ||
|
|
2ea3ba492e | ||
|
|
89d3228077 | ||
|
|
7946f5ee81 | ||
|
|
44f62eac9a | ||
|
|
47725e57b0 | ||
|
|
aac2aaa999 | ||
|
|
6ac1160bf2 | ||
|
|
70fae41042 | ||
|
|
07f5bfe3dc | ||
|
|
93a88a22b9 | ||
|
|
792faa1176 | ||
|
|
66900d594f | ||
|
|
9555f3c853 | ||
|
|
9ed2fa734a | ||
|
|
db83508920 | ||
|
|
d72a6585d4 | ||
|
|
74fc8c9cf7 | ||
|
|
f3440f3c01 | ||
|
|
b01799c606 | ||
|
|
3cbb4e3f43 | ||
|
|
36299acbfb | ||
|
|
acc20af2d9 | ||
|
|
077ce5b521 | ||
|
|
1efe82dda2 | ||
|
|
f283618209 | ||
|
|
fe6baf8dba | ||
|
|
b742dc51fb | ||
|
|
c8ea649afc | ||
|
|
a27e94f694 | ||
|
|
25f9e7829f | ||
|
|
85be7acab2 | ||
|
|
36c23227e5 | ||
|
|
0b6f68e190 | ||
|
|
6c90fc2764 | ||
|
|
4fd1e55ae8 | ||
|
|
9672f7e3da | ||
|
|
50d29f8ef0 | ||
|
|
6ffa00026e | ||
|
|
c4677505ac | ||
|
|
bac6d7cf3c | ||
|
|
85ea91e0e3 | ||
|
|
09b09086ce | ||
|
|
8fbfa86a7f | ||
|
|
6224e942dc | ||
|
|
ab5edbdd41 | ||
|
|
7825d10f18 | ||
|
|
8c1988e480 | ||
|
|
8403b811d8 | ||
|
|
1c797505ae | ||
|
|
466086b509 | ||
|
|
221f7247e6 | ||
|
|
624bc88f74 | ||
|
|
d6ca4458e4 | ||
|
|
1fe3e60468 | ||
|
|
aa65b2b97c | ||
|
|
6dea2475c7 | ||
|
|
a666cb00eb | ||
|
|
f0fac9165c | ||
|
|
e51eb8a9c1 | ||
|
|
4822984e34 | ||
|
|
4a558a7f65 | ||
|
|
48d4935c7d | ||
|
|
506accfe9b | ||
|
|
c15aba47f5 | ||
|
|
fdafa8adf6 | ||
|
|
23e15581f3 | ||
|
|
1621f866a8 | ||
|
|
f03fe33b1f | ||
|
|
6ec2a5ea35 | ||
|
|
0ae4d323f7 | ||
|
|
930404e482 | ||
|
|
92257afdab | ||
|
|
300ff09a47 | ||
|
|
14dd1103eb | ||
|
|
ff07eb1de0 | ||
|
|
b81f45bf47 | ||
|
|
eb3232e049 | ||
|
|
752f653f82 | ||
|
|
ed90dbe7b7 | ||
|
|
e1e0f2944b | ||
|
|
2269f15b66 | ||
|
|
5c0a53e02a | ||
|
|
8810439ffc | ||
|
|
9d61270937 | ||
|
|
b602a9d15d | ||
|
|
bb0ab03ad9 | ||
|
|
d674dcaeef | ||
|
|
a738ddb917 | ||
|
|
935c92b507 | ||
|
|
10d1a2d8e4 | ||
|
|
cf6d64646a | ||
|
|
c570e8b6fe | ||
|
|
849b9e0c80 | ||
|
|
8f8aa31304 | ||
|
|
a1fe79c876 | ||
|
|
7c9654a541 | ||
|
|
a86df7cdbf | ||
|
|
3d5cdd659b | ||
|
|
7a2a5d3846 | ||
|
|
62fb0acb3c | ||
|
|
c4dfe8a723 | ||
|
|
4af4df9288 | ||
|
|
25b0e18ceb | ||
|
|
c4aec8dfa6 | ||
|
|
4056a3da43 | ||
|
|
2027f8052b | ||
|
|
96bb293c1f | ||
|
|
fd73b28d66 | ||
|
|
aafa698776 | ||
|
|
a99d31535c | ||
|
|
fda8791d5a | ||
|
|
9e2ac31a08 | ||
|
|
758b32a61c | ||
|
|
62b392e555 | ||
|
|
a4c99fd361 | ||
|
|
8823656d70 | ||
|
|
b82f5da112 | ||
|
|
729f51b779 | ||
|
|
1c1171e8a7 | ||
|
|
44df319ff6 | ||
|
|
84dec337f0 | ||
|
|
2e60a9d43c | ||
|
|
4474766526 | ||
|
|
ddc1d8117d | ||
|
|
609bae4f1a | ||
|
|
ff16a4334f | ||
|
|
739e308c0e | ||
|
|
6b29f57e1d | ||
|
|
21981829fd | ||
|
|
b6e00a3107 | ||
|
|
8b8b137cad | ||
|
|
1ba1286df0 | ||
|
|
0417a82f83 | ||
|
|
4cc01a2152 | ||
|
|
c7d434a091 | ||
|
|
3e1e704a7f | ||
|
|
12a9dcaa76 | ||
|
|
fc2dd148c5 | ||
|
|
ede6f36913 | ||
|
|
f85143fb7b | ||
|
|
bbf3043fc3 | ||
|
|
bbd73d361a | ||
|
|
7d44c87aff | ||
|
|
7e81041b87 | ||
|
|
e30698459b | ||
|
|
42399469a7 | ||
|
|
8ccc7bb734 | ||
|
|
38a7c222a8 | ||
|
|
9ea21606e5 | ||
|
|
c809119d57 | ||
|
|
b8ca009e69 | ||
|
|
1e37d7da7d | ||
|
|
9df90e4edc | ||
|
|
4576e93deb | ||
|
|
ea5e0b28da | ||
|
|
19c8a01969 | ||
|
|
ebab88e7aa | ||
|
|
b4248acd9a | ||
|
|
c303174f0b | ||
|
|
91cf6465df | ||
|
|
426d2aab09 | ||
|
|
8c44e558a8 | ||
|
|
6a08e08d7c | ||
|
|
2796ad12fe | ||
|
|
45d40297bf | ||
|
|
57ac37c210 | ||
|
|
8eee0b809c | ||
|
|
5387054000 | ||
|
|
e08f072d95 | ||
|
|
44db2ca02a | ||
|
|
dc8564d18e | ||
|
|
3b8bc9fdab | ||
|
|
c5d65fa030 | ||
|
|
0252b08c8f | ||
|
|
8f29b7a91f | ||
|
|
19e1bbdc1c | ||
|
|
cd2baf105f | ||
|
|
3366acde58 | ||
|
|
775f6eff0b | ||
|
|
52d501dae8 | ||
|
|
eb24baf2c1 | ||
|
|
7fd0ef51b5 | ||
|
|
173acc5226 | ||
|
|
6643b825ee | ||
|
|
a56f20584f | ||
|
|
22664bea62 | ||
|
|
f80bf65076 | ||
|
|
28634c59c8 | ||
|
|
993377a40b | ||
|
|
9633733bc4 | ||
|
|
6b4893b854 | ||
|
|
09f7c35dac | ||
|
|
0fc4169b0b | ||
|
|
78746be0f5 | ||
|
|
2287a550d7 | ||
|
|
d6eb6d3318 | ||
|
|
db2d36eaa1 | ||
|
|
151d20341e | ||
|
|
2c51bc17f1 | ||
|
|
0448ad49ed | ||
|
|
2d4129f8f7 | ||
|
|
c42aa7c806 | ||
|
|
debeb8dfd8 | ||
|
|
2227e1dd4b | ||
|
|
1d7e73c162 | ||
|
|
3f451856a0 | ||
|
|
fdd0483c9f | ||
|
|
c1a49a52e8 | ||
|
|
d8394392c9 | ||
|
|
eb7a037f94 | ||
|
|
eb905aab86 | ||
|
|
55892097d7 | ||
|
|
6bfcda9fdc | ||
|
|
02dcbb9a52 | ||
|
|
0c6a6e4173 | ||
|
|
e044251df4 | ||
|
|
26d27a3f6a | ||
|
|
c5b9fccedb | ||
|
|
0153e5212c | ||
|
|
92835a5270 | ||
|
|
5f41c78305 | ||
|
|
1726b89dea | ||
|
|
2506e69cdc | ||
|
|
88bc30bbea | ||
|
|
2835d1bd87 | ||
|
|
e590896f01 | ||
|
|
dfb0836446 | ||
|
|
88fdd1f562 | ||
|
|
ae07c7934e | ||
|
|
9515a060ab | ||
|
|
c550416c9d | ||
|
|
712883373a | ||
|
|
50930ee609 | ||
|
|
78bffad99f | ||
|
|
b3760a961d | ||
|
|
813d92ce32 | ||
|
|
b02570e679 | ||
|
|
b7d1979d0d | ||
|
|
6e6846835a | ||
|
|
d899935b56 | ||
|
|
2a07c063ab | ||
|
|
3ab9d77930 | ||
|
|
5537507646 | ||
|
|
215dd03751 | ||
|
|
3fe73ba198 | ||
|
|
6bc7edea67 | ||
|
|
c44e69c396 | ||
|
|
f6ad697755 | ||
|
|
2abca93333 | ||
|
|
788e7c40e9 | ||
|
|
dca43f3e57 | ||
|
|
f95a98d3ee | ||
|
|
11fe3dc492 | ||
|
|
4277244150 | ||
|
|
8458bcf10e | ||
|
|
6c8c7751fd | ||
|
|
d6096d04d9 | ||
|
|
bffe6327a0 | ||
|
|
28845d6f33 | ||
|
|
6ec7da9071 | ||
|
|
5dbe564afb | ||
|
|
4794791167 | ||
|
|
7b2ae2c457 | ||
|
|
f0093c5e4f | ||
|
|
96117216ee | ||
|
|
9982557909 | ||
|
|
530331f9ee | ||
|
|
23018abdf6 | ||
|
|
23b72620a1 | ||
|
|
a80c21d77f | ||
|
|
765307ddef | ||
|
|
9a859629bc | ||
|
|
cc7b203f93 | ||
|
|
8744eadca0 | ||
|
|
76eaee5b1a | ||
|
|
7adde2a880 | ||
|
|
c02eced029 | ||
|
|
ad5ca50273 | ||
|
|
767756ba9b | ||
|
|
c3cf5ff84c | ||
|
|
a82c790855 | ||
|
|
4b22e3e0a8 | ||
|
|
2039a143ac | ||
|
|
84473dc10d | ||
|
|
5695da1d86 | ||
|
|
30583cce21 | ||
|
|
27c7c0438f | ||
|
|
b67d5eec3d | ||
|
|
7c2ea6288c | ||
|
|
9a1d71face | ||
|
|
8e346bf676 | ||
|
|
53a00a8d76 | ||
|
|
ea1e556197 | ||
|
|
402d75bfe0 | ||
|
|
a444b61edf | ||
|
|
ce41af14db | ||
|
|
d52d606088 | ||
|
|
475311f63a | ||
|
|
5509089c49 | ||
|
|
3698220b8f | ||
|
|
b22dba00a2 | ||
|
|
3d8ec5531c | ||
|
|
7df0ae0ba3 | ||
|
|
05d37cc6c6 | ||
|
|
df03f783f8 | ||
|
|
cd9263711f | ||
|
|
48c3372c33 | ||
|
|
5d1ff97bf3 | ||
|
|
1decfe8063 | ||
|
|
a3d0ffb7de | ||
|
|
59a54f8683 | ||
|
|
83e2bd6ade | ||
|
|
a59aca10ec | ||
|
|
9ac6e65087 | ||
|
|
deb8e117ad | ||
|
|
9c3cae5eca | ||
|
|
1fbbeba5bc | ||
|
|
8317972078 | ||
|
|
0a9947dbb9 | ||
|
|
51521926e7 | ||
|
|
8e08ac2ce1 | ||
|
|
fec82d127e | ||
|
|
ceb0770ea0 | ||
|
|
34eadebe00 | ||
|
|
e7f614cdf3 | ||
|
|
18507f79b1 | ||
|
|
ee1c7dbf03 | ||
|
|
868af95ff2 | ||
|
|
01f59d39e0 | ||
|
|
0226a5603d | ||
|
|
1629be3788 | ||
|
|
480bc630da | ||
|
|
165cc279de | ||
|
|
2ec5a2acff | ||
|
|
6914e83dde | ||
|
|
263762c0bc | ||
|
|
f8b8a574a6 | ||
|
|
79c80b351d | ||
|
|
2c86fb17fc | ||
|
|
e205ffafdf | ||
|
|
2680b415c6 | ||
|
|
62d8b35545 | ||
|
|
2b578efdd6 | ||
|
|
a7f37df34d | ||
|
|
3edb119422 | ||
|
|
07d4d5051a | ||
|
|
0b8e5a75f1 | ||
|
|
f263c73df7 | ||
|
|
f89f201764 | ||
|
|
9f8dcdf8ea | ||
|
|
f3baf31dcd | ||
|
|
a9400785ca | ||
|
|
7c76ad2088 | ||
|
|
6a5839d8cd | ||
|
|
744f39623f | ||
|
|
97e57c74e4 | ||
|
|
ff0d6b658b | ||
|
|
1c946a438d | ||
|
|
9b047a1927 | ||
|
|
3fc6141d57 | ||
|
|
daf7e2313b | ||
|
|
75642d785e | ||
|
|
2621b5c047 | ||
|
|
57cb9a1d0b | ||
|
|
6318ae046c | ||
|
|
57a41cde9d | ||
|
|
ac86b7a954 | ||
|
|
0bc500e34f | ||
|
|
a60e065e43 | ||
|
|
5563b6a786 | ||
|
|
0345c52aba | ||
|
|
05c858df9e | ||
|
|
0b4ef21762 | ||
|
|
64a58921a8 | ||
|
|
b96098b909 | ||
|
|
b1dbb2c408 | ||
|
|
3b1a08c67e | ||
|
|
07d37e133f | ||
|
|
161eb8bef9 | ||
|
|
eb518c673c | ||
|
|
6f32a0d6de | ||
|
|
b0684ce29c | ||
|
|
b2c8a4d8ef | ||
|
|
d09ac5bcc6 | ||
|
|
c25c3e9daa | ||
|
|
c1cb5c36a1 | ||
|
|
20118f941e | ||
|
|
f40eee4577 | ||
|
|
bee05afc87 | ||
|
|
efdc533849 | ||
|
|
1f7c6d59c1 | ||
|
|
981622f414 | ||
|
|
347c8a8716 | ||
|
|
4542564709 | ||
|
|
cb889ce06d | ||
|
|
db54a305b0 | ||
|
|
8ccf17543a | ||
|
|
72e99885aa | ||
|
|
18d2a9cab6 | ||
|
|
9c57702afc | ||
|
|
b708eb94d2 | ||
|
|
82c5531d04 | ||
|
|
e6f49b2d3b | ||
|
|
8ac97e2c8f | ||
|
|
db7174b0f3 | ||
|
|
a47911048c | ||
|
|
5a2bdbf966 | ||
|
|
aa562228ef | ||
|
|
98a70aedf2 | ||
|
|
9b9da5664b | ||
|
|
2f2314d2f8 | ||
|
|
715ebf0747 | ||
|
|
bb0443b967 | ||
|
|
2cf0b528f0 | ||
|
|
6a95d481f0 | ||
|
|
d281b21832 | ||
|
|
1d5cf43e68 | ||
|
|
6d6b2300a8 | ||
|
|
640ee55772 | ||
|
|
7ec12f487b | ||
|
|
63b42d64b1 | ||
|
|
667506172a | ||
|
|
518bb74fbf | ||
|
|
9038538718 | ||
|
|
5234f50453 | ||
|
|
6eabf73ece | ||
|
|
651d01564d | ||
|
|
52cdec8d3c | ||
|
|
998c9bdeb7 | ||
|
|
318ee89e89 | ||
|
|
031d7a1f18 | ||
|
|
7424a226c9 | ||
|
|
30a1997fd9 | ||
|
|
778ea0b720 | ||
|
|
353517f9c6 | ||
|
|
e651b2ee13 | ||
|
|
018b3a876f | ||
|
|
1b9586011e | ||
|
|
cb856ce2bb | ||
|
|
8d3c1c9f9e | ||
|
|
1ec0f67b29 | ||
|
|
093491c5b4 | ||
|
|
56191d0cd9 | ||
|
|
7342268eb8 | ||
|
|
3a09cbf42b | ||
|
|
b268368e3d | ||
|
|
59c8211c41 | ||
|
|
14560fff0a | ||
|
|
adf3172ebb | ||
|
|
4ead9cbf6a | ||
|
|
0863dc785f | ||
|
|
342538358d | ||
|
|
a8b79055ef | ||
|
|
ec3be4c36a | ||
|
|
0a2ef0e041 | ||
|
|
e7b623ea16 | ||
|
|
87777017a0 | ||
|
|
bf2c7a18d1 | ||
|
|
b5505bcd87 | ||
|
|
bdf9fbae71 | ||
|
|
04c1afc9ce | ||
|
|
458c51bdaa | ||
|
|
90a736ba43 | ||
|
|
661ce4fc1d | ||
|
|
b764f1c861 | ||
|
|
182949d8d2 | ||
|
|
a879bdeb47 | ||
|
|
9c66a4ef4e | ||
|
|
d2d75b8e41 | ||
|
|
e36c15f770 | ||
|
|
8dc6da2b7a | ||
|
|
d3ae252740 | ||
|
|
29f48bcba6 | ||
|
|
e6fe5adca7 | ||
|
|
82a96ec91d | ||
|
|
db02cbb575 | ||
|
|
749dd20704 | ||
|
|
b9db6040f4 | ||
|
|
c9628c0f75 | ||
|
|
979af88a40 | ||
|
|
98b4cd330f | ||
|
|
5ab390c3db | ||
|
|
71eaf9966f | ||
|
|
9653d07ae2 | ||
|
|
f1663d0fbf | ||
|
|
38cb2201a9 | ||
|
|
fa04bea64b | ||
|
|
2bc66af55d | ||
|
|
db5892d0ae | ||
|
|
59c7c1e302 | ||
|
|
48f63ec761 | ||
|
|
4051e34e20 | ||
|
|
428bd43d60 | ||
|
|
67415ff715 | ||
|
|
fbc494abc9 | ||
|
|
0816af3cf1 | ||
|
|
bb575fff5b | ||
|
|
cbe632839c | ||
|
|
7c972758af | ||
|
|
236f66f56f | ||
|
|
a485df2f79 | ||
|
|
54b9154457 | ||
|
|
37aabcee4f | ||
|
|
b2d18560be | ||
|
|
1429aa1edc | ||
|
|
5d4f942d46 | ||
|
|
30ea7e854d | ||
|
|
907f82338e | ||
|
|
dcb0160b64 | ||
|
|
fccd7fa438 | ||
|
|
c39711a87e | ||
|
|
a8de003cf0 | ||
|
|
6db54fc3b5 | ||
|
|
d058536011 | ||
|
|
02ad4ba98d | ||
|
|
a68a76112c | ||
|
|
975c545081 | ||
|
|
fcfee9082b | ||
|
|
e6ad14f8d4 | ||
|
|
1670f15732 | ||
|
|
5cd696792b | ||
|
|
fbc399f5fa | ||
|
|
3d6413ae05 | ||
|
|
97120a6b04 | ||
|
|
226162ee57 | ||
|
|
a888ec265f | ||
|
|
6fb7555f01 | ||
|
|
a8d0e25866 | ||
|
|
970f7fe69b | ||
|
|
c507df902e | ||
|
|
7fa5ef8165 | ||
|
|
92cb768c4b | ||
|
|
8ec406c2e0 | ||
|
|
9473c108f0 | ||
|
|
14c43d9f7e | ||
|
|
ce9a03a5a8 | ||
|
|
04e8b14fc4 | ||
|
|
43b747676c | ||
|
|
bd40cf9947 | ||
|
|
203b31d81f | ||
|
|
0430fb2772 | ||
|
|
d3746d6859 | ||
|
|
d8dfa89f87 | ||
|
|
cbdb90d06b | ||
|
|
63e040ea79 | ||
|
|
fd1a0f3b0a | ||
|
|
abaf8a676c | ||
|
|
0b96fc4701 | ||
|
|
400e210d37 | ||
|
|
ea0c697ad3 | ||
|
|
edf8c32a0f | ||
|
|
ccef5da7d9 | ||
|
|
ddf213aec4 | ||
|
|
8bd9237951 | ||
|
|
ae488312a1 | ||
|
|
1ed45656e4 | ||
|
|
07edcc5f94 | ||
|
|
1783059fd4 | ||
|
|
aab766e8ff | ||
|
|
158514f334 | ||
|
|
77d29c3728 | ||
|
|
3c3383ac03 | ||
|
|
6e46240fd7 | ||
|
|
d01c46bfee | ||
|
|
1e5007ec8b | ||
|
|
deed95e9a9 | ||
|
|
082323511a | ||
|
|
c07224cab5 | ||
|
|
1604a96f41 | ||
|
|
50963f00c0 | ||
|
|
699db93b18 | ||
|
|
85e467581c | ||
|
|
42e4588e9c | ||
|
|
93c194cff7 | ||
|
|
00450dc048 | ||
|
|
c319fd5862 | ||
|
|
5048b5b585 | ||
|
|
e7f24084af | ||
|
|
c57b9b4fa3 | ||
|
|
ac5b7a4469 | ||
|
|
884faa0e27 | ||
|
|
50b4b7bb92 | ||
|
|
cf259ace47 | ||
|
|
270389a18c | ||
|
|
a340eea769 | ||
|
|
22589e7103 | ||
|
|
2b6423d3b7 | ||
|
|
50bf193fd1 | ||
|
|
c2ba059ced | ||
|
|
856ed0c765 | ||
|
|
a73681ce8b | ||
|
|
1426ed952b | ||
|
|
d6bf6eb0a0 | ||
|
|
97b24079f7 | ||
|
|
707f84839e | ||
|
|
643d2f3fad | ||
|
|
92660e037d | ||
|
|
2e6a0411fb | ||
|
|
5d57a5fabb | ||
|
|
cb90ad803b | ||
|
|
937e8ce1ed | ||
|
|
c1976d5b13 | ||
|
|
8070e88564 | ||
|
|
15c0c691ff | ||
|
|
f68912b466 | ||
|
|
dfa4e20a8f | ||
|
|
ee1a194305 | ||
|
|
0fa88855e5 | ||
|
|
eda3d5c143 | ||
|
|
b450efe5c2 | ||
|
|
ca76626d55 | ||
|
|
ed887953b6 | ||
|
|
04debe3ea3 | ||
|
|
4312096dd2 | ||
|
|
94b079fa7b | ||
|
|
0373d86349 | ||
|
|
0f5c290785 | ||
|
|
c79f43bb27 | ||
|
|
184ad3bc4e | ||
|
|
aa0a4ae3e9 | ||
|
|
ff9c4b407f | ||
|
|
c3b01d477e | ||
|
|
3c0641745b | ||
|
|
7186a0c41b | ||
|
|
4c3bc7450e | ||
|
|
02f04e2d33 | ||
|
|
97b6e4c672 |
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"unused": true,
|
||||
"globalstrict": true,
|
||||
"predef": [ "angular", "$" ],
|
||||
"esnext": true
|
||||
}
|
||||
2
LICENSE
@@ -1,5 +1,5 @@
|
||||
The Cloudron Subscription license
|
||||
Copyright (c) 2018 Cloudron UG
|
||||
Copyright (c) 2020 Cloudron UG
|
||||
|
||||
With regard to the Cloudron Software:
|
||||
|
||||
|
||||
66
README.md
@@ -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.
|
||||
|
||||
|
||||
285
gulpfile.js
@@ -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
27
package.json
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
85
src/3rdparty/angular-ui-notification.css
vendored
Normal 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;
|
||||
}
|
||||
8
src/3rdparty/angular-ui-notification.min.css
vendored
@@ -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}
|
||||
2337
src/3rdparty/css/font-awesome.css
vendored
4
src/3rdparty/css/font-awesome.min.css
vendored
BIN
src/3rdparty/fonts/FontAwesome.otf
vendored
BIN
src/3rdparty/fonts/fontawesome-webfont.eot
vendored
2671
src/3rdparty/fonts/fontawesome-webfont.svg
vendored
|
Before Width: | Height: | Size: 434 KiB |
BIN
src/3rdparty/fonts/fontawesome-webfont.ttf
vendored
BIN
src/3rdparty/fonts/fontawesome-webfont.woff
vendored
BIN
src/3rdparty/fonts/fontawesome-webfont.woff2
vendored
10
src/3rdparty/js/Chart.min.js
vendored
271
src/3rdparty/js/angular-ui-notification.js
vendored
Normal 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>");}]);
|
||||
@@ -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
639
src/3rdparty/js/contextMenu.js
vendored
Normal 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 = '<';
|
||||
$boldStyle.float = 'left';
|
||||
} else {
|
||||
$arrow = '>';
|
||||
$boldStyle.float = 'right';
|
||||
}
|
||||
|
||||
var $bold = $('<strong style="font-family:monospace;font-weight:bold;float:right;">' + $arrow + '</strong>');
|
||||
$bold.css($boldStyle);
|
||||
$a.css('cursor', 'default');
|
||||
$a.append($bold);
|
||||
}
|
||||
$a.append(pText);
|
||||
});
|
||||
|
||||
optionText = $a;
|
||||
}
|
||||
|
||||
$li.append(optionText);
|
||||
|
||||
return optionText;
|
||||
};
|
||||
|
||||
/**
|
||||
* Process each individual item
|
||||
*
|
||||
* Properties of params:
|
||||
* - $scope
|
||||
* - event
|
||||
* - modelValue
|
||||
* - level
|
||||
* - item
|
||||
* - $ul
|
||||
* - $li
|
||||
* - $promises
|
||||
*/
|
||||
function processItem(params) {
|
||||
var nestedMenu = extractNestedMenu(params);
|
||||
|
||||
// if html property is not defined, fallback to text, otherwise use default text
|
||||
// if first item in the item array is a function then invoke .call()
|
||||
// if first item is a string, then text should be the string.
|
||||
|
||||
var text = DEFAULT_ITEM_TEXT;
|
||||
var currItemParam = angular.extend({}, params);
|
||||
var item = params.item;
|
||||
var enabled = item.enabled === undefined ? item[2] : item.enabled;
|
||||
|
||||
currItemParam.nestedMenu = nestedMenu;
|
||||
currItemParam.enabled = resolveBoolOrFunc(enabled, params);
|
||||
currItemParam.text = createAndAddOptionText(currItemParam);
|
||||
|
||||
registerCurrentItemEvents(currItemParam);
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
* Registers the appropriate mouse events for options if the item is enabled.
|
||||
* Otherwise, it ensures that clicks to the item do not propagate.
|
||||
*/
|
||||
function registerCurrentItemEvents (params) {
|
||||
// Destructuring:
|
||||
var item = params.item;
|
||||
var $ul = params.$ul;
|
||||
var $li = params.$li;
|
||||
var $scope = params.$scope;
|
||||
var modelValue = params.modelValue;
|
||||
var level = params.level;
|
||||
var event = params.event;
|
||||
var text = params.text;
|
||||
var nestedMenu = params.nestedMenu;
|
||||
var enabled = params.enabled;
|
||||
var orientation = String(params.orientation).toLowerCase();
|
||||
var customClass = params.customClass;
|
||||
|
||||
if (enabled) {
|
||||
var openNestedMenu = function ($event) {
|
||||
removeContextMenus(level + 1);
|
||||
/*
|
||||
* The object here needs to be constructed and filled with data
|
||||
* on an "as needed" basis. Copying the data from event directly
|
||||
* or cloning the event results in unpredictable behavior.
|
||||
*/
|
||||
/// adding the original event in the object to use the attributes of the mouse over event in the promises
|
||||
var ev = {
|
||||
pageX: orientation === 'left' ? event.pageX - $ul[0].offsetWidth + 1 : event.pageX + $ul[0].offsetWidth - 1,
|
||||
pageY: $ul[0].offsetTop + $li[0].offsetTop - 3,
|
||||
// eslint-disable-next-line angular/window-service
|
||||
view: event.view || window,
|
||||
target: event.target,
|
||||
event: $event
|
||||
};
|
||||
|
||||
/*
|
||||
* At this point, nestedMenu can only either be an Array or a promise.
|
||||
* Regardless, passing them to `when` makes the implementation singular.
|
||||
*/
|
||||
$q.when(nestedMenu).then(function(promisedNestedMenu) {
|
||||
if (angular.isFunction(promisedNestedMenu)) {
|
||||
// support for dynamic subitems
|
||||
promisedNestedMenu = promisedNestedMenu.call($scope, $scope, event, modelValue, text, $li);
|
||||
}
|
||||
var nestedParam = {
|
||||
$scope : $scope,
|
||||
event : ev,
|
||||
options : promisedNestedMenu,
|
||||
modelValue : modelValue,
|
||||
level : level + 1,
|
||||
orientation: orientation,
|
||||
customClass: customClass
|
||||
};
|
||||
renderContextMenu(nestedParam);
|
||||
});
|
||||
};
|
||||
|
||||
$li.on('click', function ($event) {
|
||||
if($event.which == 1) {
|
||||
$event.preventDefault();
|
||||
$scope.$apply(function () {
|
||||
|
||||
var cleanupFunction = function () {
|
||||
$(event.currentTarget).removeClass('context');
|
||||
removeAllContextMenus();
|
||||
};
|
||||
var clickFunction = angular.isFunction(item.click)
|
||||
? item.click
|
||||
: (angular.isFunction(item[1])
|
||||
? item[1]
|
||||
: null);
|
||||
|
||||
if (clickFunction) {
|
||||
var res = clickFunction.call($scope, $scope, event, modelValue, text, $li);
|
||||
if(res === undefined || res) {
|
||||
cleanupFunction();
|
||||
}
|
||||
} else {
|
||||
cleanupFunction();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$li.on('mouseover', function ($event) {
|
||||
$scope.$apply(function () {
|
||||
if (nestedMenu) {
|
||||
openNestedMenu($event);
|
||||
} else {
|
||||
removeContextMenus(level + 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
setElementDisabled($li);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param params - an object containing the `item` parameter
|
||||
* @returns an Array or a Promise containing the children,
|
||||
* or null if the option has no submenu
|
||||
*/
|
||||
function extractNestedMenu(params) {
|
||||
// Destructuring:
|
||||
var item = params.item;
|
||||
|
||||
// New implementation:
|
||||
if (item.children) {
|
||||
if (angular.isFunction(item.children)) {
|
||||
// Expects a function that returns a Promise or an Array
|
||||
return item.children();
|
||||
} else if (angular.isFunction(item.children.then) || angular.isArray(item.children)) {
|
||||
// Returns the promise
|
||||
// OR, returns the actual array
|
||||
return item.children;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
} else {
|
||||
// nestedMenu is either an Array or a Promise that will return that array.
|
||||
// NOTE: This might be changed soon as it's a hangover from an old implementation
|
||||
|
||||
return angular.isArray(item[1]) ||
|
||||
(item[1] && angular.isFunction(item[1].then)) ? item[1] : angular.isArray(item[2]) ||
|
||||
(item[2] && angular.isFunction(item[2].then)) ? item[2] : angular.isArray(item[3]) ||
|
||||
(item[3] && angular.isFunction(item[3].then)) ? item[3] : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for the actual rendering of the context menu.
|
||||
*
|
||||
* The parameters in params are:
|
||||
* - $scope = the scope of this context menu
|
||||
* - event = the event that triggered this context menu
|
||||
* - options = the options for this context menu
|
||||
* - modelValue = the value of the model attached to this context menu
|
||||
* - level = the current context menu level (defauts to 0)
|
||||
* - customClass = the custom class to be used for the context menu
|
||||
*/
|
||||
function renderContextMenu (params) {
|
||||
/// <summary>Render context menu recursively.</summary>
|
||||
|
||||
// Destructuring:
|
||||
var $scope = params.$scope;
|
||||
var event = params.event;
|
||||
var options = params.options;
|
||||
var modelValue = params.modelValue;
|
||||
var level = params.level;
|
||||
var customClass = params.customClass;
|
||||
|
||||
// Initialize the container. This will be passed around
|
||||
var $ul = initContextMenuContainer(params);
|
||||
params.$ul = $ul;
|
||||
|
||||
// Register this level of the context menu
|
||||
_contextMenus.push($ul);
|
||||
|
||||
/*
|
||||
* This object will contain any promises that we have
|
||||
* to wait for before trying to adjust the context menu.
|
||||
*/
|
||||
var $promises = [];
|
||||
params.$promises = $promises;
|
||||
|
||||
angular.forEach(options, function (item) {
|
||||
|
||||
if (item === null) {
|
||||
appendDivider($ul);
|
||||
} else {
|
||||
// If displayed is anything other than a function or a boolean
|
||||
var displayed = resolveBoolOrFunc(item.displayed, params);
|
||||
|
||||
// Only add the <li> if the item is displayed
|
||||
if (displayed) {
|
||||
var $li = $('<li>');
|
||||
var itemParams = angular.extend({}, params);
|
||||
itemParams.item = item;
|
||||
itemParams.$li = $li;
|
||||
|
||||
if (typeof item[0] === 'object') {
|
||||
custom.initialize($li, item);
|
||||
} else {
|
||||
processItem(itemParams);
|
||||
}
|
||||
if (resolveBoolOrFunc(item.hasTopDivider, itemParams, false)) {
|
||||
appendDivider($ul);
|
||||
}
|
||||
$ul.append($li);
|
||||
if (resolveBoolOrFunc(item.hasBottomDivider, itemParams, false)) {
|
||||
appendDivider($ul);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($ul.children().length === 0) {
|
||||
var $emptyLi = angular.element('<li>');
|
||||
setElementDisabled($emptyLi);
|
||||
$emptyLi.html('<a>' + _emptyText + '</a>');
|
||||
$ul.append($emptyLi);
|
||||
}
|
||||
|
||||
$document.find('body').append($ul);
|
||||
|
||||
doAfterAllPromises(params);
|
||||
|
||||
$rootScope.$broadcast(ContextMenuEvents.ContextMenuOpened, {
|
||||
context: _clickedElement,
|
||||
contextMenu: $ul,
|
||||
params: params
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* calculate if drop down menu would go out of screen at left or bottom
|
||||
* calculation need to be done after element has been added (and all texts are set; thus the promises)
|
||||
* to the DOM the get the actual height
|
||||
*/
|
||||
function doAfterAllPromises (params) {
|
||||
|
||||
// Desctructuring:
|
||||
var $ul = params.$ul;
|
||||
var $promises = params.$promises;
|
||||
var level = params.level;
|
||||
var event = params.event;
|
||||
var leftOriented = String(params.orientation).toLowerCase() === 'left';
|
||||
|
||||
$q.all($promises).then(function () {
|
||||
var topCoordinate = event.pageY;
|
||||
var menuHeight = angular.element($ul[0]).prop('offsetHeight');
|
||||
var winHeight = $window.pageYOffset + event.view.innerHeight;
|
||||
|
||||
/// the 20 pixels in second condition are considering the browser status bar that sometimes overrides the element
|
||||
if (topCoordinate > menuHeight && winHeight - topCoordinate < menuHeight + 20) {
|
||||
topCoordinate = event.pageY - menuHeight;
|
||||
/// If the element is a nested menu, adds the height of the parent li to the topCoordinate to align with the parent
|
||||
if(level && level > 0) {
|
||||
topCoordinate += event.event.currentTarget.offsetHeight;
|
||||
}
|
||||
} else if(winHeight <= menuHeight) {
|
||||
// If it really can't fit, reset the height of the menu to one that will fit
|
||||
angular.element($ul[0]).css({ 'height': winHeight - 5, 'overflow-y': 'scroll' });
|
||||
// ...then set the topCoordinate height to 0 so the menu starts from the top
|
||||
topCoordinate = 0;
|
||||
} else if(winHeight - topCoordinate < menuHeight) {
|
||||
var reduceThresholdY = 5;
|
||||
if(topCoordinate < reduceThresholdY) {
|
||||
reduceThresholdY = topCoordinate;
|
||||
}
|
||||
topCoordinate = winHeight - menuHeight - reduceThresholdY;
|
||||
}
|
||||
|
||||
var leftCoordinate = event.pageX;
|
||||
var menuWidth = angular.element($ul[0]).prop('offsetWidth');
|
||||
var winWidth = event.view.innerWidth + window.pageXOffset;
|
||||
var padding = 5;
|
||||
|
||||
if (leftOriented) {
|
||||
if (winWidth - leftCoordinate > menuWidth && leftCoordinate < menuWidth + padding) {
|
||||
leftCoordinate = padding;
|
||||
} else if (leftCoordinate < menuWidth) {
|
||||
var reduceThresholdX = 5;
|
||||
if (winWidth - leftCoordinate < reduceThresholdX + padding) {
|
||||
reduceThresholdX = winWidth - leftCoordinate + padding;
|
||||
}
|
||||
leftCoordinate = menuWidth + reduceThresholdX + padding;
|
||||
} else {
|
||||
leftCoordinate = leftCoordinate - menuWidth;
|
||||
}
|
||||
} else {
|
||||
if (leftCoordinate > menuWidth && winWidth - leftCoordinate - padding < menuWidth) {
|
||||
leftCoordinate = winWidth - menuWidth - padding;
|
||||
} else if(winWidth - leftCoordinate < menuWidth) {
|
||||
var reduceThresholdX = 5;
|
||||
if(leftCoordinate < reduceThresholdX + padding) {
|
||||
reduceThresholdX = leftCoordinate + padding;
|
||||
}
|
||||
leftCoordinate = winWidth - menuWidth - reduceThresholdX - padding;
|
||||
}
|
||||
}
|
||||
|
||||
$ul.css({
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
left: leftCoordinate + 'px',
|
||||
top: topCoordinate + 'px'
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the container of the context menu (a <ul> element),
|
||||
* applies the appropriate styles and then returns that container
|
||||
*
|
||||
* @return a <ul> jqLite/jQuery element
|
||||
*/
|
||||
function initContextMenuContainer(params) {
|
||||
// Destructuring
|
||||
var customClass = params.customClass;
|
||||
|
||||
var $ul = $('<ul>');
|
||||
$ul.addClass('dropdown-menu');
|
||||
$ul.attr({ 'role': 'menu' });
|
||||
$ul.css({
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
left: params.event.pageX + 'px',
|
||||
top: params.event.pageY + 'px',
|
||||
'z-index': 10000
|
||||
});
|
||||
|
||||
if(customClass) { $ul.addClass(customClass); }
|
||||
|
||||
return $ul;
|
||||
}
|
||||
|
||||
function isTouchDevice() {
|
||||
return 'ontouchstart' in window || navigator.maxTouchPoints; // works on most browsers | works on IE10/11 and Surface
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the context menus with level greater than or equal
|
||||
* to the value passed. If undefined, null or 0, all context menus
|
||||
* are removed.
|
||||
*/
|
||||
function removeContextMenus (level) {
|
||||
while (_contextMenus.length && (!level || _contextMenus.length > level)) {
|
||||
var cm = _contextMenus.pop();
|
||||
$rootScope.$broadcast(ContextMenuEvents.ContextMenuClosed, { context: _clickedElement, contextMenu: cm });
|
||||
cm.remove();
|
||||
}
|
||||
if(!level) {
|
||||
$rootScope.$broadcast(ContextMenuEvents.ContextMenuAllClosed, { context: _clickedElement });
|
||||
}
|
||||
}
|
||||
|
||||
function removeOnScrollEvent(e) {
|
||||
removeAllContextMenus(e);
|
||||
}
|
||||
|
||||
function removeOnOutsideClickEvent(e) {
|
||||
|
||||
var $curr = $(e.target);
|
||||
var shouldRemove = true;
|
||||
|
||||
while($curr.length) {
|
||||
if ($curr.hasClass('dropdown-menu')) {
|
||||
shouldRemove = false;
|
||||
break;
|
||||
} else {
|
||||
$curr = $curr.parent();
|
||||
}
|
||||
}
|
||||
if (shouldRemove) {
|
||||
removeAllContextMenus(e);
|
||||
}
|
||||
}
|
||||
|
||||
function removeAllContextMenus(e) {
|
||||
$document.find('body').off('mousedown touchstart', removeOnOutsideClickEvent);
|
||||
$document.off('scroll', removeOnScrollEvent);
|
||||
$(_clickedElement).removeClass('context');
|
||||
removeContextMenus();
|
||||
$rootScope.$broadcast('');
|
||||
}
|
||||
|
||||
function isBoolean(a) {
|
||||
return a === false || a === true;
|
||||
}
|
||||
|
||||
/** Resolves a boolean or a function that returns a boolean
|
||||
* Returns true by default if the param is null or undefined
|
||||
* @param a - the parameter to be checked
|
||||
* @param params - the object for the item's parameters
|
||||
* @param defaultValue - the default boolean value to use if the parameter is
|
||||
* neither a boolean nor function. True by default.
|
||||
*/
|
||||
function resolveBoolOrFunc(a, params, defaultValue) {
|
||||
var item = params.item;
|
||||
var $scope = params.$scope;
|
||||
var event = params.event;
|
||||
var modelValue = params.modelValue;
|
||||
|
||||
defaultValue = isBoolean(defaultValue) ? defaultValue : true;
|
||||
|
||||
if (isBoolean(a)) {
|
||||
return a;
|
||||
} else if (angular.isFunction(a)) {
|
||||
return a.call($scope, $scope, event, modelValue);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
function appendDivider($ul) {
|
||||
var $li = angular.element('<li>');
|
||||
$li.addClass('divider');
|
||||
$ul.append($li);
|
||||
}
|
||||
|
||||
function setElementDisabled($li) {
|
||||
$li.on('click', function ($event) {
|
||||
$event.preventDefault();
|
||||
});
|
||||
$li.addClass('disabled');
|
||||
}
|
||||
|
||||
return function ($scope, element, attrs) {
|
||||
var openMenuEvents = ['contextmenu'];
|
||||
_emptyText = $scope.$eval(attrs.contextMenuEmptyText) || 'empty';
|
||||
|
||||
if(attrs.contextMenuOn && typeof(attrs.contextMenuOn) === 'string'){
|
||||
openMenuEvents = attrs.contextMenuOn.split(',');
|
||||
}
|
||||
|
||||
angular.forEach(openMenuEvents, function (openMenuEvent) {
|
||||
element.on(openMenuEvent.trim(), function (event) {
|
||||
// Cleanup any leftover contextmenus(there are cases with longpress on touch where we
|
||||
// still see multiple contextmenus)
|
||||
removeAllContextMenus();
|
||||
|
||||
if(!attrs.allowEventPropagation) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Don't show context menu if on touch device and element is draggable
|
||||
if(isTouchDevice() && element.attr('draggable') === 'true') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove if the user clicks outside
|
||||
$document.find('body').on('mousedown touchstart', removeOnOutsideClickEvent);
|
||||
// Remove the menu when the scroll moves
|
||||
$document.on('scroll', removeOnScrollEvent);
|
||||
|
||||
_clickedElement = event.currentTarget;
|
||||
$(_clickedElement).addClass('context');
|
||||
|
||||
$scope.$apply(function () {
|
||||
var options = $scope.$eval(attrs.contextMenu);
|
||||
var customClass = attrs.contextMenuClass;
|
||||
var modelValue = $scope.$eval(attrs.model);
|
||||
var orientation = attrs.contextMenuOrientation;
|
||||
|
||||
$q.when(options).then(function(promisedMenu) {
|
||||
if (angular.isFunction(promisedMenu)) {
|
||||
// support for dynamic items
|
||||
promisedMenu = promisedMenu.call($scope, $scope, event, modelValue);
|
||||
}
|
||||
var params = {
|
||||
'$scope' : $scope,
|
||||
'event' : event,
|
||||
'options' : promisedMenu,
|
||||
'modelValue' : modelValue,
|
||||
'level' : 0,
|
||||
'customClass' : customClass,
|
||||
'orientation': orientation
|
||||
};
|
||||
$rootScope.$broadcast(ContextMenuEvents.ContextMenuOpening, { context: _clickedElement });
|
||||
renderContextMenu(params);
|
||||
});
|
||||
});
|
||||
|
||||
// Remove all context menus if the scope is destroyed
|
||||
$scope.$on('$destroy', function () {
|
||||
removeAllContextMenus();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (attrs.closeMenuOn) {
|
||||
$scope.$on(attrs.closeMenuOn, function () {
|
||||
removeAllContextMenus();
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
// eslint-disable-next-line angular/window-service
|
||||
})(window.angular.element, window.angular);
|
||||
10
src/3rdparty/js/mimer.min.js
vendored
Normal file
1
src/3rdparty/js/mimer.min.js.map
vendored
Normal file
5
src/3rdparty/js/showdown-1.6.4.min.js
vendored
3
src/3rdparty/js/showdown-1.9.1.min.js
vendored
Normal file
4
src/3rdparty/js/showdown-target-blank.min.js
vendored
@@ -1,4 +0,0 @@
|
||||
/*! showdown-target-blank 02-11-2015 */
|
||||
|
||||
!function(){"use strict";var a=function(){return[{type:"output",regex:"<a(.*?)>",replace:function(a,b){return'<a target="_blank"'+b+">"}}]};"undefined"!=typeof window&&window.showdown&&window.showdown.extensions&&window.showdown.extension("targetblank",a),"undefined"!=typeof module&&(module.exports=a)}();
|
||||
//# sourceMappingURL=showdown-target-blank.min.js.map
|
||||
130
src/3rdparty/xterm/addons/attach/attach.js
vendored
@@ -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;
|
||||
});
|
||||
86
src/3rdparty/xterm/addons/fit/fit.js
vendored
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* Fit terminal columns and rows to the dimensions of its DOM element.
|
||||
*
|
||||
* ## Approach
|
||||
* - Rows: Truncate the division of the terminal parent element height by the terminal row height.
|
||||
*
|
||||
* - Columns: Truncate the division of the terminal parent element width by the terminal character
|
||||
* width (apply display: inline at the terminal row and truncate its width with the current
|
||||
* number of columns).
|
||||
* @module xterm/addons/fit/fit
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
(function (fit) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = fit(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], fit);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
fit(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
var exports = {};
|
||||
|
||||
exports.proposeGeometry = function (term) {
|
||||
if (!term.element.parentElement) {
|
||||
return null;
|
||||
}
|
||||
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
|
||||
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
|
||||
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
|
||||
elementStyle = window.getComputedStyle(term.element),
|
||||
elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')),
|
||||
availableHeight = parentElementHeight - elementPaddingVer,
|
||||
availableWidth = parentElementWidth - elementPaddingHor,
|
||||
container = term.rowContainer,
|
||||
subjectRow = term.rowContainer.firstElementChild,
|
||||
contentBuffer = subjectRow.innerHTML,
|
||||
characterHeight,
|
||||
rows,
|
||||
characterWidth,
|
||||
cols,
|
||||
geometry;
|
||||
|
||||
subjectRow.style.display = 'inline';
|
||||
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
|
||||
characterWidth = subjectRow.getBoundingClientRect().width;
|
||||
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
|
||||
characterHeight = subjectRow.getBoundingClientRect().height;
|
||||
subjectRow.innerHTML = contentBuffer;
|
||||
|
||||
rows = parseInt(availableHeight / characterHeight);
|
||||
cols = parseInt(availableWidth / characterWidth);
|
||||
|
||||
geometry = {cols: cols, rows: rows};
|
||||
return geometry;
|
||||
};
|
||||
|
||||
exports.fit = function (term) {
|
||||
var geometry = exports.proposeGeometry(term);
|
||||
|
||||
if (geometry) {
|
||||
term.resize(geometry.cols, geometry.rows);
|
||||
}
|
||||
};
|
||||
|
||||
Xterm.prototype.proposeGeometry = function () {
|
||||
return exports.proposeGeometry(this);
|
||||
};
|
||||
|
||||
Xterm.prototype.fit = function () {
|
||||
return exports.fit(this);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
.xterm.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: 255;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* Fullscreen addon for xterm.js
|
||||
* @module xterm/addons/fullscreen/fullscreen
|
||||
* @license MIT
|
||||
*/
|
||||
(function (fullscreen) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = fullscreen(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], fullscreen);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
fullscreen(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
var exports = {};
|
||||
|
||||
/**
|
||||
* Toggle the given terminal's fullscreen mode.
|
||||
* @param {Xterm} term - The terminal to toggle full screen mode
|
||||
* @param {boolean} fullscreen - Toggle fullscreen on (true) or off (false)
|
||||
*/
|
||||
exports.toggleFullScreen = function (term, fullscreen) {
|
||||
var fn;
|
||||
|
||||
if (typeof fullscreen == 'undefined') {
|
||||
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
|
||||
} else if (!fullscreen) {
|
||||
fn = 'remove';
|
||||
} else {
|
||||
fn = 'add';
|
||||
}
|
||||
|
||||
term.element.classList[fn]('fullscreen');
|
||||
};
|
||||
|
||||
Xterm.prototype.toggleFullscreen = function (fullscreen) {
|
||||
exports.toggleFullScreen(this, fullscreen);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
||||
207
src/3rdparty/xterm/addons/linkify/linkify.js
vendored
@@ -1,207 +0,0 @@
|
||||
/**
|
||||
* Methods for turning URL subscrings in the terminal's content into links (`a` DOM elements).
|
||||
* @module xterm/addons/linkify/linkify
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
(function (linkify) {
|
||||
if (typeof exports === 'object' && typeof module === 'object') {
|
||||
/*
|
||||
* CommonJS environment
|
||||
*/
|
||||
module.exports = linkify(require('../../xterm'));
|
||||
} else if (typeof define == 'function') {
|
||||
/*
|
||||
* Require.js is available
|
||||
*/
|
||||
define(['../../xterm'], linkify);
|
||||
} else {
|
||||
/*
|
||||
* Plain browser environment
|
||||
*/
|
||||
linkify(window.Terminal);
|
||||
}
|
||||
})(function (Xterm) {
|
||||
'use strict';
|
||||
|
||||
var exports = {},
|
||||
protocolClause = '(https?:\\/\\/)',
|
||||
domainCharacterSet = '[\\da-z\\.-]+',
|
||||
negatedDomainCharacterSet = '[^\\da-z\\.-]+',
|
||||
domainBodyClause = '(' + domainCharacterSet + ')',
|
||||
tldClause = '([a-z\\.]{2,6})',
|
||||
ipClause = '((\\d{1,3}\\.){3}\\d{1,3})',
|
||||
portClause = '(:\\d{1,5})',
|
||||
hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + ')' + portClause + '?',
|
||||
pathClause = '(\\/[\\/\\w\\.-]*)*',
|
||||
negatedPathCharacterSet = '[^\\/\\w\\.-]+',
|
||||
bodyClause = hostClause + pathClause,
|
||||
start = '(?:^|' + negatedDomainCharacterSet + ')(',
|
||||
end = ')($|' + negatedPathCharacterSet + ')',
|
||||
lenientUrlClause = start + protocolClause + '?' + bodyClause + end,
|
||||
strictUrlClause = start + protocolClause + bodyClause + end,
|
||||
lenientUrlRegex = new RegExp(lenientUrlClause),
|
||||
strictUrlRegex = new RegExp(strictUrlClause);
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the given terminal line into
|
||||
* hyperlinks. The terminal line can be either the HTML element itself
|
||||
* or the index of the termina line in the children of the terminal
|
||||
* rows container.
|
||||
*
|
||||
* @param {Xterm} terminal - The terminal that owns the given line.
|
||||
* @param {number|HTMLDivElement} line - The terminal line that should get
|
||||
* "linkified".
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
* @emits linkify
|
||||
* @emits linkify:line
|
||||
*/
|
||||
exports.linkifyTerminalLine = function (terminal, line, lenient, target) {
|
||||
if (typeof line == 'number') {
|
||||
line = terminal.rowContainer.children[line];
|
||||
} else if (! (line instanceof HTMLDivElement)) {
|
||||
var message = 'The "line" argument should be either a number';
|
||||
message += ' or an HTMLDivElement';
|
||||
|
||||
throw new TypeError(message);
|
||||
}
|
||||
|
||||
if (typeof target === 'undefined') {
|
||||
target = '';
|
||||
} else {
|
||||
target = 'target="' + target + '"';
|
||||
}
|
||||
|
||||
var buffer = document.createElement('span'),
|
||||
nodes = line.childNodes;
|
||||
|
||||
for (var j=0; j<nodes.length; j++) {
|
||||
var node = nodes[j],
|
||||
match;
|
||||
|
||||
/**
|
||||
* Since we cannot access the TextNode's HTML representation
|
||||
* from the instance itself, we assign its data as textContent
|
||||
* to a dummy buffer span, in order to retrieve the TextNode's
|
||||
* HTML representation from the buffer's innerHTML.
|
||||
*/
|
||||
buffer.textContent = node.data;
|
||||
|
||||
var nodeHTML = buffer.innerHTML;
|
||||
|
||||
/**
|
||||
* Apply function only on TextNodes
|
||||
*/
|
||||
if (node.nodeType != node.TEXT_NODE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var url = exports.findLinkMatch(node.data, lenient);
|
||||
|
||||
if (!url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var startsWithProtocol = new RegExp('^' + protocolClause),
|
||||
urlHasProtocol = url.match(startsWithProtocol),
|
||||
href = (urlHasProtocol) ? url : 'http://' + url,
|
||||
link = '<a href="' + href + '" ' + target + '>' + url + '</a>',
|
||||
newHTML = nodeHTML.replace(url, link);
|
||||
|
||||
line.innerHTML = line.innerHTML.replace(nodeHTML, newHTML);
|
||||
}
|
||||
|
||||
/**
|
||||
* This event gets emitted when conversion of all URL susbtrings
|
||||
* to HTML anchor elements (links) has finished, for a specific
|
||||
* line of the current Xterm instance.
|
||||
*
|
||||
* @event linkify:line
|
||||
*/
|
||||
terminal.emit('linkify:line', line);
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a link within a block of text.
|
||||
*
|
||||
* @param {string} text - The text to search .
|
||||
* @param {boolean} lenient - Whether to use the lenient search.
|
||||
* @return {string} A URL.
|
||||
*/
|
||||
exports.findLinkMatch = function (text, lenient) {
|
||||
var match = text.match(lenient ? lenientUrlRegex : strictUrlRegex);
|
||||
if (!match || match.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the terminal view into hyperlinks.
|
||||
*
|
||||
* @param {Xterm} terminal - The terminal that should get "linkified".
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
* @emits linkify
|
||||
* @emits linkify:line
|
||||
*/
|
||||
exports.linkify = function (terminal, lenient, target) {
|
||||
var rows = terminal.rowContainer.children;
|
||||
|
||||
lenient = (typeof lenient == "boolean") ? lenient : true;
|
||||
for (var i=0; i<rows.length; i++) {
|
||||
var line = rows[i];
|
||||
|
||||
exports.linkifyTerminalLine(terminal, line, lenient, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* This event gets emitted when conversion of all URL substrings to
|
||||
* HTML anchor elements (links) has finished for the current Xterm
|
||||
* instance's view.
|
||||
*
|
||||
* @event linkify
|
||||
*/
|
||||
terminal.emit('linkify');
|
||||
};
|
||||
|
||||
/**
|
||||
* Extend Xterm prototype.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the current terminal linte into
|
||||
* hyperlinks.
|
||||
*
|
||||
* @memberof Xterm
|
||||
* @param {number|HTMLDivElement} line - The terminal line that should get
|
||||
* "linkified".
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
*/
|
||||
Xterm.prototype.linkifyTerminalLine = function (line, lenient, target) {
|
||||
return exports.linkifyTerminalLine(this, line, lenient, target);
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts all valid URLs found in the current terminal into hyperlinks.
|
||||
*
|
||||
* @memberof Xterm
|
||||
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
|
||||
* false, the regex requires a protocol clause. Defaults to true.
|
||||
* @param {string} target - Sets target="" attribute with value provided to links.
|
||||
* Default doesn't set target attribute
|
||||
*/
|
||||
Xterm.prototype.linkify = function (lenient, target) {
|
||||
return exports.linkify(this, lenient, target);
|
||||
};
|
||||
|
||||
return exports;
|
||||
});
|
||||
135
src/3rdparty/xterm/addons/terminado/terminado.js
vendored
@@ -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;
|
||||
});
|
||||
2261
src/3rdparty/xterm/xterm.css
vendored
5132
src/3rdparty/xterm/xterm.js
vendored
1
src/3rdparty/xterm/xterm.js.map
vendored
@@ -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>
|
||||
|
||||
@@ -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">©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
@@ -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%;"> </th>
|
||||
<th style="width:82%">Name</th>
|
||||
<th style="width:10%">Size</th>
|
||||
<th style="width:10%">Owner</th>
|
||||
<th style="width: 5%"> </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>
|
||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 13 KiB |
102
src/img/appicon_fallback.svg
Normal 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 |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
BIN
src/img/avatars/logo-black.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/img/avatars/logo-darkblue.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/img/avatars/logo-green.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/img/avatars/logo-orange.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/img/avatars/logo-red.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/img/avatars/logo-yellow.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/img/avatars/logo.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
128
src/index.html
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
|
||||
<!-- this gets changed once we get the config (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title> Cloudron </title>
|
||||
<title>‎</title>
|
||||
|
||||
<link id="favicon" type="image/png" rel="icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
@@ -13,16 +13,19 @@
|
||||
|
||||
<!-- 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>
|
||||
|
||||
@@ -41,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>
|
||||
|
||||
@@ -52,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>
|
||||
@@ -73,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>
|
||||
|
||||
@@ -90,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">×</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">
|
||||
|
||||
@@ -130,40 +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">
|
||||
<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 || config.features.spaces">
|
||||
<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>
|
||||
@@ -171,10 +168,7 @@
|
||||
<div ng-view id="ng-view" class="layout-content"></div>
|
||||
|
||||
<footer class="text-center ng-cloak">
|
||||
<span class="text-muted">© 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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
}]);
|
||||
2379
src/js/client.js
641
src/js/filemanager.js
Normal 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('.'));
|
||||
});
|
||||
});
|
||||
}]);
|
||||
531
src/js/index.js
@@ -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,39 +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();
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
function asyncSeries(funcs, callback) {
|
||||
var cur = 0;
|
||||
|
||||
if (funcs.length === 0) return callback();
|
||||
|
||||
(function iterator() {
|
||||
funcs[cur](function (error) {
|
||||
if (error) return callback(error);
|
||||
if (cur >= funcs.length-1) return callback();
|
||||
++cur;
|
||||
|
||||
iterator();
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld', 'ui.multiselect']);
|
||||
|
||||
@@ -58,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('/', {
|
||||
@@ -70,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 %>'
|
||||
@@ -79,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 %>'
|
||||
@@ -106,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
|
||||
@@ -144,93 +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('appIsInstalledAndHealthy', function () {
|
||||
return function (app) {
|
||||
return (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY);
|
||||
}
|
||||
});
|
||||
|
||||
app.filter('activeOAuthClients', function () {
|
||||
return function (clients, user) {
|
||||
return clients.filter(function (c) { return user.admin || (c.activeTokens && c.activeTokens.length > 0); });
|
||||
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) {
|
||||
@@ -247,16 +352,6 @@ app.filter('inProgressApps', function () {
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('applicationLink', function() {
|
||||
return function(app) {
|
||||
if (app.installationState === ISTATES.INSTALLED && app.health === HSTATES.HEALTHY && !app.pendingPostInstallConfirmation) {
|
||||
return 'https://' + app.fqdn;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.filter('prettyHref', function () {
|
||||
return function (input) {
|
||||
if (!input) return input;
|
||||
@@ -268,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);
|
||||
|
||||
@@ -291,175 +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';
|
||||
var ACTION_USER_TRANSFER = 'user.transfer';
|
||||
|
||||
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_TRANSFER:
|
||||
return 'Apps of ' + data.oldOwnerId + ' was transferred to ' + data.newOwnerId;
|
||||
|
||||
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);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -502,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();
|
||||
@@ -543,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;
|
||||
@@ -551,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) {
|
||||
@@ -575,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;
|
||||
@@ -588,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) {
|
||||
@@ -599,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()');
|
||||
}
|
||||
@@ -607,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)">×</div>' +
|
||||
'<div class="btn-group input-tag" data-ng-repeat="tag in tagArray()">' +
|
||||
'<button type="button" class="btn btn-xs btn-primary" disabled>{{ tag }}</button>' +
|
||||
'<button type="button" class="btn btn-xs btn-primary" data-ng-click="deleteTag($index)">×</button>' +
|
||||
'</div>' +
|
||||
'<input type="text" data-ng-model="tagText" ng-blur="addTag()" placeholder="{{placeholder}}"/>' +
|
||||
'</div>'
|
||||
|
||||
149
src/js/login.js
Normal 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();
|
||||
}]);
|
||||
176
src/js/logs.js
@@ -1,11 +1,13 @@
|
||||
'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;
|
||||
@@ -14,11 +16,7 @@ app.controller('LogsController', ['$scope', '$timeout', '$location', 'Client', f
|
||||
$scope.activeEventSource = null;
|
||||
$scope.lines = 100;
|
||||
$scope.selectedAppInfo = null;
|
||||
|
||||
$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));
|
||||
@@ -29,11 +27,34 @@ app.controller('LogsController', ['$scope', '$timeout', '$location', 'Client', f
|
||||
logViewer.empty();
|
||||
};
|
||||
|
||||
// https://github.com/janl/mustache.js/blob/master/mustache.js#L60
|
||||
var entityMap = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -51,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' });
|
||||
@@ -60,71 +82,109 @@ app.controller('LogsController', ['$scope', '$timeout', '$location', 'Client', f
|
||||
});
|
||||
}
|
||||
|
||||
function loadId(id, callback) {
|
||||
// Add built-in log types for now
|
||||
var BUILT_IN_LOGS = [
|
||||
{ name: 'Box', type: 'platform', value: 'box', url: Client.makeURL('/api/v1/cloudron/logs/box') },
|
||||
{ name: 'Mail', type: 'platform', value: 'mail', url: Client.makeURL('/api/v1/cloudron/logs/mail') },
|
||||
{ name: 'Backup', type: 'platform', value: 'backup', url: Client.makeURL('/api/v1/cloudron/logs/backup') }
|
||||
];
|
||||
|
||||
$scope.selected = BUILT_IN_LOGS.find(function (e) { return e.value === id; });
|
||||
if ($scope.selected) return callback();
|
||||
|
||||
Client.getApp(id, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.selectedAppInfo = app;
|
||||
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') },
|
||||
];
|
||||
|
||||
$scope.selected = BUILT_IN_LOGS.find(function (e) { return e.value === ids.id; });
|
||||
callback();
|
||||
} else if (ids.crashId) {
|
||||
$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
|
||||
type: 'platform',
|
||||
value: 'crash-' + ids.crashId,
|
||||
name: 'Crash',
|
||||
url: Client.makeURL('/api/v1/cloudron/logs/crash-' + ids.crashId)
|
||||
};
|
||||
|
||||
callback();
|
||||
});
|
||||
} else if (ids.appId) {
|
||||
Client.getApp(ids.appId, function (error, app) {
|
||||
if (error) return callback(error);
|
||||
|
||||
$scope.selectedAppInfo = app;
|
||||
|
||||
$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
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) return $scope.error(error);
|
||||
function init() {
|
||||
|
||||
if (!status.activated) {
|
||||
console.log('Not activated yet, redirecting', status);
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
// 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);
|
||||
}
|
||||
if (!status.activated) {
|
||||
console.log('Not activated yet, redirecting', status);
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Running log version ', localStorage.version);
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
console.log('Running log version ', localStorage.version);
|
||||
|
||||
Client.refreshConfig(function (error) {
|
||||
if (error) return $scope.error(error);
|
||||
// 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);
|
||||
|
||||
loadId(search.id, function (error) {
|
||||
if (error) return $scope.error(error);
|
||||
Client.refreshConfig(function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
// now mark the Client to be ready
|
||||
Client.setReady();
|
||||
select({ id: search.id, taskId: search.taskId, appId: search.appId, crashId: search.crashId }, function (error) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
$scope.initialized = true;
|
||||
// now mark the Client to be ready
|
||||
Client.setReady();
|
||||
|
||||
showLogs();
|
||||
$scope.initialized = true;
|
||||
|
||||
showLogs();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
}]);
|
||||
|
||||
229
src/js/main.js
@@ -1,16 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('MainController', ['$scope', '$route', '$timeout', '$location', 'Client', 'AppStore', function ($scope, $route, $timeout, $location, Client, AppStore) {
|
||||
/* 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,193 +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 = '/#/backups';
|
||||
|
||||
Client.notify('Backup Configuration', 'Cloudron backups are disabled. Please ensure this server is backed up using alternate means.', false, 'info', actionScope);
|
||||
} else if (backupConfig.provider === 'filesystem' && !backupConfig.externalDisk) {
|
||||
var actionScope = $scope.$new(true);
|
||||
actionScope.action = '/#/backups';
|
||||
|
||||
Client.notify('Backup Configuration',
|
||||
'Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails.',
|
||||
false /* persistent */, '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);
|
||||
if (status.webadminStatus.restore.active || status.webadminStatus.restore.error) {
|
||||
window.location.href = '/restore.html';
|
||||
} else {
|
||||
window.location.href = status.adminFqdn ? '/setup.html' : '/setupdns.html';
|
||||
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;
|
||||
}
|
||||
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();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -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)) {
|
||||
@@ -96,12 +174,24 @@ app.controller('RestoreController', ['$scope', '$http', 'Client', function ($sco
|
||||
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';
|
||||
}
|
||||
@@ -136,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';
|
||||
@@ -144,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) {
|
||||
@@ -188,22 +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.restore.active) { // restore finished
|
||||
if (status.webadminStatus.restore.error) {
|
||||
if (!error && !status.restore.active) { // restore finished
|
||||
if (status.restore.errorMessage) {
|
||||
$scope.busy = false;
|
||||
$scope.error.generic = status.webadminStatus.restore.error;
|
||||
$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);
|
||||
});
|
||||
}
|
||||
@@ -226,22 +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.webadminStatus.restore.active) return waitForRestore();
|
||||
var backupConfig;
|
||||
try {
|
||||
backupConfig = JSON.parse(result.target.result);
|
||||
} catch (e) {
|
||||
console.error('Unable to parse backup config');
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.webadminStatus.restore.error) $scope.error.generic = status.webadminStatus.restore.error;
|
||||
$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]);
|
||||
};
|
||||
|
||||
if (status.activated) {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
function init() {
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) return Client.initError(error, init);
|
||||
|
||||
$scope.instanceId = search.instanceId;
|
||||
$scope.initialized = true;
|
||||
});
|
||||
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();
|
||||
}]);
|
||||
|
||||
120
src/js/setup.js
@@ -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
@@ -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;
|
||||
});
|
||||
}]);
|
||||
@@ -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,19 +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.hyphenatedSubdomains = 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: '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;
|
||||
|
||||
@@ -47,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: '',
|
||||
@@ -69,18 +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'
|
||||
},
|
||||
hyphenatedSubdomains: false
|
||||
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 () {
|
||||
@@ -101,65 +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,
|
||||
hyphenatedSubdomains: $scope.hyphenatedSubdomains
|
||||
};
|
||||
|
||||
// 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 === 401) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -171,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);
|
||||
});
|
||||
}
|
||||
@@ -193,20 +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.hyphenatedSubdomains = status.edition === 'hostingprovider';
|
||||
$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();
|
||||
}]);
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
@@ -107,7 +108,7 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
|
||||
|
||||
function reset() {
|
||||
if ($scope.terminal) {
|
||||
$scope.terminal.destroy();
|
||||
$scope.terminal.dispose();
|
||||
$scope.terminal = null;
|
||||
}
|
||||
|
||||
@@ -120,82 +121,37 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
|
||||
|
||||
$scope.restartApp = function () {
|
||||
$scope.restartAppBusy = true;
|
||||
$scope.appBusy = true;
|
||||
|
||||
var appId = $scope.selected.value;
|
||||
|
||||
function waitUntilStopped(callback) {
|
||||
function waitUntilRestarted(callback) {
|
||||
refreshApp(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.restartAppBusy = false;
|
||||
$scope.appBusy = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.repairApp = function () {
|
||||
$('#repairAppModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.repairAppBegin = function () {
|
||||
$scope.appBusy = true;
|
||||
|
||||
function waitUntilInRepairState() {
|
||||
refreshApp($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);
|
||||
|
||||
$('#repairAppModal').modal('hide');
|
||||
|
||||
waitUntilInRepairState();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.repairAppDone = function () {
|
||||
$scope.appBusy = true;
|
||||
|
||||
function waitUntilInNormalState() {
|
||||
refreshApp($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);
|
||||
|
||||
waitUntilInNormalState();
|
||||
});
|
||||
};
|
||||
|
||||
function createTerminalSocket() {
|
||||
try {
|
||||
// websocket cannot use relative urls
|
||||
var url = Client.apiOrigin.replace('https', 'wss') + '/api/v1/apps/' + $scope.selected.value + '/execws?tty=true&rows=' + $scope.terminal.rows + '&columns=' + $scope.terminal.cols + '&access_token=' + Client.getToken();
|
||||
$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
|
||||
@@ -231,17 +187,14 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
|
||||
|
||||
var result = $scope.selectedAppInfo;
|
||||
|
||||
// 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.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;
|
||||
|
||||
@@ -256,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);
|
||||
});
|
||||
}
|
||||
@@ -271,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;
|
||||
|
||||
@@ -303,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();
|
||||
|
||||
@@ -325,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) {
|
||||
@@ -361,7 +341,8 @@ app.controller('TerminalController', ['$scope', '$timeout', '$location', 'Client
|
||||
type: 'app',
|
||||
value: app.id,
|
||||
name: app.fqdn + ' (' + app.manifest.title + ')',
|
||||
addons: app.manifest.addons
|
||||
addons: app.manifest.addons,
|
||||
manifest: app.manifest
|
||||
};
|
||||
|
||||
// now mark the Client to be ready
|
||||
|
||||
@@ -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
@@ -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>‎</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>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,70 +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 rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
<link id="favicon" href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/api/v1/cloudron/avatar">
|
||||
<link rel="icon" href="/api/v1/cloudron/avatar">
|
||||
|
||||
<!-- CSS -->
|
||||
<link type="text/css" rel="stylesheet" href="/3rdparty/angular-ui-notification.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-href="{{ '/terminal.html?id=' + selected.value }}" target="_blank" ng-show="selected.type === 'app'"><i class="fa fa-terminal"></i> Terminal</a>
|
||||
<a class="btn btn-primary" ng-click="clear()"><i class="fa fa-trash"></i> Clear View</a>
|
||||
<a class="btn btn-primary" ng-href="{{ selected.url }}&format=short&lines=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>
|
||||
|
||||
108
src/restore.html
@@ -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'">
|
||||
<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">©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">©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>
|
||||
|
||||
|
||||
174
src/setup.html
@@ -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> </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">©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>
|
||||
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 & 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">©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
@@ -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>
|
||||
@@ -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 class="col-md-10 col-md-offset-1 text-center">
|
||||
<h1>Domain Setup</h1>
|
||||
<p class="has-error text-center" ng-show="error.setup">{{ error.setup }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.domain.$dirty && dnsCredentialsForm.domain.$invalid }">
|
||||
<label class="control-label">Domain <sup><a ng-href="https://docs.cloudron.io/installation/#domain-setup" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.domain" name="domain" placeholder="example.com" required autofocus ng-disabled="dnsCredentials.busy">
|
||||
<p style="margin-top: 5px; font-size: 13px;">
|
||||
Apps will be installed on subdomains of this domain. The dashboard will be available on the <b>my</b> subdomain. You can add more domains later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<br/>
|
||||
<h3 class="text-center">Domain Configuration <sup><a ng-href="https://docs.cloudron.io/domains/#dns-providers" class="help" target="_blank" tabindex="-1"><i class="fa fa-question-circle"></i></a></sup> </h3>
|
||||
<p class="has-error text-center" ng-show="error.dnsCredentials">{{ error.dnsCredentials }}</p>
|
||||
|
||||
<div class="form-group" 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>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<h3 class="text-center">Domain Configuration</h3>
|
||||
<p class="has-error text-center" ng-show="dnsCredentials.error">{{ dnsCredentials.error }}</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,71 +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 & 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> <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 uib-collapse="!dnsCredentials.advancedVisible">
|
||||
|
||||
<div class="checkbox" ng-show="hyphenatedSubdomains">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="dnsCredentials.hyphenatedSubdomains" name="hyphenatedSubdomains" ng-disabled="dnsCredentials.busy"/>Hyphenate Subdomains
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<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="dnsCredentials.advancedVisible = true" ng-hide="dnsCredentials.advancedVisible">Advanced settings...</a>
|
||||
<a href="" ng-click="dnsCredentials.advancedVisible = false" ng-show="dnsCredentials.advancedVisible">Hide Advanced settings</a>
|
||||
<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>
|
||||
@@ -216,8 +268,7 @@
|
||||
</div>
|
||||
|
||||
<footer class="text-center">
|
||||
<span class="text-muted">©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">©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
@@ -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>
|
||||
@@ -4,7 +4,6 @@
|
||||
<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">
|
||||
@@ -12,16 +11,18 @@
|
||||
<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>
|
||||
|
||||
@@ -32,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/ -->
|
||||
@@ -42,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>
|
||||
@@ -76,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>
|
||||
@@ -90,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>
|
||||
@@ -101,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">
|
||||
@@ -129,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">
|
||||
@@ -138,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>
|
||||
<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>
|
||||
|
||||
|
||||
923
src/theme.scss
@@ -1,69 +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" ng-show="!error">{{title}}</h4>
|
||||
<h4 class="modal-title text-danger" ng-show="error">{{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">©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>
|
||||
@@ -1,408 +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.apiClient = {};
|
||||
|
||||
$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');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.tokenAdd = {
|
||||
token: {},
|
||||
tokenName: '',
|
||||
busy: false,
|
||||
error: {},
|
||||
|
||||
reset: function () {
|
||||
$scope.tokenAdd.busy = false;
|
||||
$scope.tokenAdd.token = {};
|
||||
$scope.tokenAdd.error.tokenName = null;
|
||||
$scope.tokenAdd.tokenName = '';
|
||||
|
||||
$scope.tokenAddForm.$setUntouched();
|
||||
$scope.tokenAddForm.$setPristine();
|
||||
},
|
||||
|
||||
show: function (client) {
|
||||
$scope.tokenAdd.reset();
|
||||
$('#tokenAddModal').modal('show');
|
||||
},
|
||||
|
||||
submit: function (client) {
|
||||
$scope.tokenAdd.busy = true;
|
||||
$scope.tokenAdd.token = {};
|
||||
|
||||
var expiresAt = Date.now() + 100 * 365 * 24 * 60 * 60 * 1000; // ~100 years from now
|
||||
|
||||
Client.createTokenByClientId(client.id, '*' /* scope */, expiresAt, $scope.tokenAdd.tokenName, function (error, result) {
|
||||
if (error) {
|
||||
if (error.statusCode === 400) {
|
||||
$scope.tokenAdd.error.tokenName = 'Invalid token name';
|
||||
$scope.tokenAddForm.tokenName.$setPristine();
|
||||
$('#inputTokenAddName').focus();
|
||||
} else {
|
||||
console.error('Unable to create token.', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.tokenAdd.busy = false;
|
||||
$scope.tokenAdd.token = result;
|
||||
|
||||
refreshClientTokens(client);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeToken = function (client, token) {
|
||||
Client.delToken(client.id, token.accessToken, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
refreshClientTokens(client);
|
||||
});
|
||||
};
|
||||
|
||||
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 || [];
|
||||
|
||||
if (callback) callback();
|
||||
});
|
||||
}
|
||||
|
||||
Client.onReady(function () {
|
||||
Client.getOAuthClients(function (error, activeClients) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
asyncForEach(activeClients, refreshClientTokens, function () {
|
||||
$scope.webadminClient = activeClients.filter(function (c) { return c.id === 'cid-webadmin'; })[0];
|
||||
$scope.apiClient = activeClients.filter(function (c) { return c.id === 'cid-sdk'; })[0];
|
||||
|
||||
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.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', 'tokenAddModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
|
||||
$('.modal-backdrop').remove();
|
||||
}]);
|
||||
@@ -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>
|
||||
|
||||
@@ -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.getUserInfo().admin) $location.path('/'); });
|
||||
Client.onReady(function () { if (!Client.getUserInfo().isAtLeastAdmin) $location.path('/'); });
|
||||
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
@@ -18,20 +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.transfer', value: 'user.transfer' },
|
||||
{ name: 'user.update', value: 'user.update' }
|
||||
{ name: 'user.update', value: 'user.update' },
|
||||
{ name: 'System Crash', value: 'system.crash' }
|
||||
];
|
||||
|
||||
$scope.pageItemCount = [
|
||||
@@ -46,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(', ');
|
||||
@@ -55,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
1623
src/views/app.js
Normal file
@@ -1,346 +1,3 @@
|
||||
<!-- 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>
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||
<!-- the admin check is to check for spaces user -->
|
||||
<span ng-if="user.admin">{{ (!appConfigure.location ? '' : (appConfigure.domain.config.hyphenatedSubdomains ? '-' : '.')) + appConfigure.domain.domain }}</span>
|
||||
<span ng-if="!user.admin">{{ (!appConfigure.location ? '' : '-') + spacesSuffix + (appConfigure.domain.config.hyphenatedSubdomains ? '-' : '.') + appConfigure.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="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" 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="has-error" ng-show="appConfigure.error.alternateDomains">{{ appConfigure.error.alternateDomains }}</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.alternateSubdomain.$dirty && appConfigureForm.alternateSubdomain.$invalid) || (!appConfigureForm.alternateSubdomain.$dirty && appConfigure.error.alternateDomains) }">
|
||||
<input type="checkbox" id="appConfigureAlternateDomainEnabled" ng-model="appConfigure.alternateDomainEnabled">
|
||||
<label class="control-label" for="appConfigureAlternateDomainEnabled">Redirect the following domain to this app</label>
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="appConfigure.alternateSubdomain" id="appConfigureAlternateSubdomainInput" name="alternateSubdomain" placeholder="Leave empty to use bare domain" ng-disabled="!appConfigure.alternateDomainEnabled">
|
||||
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" ng-disabled="!appConfigure.alternateDomainEnabled">
|
||||
<!-- the admin check is to check for spaces user -->
|
||||
<span ng-if="user.admin">{{ (!appConfigure.alternateSubdomain ? '' : (appConfigure.alternateDomain.config.hyphenatedSubdomains ? '-' : '.')) + appConfigure.alternateDomain.domain }}</span>
|
||||
<span ng-if="!user.admin">{{ (!appConfigure.alternateSubdomain ? '' : '-') + spacesSuffix + (appConfigure.alternateDomain.config.hyphenatedSubdomains ? '-' : '.') + appConfigure.alternateDomain.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="appConfigure.alternateDomain = domain">{{ domain.domain }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
</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" ng-submit="appRestore.clone()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': appRestore.error.location }">
|
||||
<label class="control-label" for="appRestoreLocationInput">Location</label>
|
||||
<div ng-show="appRestore.error.location"><small>{{ appRestore.error.location }}</small></div>
|
||||
<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">
|
||||
<!-- the admin check is to check for spaces user -->
|
||||
<span ng-if="user.admin">{{ (!appRestore.location ? '' : (appRestore.domain.config.hyphenatedSubdomains ? '-' : '.')) + appRestore.domain.domain }}</span>
|
||||
<span ng-if="!user.admin">{{ (!appRestore.location ? '' : '-') + spacesSuffix + (appRestore.domain.config.hyphenatedSubdomains ? '-' : '.') + appRestore.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="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="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 -->
|
||||
<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>
|
||||
<br/>
|
||||
App ID <span class="app-info-meta text-small">{{ appInfo.app.id }}</a> </span>
|
||||
<br/>
|
||||
Last updated <span class="app-info-meta text-small">{{ appInfo.app.updateTime | prettyDate }}</span>
|
||||
</h5>
|
||||
</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 ng-show="appInfo.app.manifest.documentationUrl">
|
||||
<br/>
|
||||
Please see the <a target="_blank" ng-href="{{appInfo.app.manifest.documentationUrl}}">documentation</a> for more information.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" autofocus>Got it</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal postinstall confirm -->
|
||||
<div class="modal fade" id="appPostInstallConfirmModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
@@ -356,16 +13,20 @@
|
||||
</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-bind-html="appPostInstallConfirm.app.manifest.postInstallMessage | postInstallMessage:appPostInstallConfirm.app | markdown2html"></div>
|
||||
<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 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>
|
||||
<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>
|
||||
@@ -373,88 +34,58 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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" 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 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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- 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 id="appinfoPostinstallMessage" class="panel-collapse collapse">
|
||||
<br/>
|
||||
<div ng-bind-html="appInfo.app.manifest.postInstallMessage | markdown2html"></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>
|
||||
|
||||
@@ -463,7 +94,7 @@
|
||||
<!-- Workaround for select-all issue, see commit message -->
|
||||
<div style="font-size: 1px;"> </div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && (user.admin || config.features.spaces)">
|
||||
<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>
|
||||
@@ -472,108 +103,71 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && !(user.admin || config.features.spaces)">
|
||||
<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)) || ((app | appIsInstalledAndHealthy) && app.pendingPostInstallConfirmation && appPostInstallConfirm.show(app))" target="_blank" ng-class="{ 'hand': (app | appIsInstalledAndHealthy) }">
|
||||
<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 || (config.features.spaces && app.ownerId === user.id)">
|
||||
<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 || (config.features.spaces && app.ownerId === user.id)">
|
||||
<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 ng-href="{{ '/terminal.html?id=' + app.id }}" target="_blank" uib-tooltip="Terminal" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-terminal scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a ng-href="{{ '/logs.html?id=' + app.id }}" target="_blank" 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>
|
||||
|
||||
@@ -1,346 +1,52 @@
|
||||
'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.spacesSuffix = '';
|
||||
$scope.appSearch = '';
|
||||
|
||||
$scope.appConfigure = {
|
||||
busy: false,
|
||||
error: {},
|
||||
app: {},
|
||||
domain: null,
|
||||
location: '',
|
||||
advancedVisible: false,
|
||||
portBindings: {},
|
||||
portBindingsEnabled: {},
|
||||
portBindingsInfo: {},
|
||||
robotsTxt: '',
|
||||
certificateFile: null,
|
||||
certificateFileName: '',
|
||||
keyFile: null,
|
||||
keyFileName: '',
|
||||
memoryLimit: 0,
|
||||
memoryTicks: [],
|
||||
mailboxName: '',
|
||||
$scope.$watch('selectedTags', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
accessRestrictionOption: 'any',
|
||||
accessRestriction: { users: [], groups: [] },
|
||||
xFrameOptions: '',
|
||||
alternateDomainEnabled: false,
|
||||
alternateSubdomain: '',
|
||||
alternateDomain: null,
|
||||
ssoAuth: false,
|
||||
localStorage.selectedTags = newVal.join(',');
|
||||
});
|
||||
|
||||
isAccessRestrictionValid: function () {
|
||||
var tmp = $scope.appConfigure.accessRestriction;
|
||||
return !!(tmp.users.length || tmp.groups.length);
|
||||
},
|
||||
$scope.$watch('selectedDomain', function (newVal, oldVal) {
|
||||
if (newVal === oldVal) return;
|
||||
|
||||
show: function (app) {
|
||||
$scope.reset();
|
||||
|
||||
// fill relevant info from the app
|
||||
$scope.appConfigure.app = app;
|
||||
if ($scope.user.admin) {
|
||||
$scope.appConfigure.location = app.location;
|
||||
} else { // strip the trailing username in spaces mode
|
||||
$scope.appConfigure.location = app.location === $scope.spacesSuffix ? '' : app.location.replace(new RegExp('-' + $scope.spacesSuffix + '$'),'');
|
||||
}
|
||||
$scope.appConfigure.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0];
|
||||
$scope.appConfigure.portBindingsInfo = angular.extend({}, app.manifest.tcpPorts, app.manifest.udpPorts); // Portbinding map only for information
|
||||
$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.alternateDomainEnabled = !!app.alternateDomains[0];
|
||||
$scope.appConfigure.alternateSubdomain = app.alternateDomains[0] ? app.alternateDomains[0].subdomain : '';
|
||||
$scope.appConfigure.alternateDomain = app.alternateDomains[0] ? $scope.domains.filter(function (d) { return d.domain === app.alternateDomains[0].domain; })[0] : $scope.appConfigure.domain;
|
||||
$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.alternateDomains = 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,
|
||||
alternateDomains: []
|
||||
};
|
||||
|
||||
// The backend supports multiple alternateDomains, however we only have ui for one
|
||||
if ($scope.appConfigure.alternateDomainEnabled) data.alternateDomains = [{ domain: $scope.appConfigure.alternateDomain.domain, subdomain: $scope.appConfigure.alternateSubdomain }];
|
||||
|
||||
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 = error.message;
|
||||
$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 if (error.statusCode === 400 && error.message.indexOf('alternateDomains') !== -1 ) {
|
||||
$scope.appConfigure.error.alternateDomains = error.message;
|
||||
$scope.appConfigureForm.alternateDomains.$setPristine();
|
||||
$('#appConfigureAlternateSubdomainInput').focus();
|
||||
} else {
|
||||
$scope.appConfigure.error.other = error.message;
|
||||
}
|
||||
|
||||
$scope.appConfigure.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appConfigure.busy = false;
|
||||
|
||||
Client.refreshAppCache($scope.appConfigure.app.id); // 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.refreshAppCache($scope.appRestore.app.id, 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, clonedApp) {
|
||||
$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.';
|
||||
$('#appRestoreLocationInput').focus();
|
||||
} else {
|
||||
Client.error(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
$('#appRestoreModal').modal('hide');
|
||||
|
||||
Client.refreshAppCache(clonedApp.id); // 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 = angular.extend({}, $scope.appRestore.app.manifest.tcpPorts, $scope.appRestore.app.manifest.udpPorts); // Portbinding map only for information
|
||||
// set default ports
|
||||
for (var env in $scope.appRestore.portBindingsInfo) {
|
||||
$scope.appRestore.portBindings[env] = $scope.appRestore.portBindingsInfo[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;
|
||||
}
|
||||
});
|
||||
|
||||
return false; // prevent propagation and default
|
||||
},
|
||||
|
||||
restore: function () {
|
||||
$scope.appRestore.busy = true;
|
||||
$scope.appRestore.error.password = null;
|
||||
|
||||
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.appRestore.busy = false;
|
||||
|
||||
Client.refreshAppCache($scope.appRestore.app.id); // reflect the new app state immediately
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.appInfo = {
|
||||
app: {},
|
||||
message: ''
|
||||
};
|
||||
if (newVal._alldomains) localStorage.removeItem('selectedDomain');
|
||||
else localStorage.selectedDomain = newVal.domain;
|
||||
});
|
||||
|
||||
$scope.appPostInstallConfirm = {
|
||||
app: {},
|
||||
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']);
|
||||
|
||||
$('#appPostInstallConfirmModal').modal('show');
|
||||
|
||||
@@ -357,272 +63,60 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
}
|
||||
};
|
||||
|
||||
$scope.appError = {
|
||||
app: {}
|
||||
};
|
||||
|
||||
$scope.appUpdate = {
|
||||
busy: false,
|
||||
error: {},
|
||||
$scope.appInfo = {
|
||||
app: {},
|
||||
manifest: {},
|
||||
portBindings: {}
|
||||
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.reset = function () {
|
||||
// close all dialogs
|
||||
$('#appErrorModal').modal('hide');
|
||||
$('#appConfigureModal').modal('hide');
|
||||
$('#appRestoreModal').modal('hide');
|
||||
$('#appUpdateModal').modal('hide');
|
||||
$('#appInfoModal').modal('hide');
|
||||
$('#appUninstallModal').modal('hide');
|
||||
$('#appPostInstallConfirmModal').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';
|
||||
|
||||
// post install confirmation dialog
|
||||
$scope.appPostInstallConfirm.app = {};
|
||||
$scope.appPostInstallConfirm.message = '';
|
||||
$scope.appPostInstallConfirm.confirmed = false;
|
||||
$scope.showAppConfigure = function (app, view) {
|
||||
$location.path('/app/' + app.id + '/' + view);
|
||||
};
|
||||
|
||||
$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.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');
|
||||
Client.refreshAppCache($scope.appUninstall.app.id); // reflect the new app state immediately
|
||||
$scope.reset();
|
||||
}
|
||||
|
||||
$scope.appUninstall.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$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.refreshAppCache($scope.appUpdate.app.id); // 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(); // refresh the new list immediately when switching from another view (appstore)
|
||||
setTimeout(function () { $('#appSearch').focus(); }, 1);
|
||||
|
||||
$scope.spacesSuffix = $scope.user.username.replace(/\./g, '-');
|
||||
// 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.admin || $scope.config.features.spaces) {
|
||||
fetchUsers();
|
||||
fetchGroups();
|
||||
getDomains();
|
||||
if ($scope.user.admin) getBackupConfig(); // FIXME: detect disabled backups some other way
|
||||
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(',');
|
||||
}
|
||||
|
||||
var refreshAppsTimer = $interval(Client.refreshInstalledApps.bind(Client), 5000);
|
||||
Client.getDomains(function (error, result) {
|
||||
if (error) Client.error(error);
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
$interval.cancel(refreshAppsTimer);
|
||||
$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();
|
||||
|
||||
@@ -1,169 +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">
|
||||
<!-- the admin check is to check for spaces user -->
|
||||
<span ng-if="user.admin">{{ (appInstall.location ? (appInstall.domain.config.hyphenatedSubdomains ? '-' : '.') : '') + appInstall.domain.domain }}</span>
|
||||
<span ng-if="!user.admin">{{ (appInstall.location ? '-' : '') + spacesSuffix + (appInstall.domain.config.hyphenatedSubdomains ? '-' : '.') + 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>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- for spaces users, the User management is hidden. thus the admin flag check -->
|
||||
<div class="form-group" ng-show="user.admin && 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="user.admin && 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="user.admin && !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:
|
||||
<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:
|
||||
<multiselect ng-model="appInstall.accessRestriction.groups" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" options="group.name for group in groups" 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 allows 2 apps. Please sign up for a paid plan to install more apps.</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:
|
||||
<multiselect ng-model="appInstall.accessRestriction.users" ng-disabled="appInstall.accessRestrictionOption !== 'groups'" options="user.username for user in users" data-multiple="true" filter-after-rows="5" scroll-after-rows="10"></multiselect>
|
||||
</div>
|
||||
|
||||
<div class="col-md-5">
|
||||
Groups:
|
||||
<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"data-dismiss="modal">Close</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'" ng-click="appInstall.showForm()">Install</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.state === 'installForm'" 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 -->
|
||||
@@ -184,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>
|
||||
@@ -229,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 & 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>
|
||||
@@ -239,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/>
|
||||
@@ -254,65 +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 === 'document' }" category="document"><i class="fa fa-file-word-o"></i> Documents</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'email' }" category="email"><i class="fa fa-envelope-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>
|
||||
|
||||