Compare commits
1762 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f85f6eab9e | ||
|
|
b6dc8b507d | ||
|
|
831308ee05 | ||
|
|
eb5bcb759f | ||
|
|
8286570811 | ||
|
|
10b511f0ed | ||
|
|
751e335bc0 | ||
|
|
cb107521f2 | ||
|
|
e56af57b74 | ||
|
|
a2a1cbab6e | ||
|
|
306a021a8d | ||
|
|
d8c414af2f | ||
|
|
ec4c76b2e0 | ||
|
|
e23b8a6891 | ||
|
|
34006bcbf6 | ||
|
|
ed9aeabf6a | ||
|
|
799fc5089f | ||
|
|
683d510aa6 | ||
|
|
ebd7e58f61 | ||
|
|
9a498b54ac | ||
|
|
2687f45e6e | ||
|
|
f79a17fcec | ||
|
|
8fd377d1e2 | ||
|
|
fda06fbd29 | ||
|
|
cee4378e6d | ||
|
|
ab6d342886 | ||
|
|
9954c08993 | ||
|
|
3ae80aeab3 | ||
|
|
2a3534f659 | ||
|
|
fc39de0d5a | ||
|
|
64e4b79d41 | ||
|
|
53887da3da | ||
|
|
7c60d68f56 | ||
|
|
2ac1b991b1 | ||
|
|
8257714cdb | ||
|
|
1b8bacbf5a | ||
|
|
1d5b84389d | ||
|
|
f7dcf52977 | ||
|
|
e26dd5147a | ||
|
|
bb8f96c2e2 | ||
|
|
95d4cc9055 | ||
|
|
cb84a85f8b | ||
|
|
0a8aa2ecf5 | ||
|
|
5941321e84 | ||
|
|
8cf62280f4 | ||
|
|
4cea142b57 | ||
|
|
64d9245bc4 | ||
|
|
2d78c0c4c3 | ||
|
|
aa585e2d25 | ||
|
|
325ab17dcc | ||
|
|
443ea44bcd | ||
|
|
07958d8efa | ||
|
|
f19affe599 | ||
|
|
f7b7c27b6c | ||
|
|
c7af5b384c | ||
|
|
436a9dfc14 | ||
|
|
1d6d8ccb28 | ||
|
|
7d0862ecfd | ||
|
|
7de059919b | ||
|
|
dfd1fb86cb | ||
|
|
847a92433f | ||
|
|
53af4df47b | ||
|
|
09db7c999e | ||
|
|
1b4c958aba | ||
|
|
9368d5df01 | ||
|
|
f3b5026190 | ||
|
|
19dcd81639 | ||
|
|
d38c171151 | ||
|
|
af3049925f | ||
|
|
a79825d18c | ||
|
|
c4b456b470 | ||
|
|
ccdf28767a | ||
|
|
2561f7d793 | ||
|
|
57bd8c1a49 | ||
|
|
8387e4ae04 | ||
|
|
5c02935017 | ||
|
|
726ffb9b1b | ||
|
|
5dcc3f4076 | ||
|
|
4639d7872f | ||
|
|
71cb6af8c4 | ||
|
|
52060301bd | ||
|
|
dfa3e6d8e4 | ||
|
|
f38f3fe5c9 | ||
|
|
24bf031270 | ||
|
|
eeadd72e1f | ||
|
|
e4139bab04 | ||
|
|
7c7205849b | ||
|
|
03b2b13f14 | ||
|
|
8caf9f7fde | ||
|
|
5b8a5ac6b6 | ||
|
|
4429bed1cf | ||
|
|
b9beda3484 | ||
|
|
354c9bc927 | ||
|
|
a2d88f7fbf | ||
|
|
83cad000e7 | ||
|
|
1b78791aa9 | ||
|
|
2b05fbf6a0 | ||
|
|
7b677cddaf | ||
|
|
18a8fcaa70 | ||
|
|
dd1bc757d5 | ||
|
|
27ca0fdfcc | ||
|
|
578de05a40 | ||
|
|
cd3e1d6bd4 | ||
|
|
de160bb51b | ||
|
|
14417e14c0 | ||
|
|
f2d8b4e444 | ||
|
|
b7c41fee28 | ||
|
|
4f0678d6a2 | ||
|
|
880c624935 | ||
|
|
0fa0902262 | ||
|
|
a2ab3ccaee | ||
|
|
77a0d1c2ff | ||
|
|
7fdb022819 | ||
|
|
878a630b69 | ||
|
|
fbcfc69983 | ||
|
|
a1bd327524 | ||
|
|
e62829debd | ||
|
|
d9d669964f | ||
|
|
ced17b632a | ||
|
|
0aada62a5a | ||
|
|
2fece7a8fe | ||
|
|
6680373c76 | ||
|
|
68ae43fd72 | ||
|
|
6b6f452d06 | ||
|
|
7153ff17e8 | ||
|
|
0fde5a1b3d | ||
|
|
b17fbdd19b | ||
|
|
61ae522486 | ||
|
|
bd414ae9f2 | ||
|
|
7579db5876 | ||
|
|
994ce8dab2 | ||
|
|
e8a84dce7d | ||
|
|
fdca9eda90 | ||
|
|
e007009a00 | ||
|
|
1d5cc209dd | ||
|
|
09b18e1563 | ||
|
|
363db0edea | ||
|
|
e500240a35 | ||
|
|
6694977b87 | ||
|
|
b173dc1f28 | ||
|
|
aa91f5649f | ||
|
|
74efd563ab | ||
|
|
b0b389fb4d | ||
|
|
e2d9131a07 | ||
|
|
3b1c8216b9 | ||
|
|
7f259a43cf | ||
|
|
874a504df3 | ||
|
|
6a4c6318e3 | ||
|
|
09bf2b87dc | ||
|
|
9c5c9838ae | ||
|
|
3924033d9a | ||
|
|
d81e45e456 | ||
|
|
631a762b56 | ||
|
|
a9cf79942f | ||
|
|
bd31476933 | ||
|
|
9b9e4c2ffa | ||
|
|
ec93daac7e | ||
|
|
3431b2dfb1 | ||
|
|
4270abaf1c | ||
|
|
0bd288afbd | ||
|
|
b72d5d50a1 | ||
|
|
d51889c233 | ||
|
|
332b093ee9 | ||
|
|
8be332208f | ||
|
|
85d1188628 | ||
|
|
56896996c3 | ||
|
|
896374e069 | ||
|
|
ac36505fb2 | ||
|
|
d36df1a8ae | ||
|
|
edd939c069 | ||
|
|
f80225ba54 | ||
|
|
3ccd87b369 | ||
|
|
59d3dd9255 | ||
|
|
392f08059d | ||
|
|
0d5c9a2bba | ||
|
|
f27de8015b | ||
|
|
3b7bdee814 | ||
|
|
397ed9d581 | ||
|
|
a098880669 | ||
|
|
047d4cb650 | ||
|
|
736904c579 | ||
|
|
e883c668b5 | ||
|
|
14181aa8a7 | ||
|
|
0b1ba99afa | ||
|
|
88a6215939 | ||
|
|
d5ebd33038 | ||
|
|
b2ac214c0f | ||
|
|
3dee41a511 | ||
|
|
934818c07d | ||
|
|
4dc614a58e | ||
|
|
35ea095b75 | ||
|
|
ae1a4c73b3 | ||
|
|
b58dbe89be | ||
|
|
1b4551b622 | ||
|
|
72a8f819d3 | ||
|
|
df8e16379c | ||
|
|
7dbbc7e25c | ||
|
|
1eaae70adb | ||
|
|
d4a61782c4 | ||
|
|
0e39c6f895 | ||
|
|
fcc3ede485 | ||
|
|
f001e7e713 | ||
|
|
3fb8fae821 | ||
|
|
349f3185c5 | ||
|
|
b457b8409f | ||
|
|
c61e5e1ac8 | ||
|
|
9e60f9d9fd | ||
|
|
a95d40078f | ||
|
|
515798bd9f | ||
|
|
20b28135a3 | ||
|
|
33d2b8bbeb | ||
|
|
7c2059af2b | ||
|
|
1e0f57bd1a | ||
|
|
9484f1dbe6 | ||
|
|
658766c9e4 | ||
|
|
9e47d9acf1 | ||
|
|
5a1247c021 | ||
|
|
a8b7972f3c | ||
|
|
836c2127f7 | ||
|
|
6cef200aed | ||
|
|
c9c80b1d62 | ||
|
|
04328bc2d1 | ||
|
|
d909d0eeeb | ||
|
|
80f5e913ec | ||
|
|
57eca31a9c | ||
|
|
c645fc7ad1 | ||
|
|
78b524b2e8 | ||
|
|
1ff1b6931b | ||
|
|
29ac883616 | ||
|
|
467a147603 | ||
|
|
7b49b6304c | ||
|
|
25991027b9 | ||
|
|
fce83dfa66 | ||
|
|
8603d5d468 | ||
|
|
bd17f85140 | ||
|
|
037dddb945 | ||
|
|
dd2151e611 | ||
|
|
a1e0cdadd6 | ||
|
|
8d36efa66c | ||
|
|
b9a12a6dcc | ||
|
|
08e4fe9990 | ||
|
|
2333fec181 | ||
|
|
05676a78e3 | ||
|
|
02aaae240c | ||
|
|
158924fe3c | ||
|
|
0341b926b9 | ||
|
|
69d1f93ea4 | ||
|
|
423fb56ae0 | ||
|
|
c5fc8d437f | ||
|
|
0811addf9c | ||
|
|
e6f8108dc0 | ||
|
|
4aa4a8c75d | ||
|
|
bbe0467d16 | ||
|
|
88ca69138b | ||
|
|
6a0d9c8805 | ||
|
|
1a57f9f134 | ||
|
|
109aedd3ae | ||
|
|
bd9f9344e5 | ||
|
|
5190873e99 | ||
|
|
c5fe7eb0dd | ||
|
|
fef1b14d69 | ||
|
|
472fc02533 | ||
|
|
ed29524cf3 | ||
|
|
69f35436c2 | ||
|
|
a0ca1cddb5 | ||
|
|
be4ffd8308 | ||
|
|
8e246f08ee | ||
|
|
73eda65300 | ||
|
|
be4df02844 | ||
|
|
7de461319f | ||
|
|
970fc16aab | ||
|
|
5db2c5804d | ||
|
|
6c2924a08a | ||
|
|
32511fe6a0 | ||
|
|
94d5b0f083 | ||
|
|
0e957b9566 | ||
|
|
ec93f21f0a | ||
|
|
bbc4f3beb4 | ||
|
|
c271a25a51 | ||
|
|
c986bf0c46 | ||
|
|
6f994b75e5 | ||
|
|
a227039260 | ||
|
|
ee38c07a3f | ||
|
|
9678ebd71e | ||
|
|
82ce0d3461 | ||
|
|
8315c79ef7 | ||
|
|
69cb6d30b5 | ||
|
|
f4beef514e | ||
|
|
f002677134 | ||
|
|
6270d2d3af | ||
|
|
83625e4ba7 | ||
|
|
d039112b5b | ||
|
|
d8481af288 | ||
|
|
ea902c1073 | ||
|
|
db62c18a39 | ||
|
|
d004e2f759 | ||
|
|
1f7e457c64 | ||
|
|
4eae9398d8 | ||
|
|
4766121570 | ||
|
|
3180641e33 | ||
|
|
9273002905 | ||
|
|
3fc9c5ec90 | ||
|
|
3266cea1d6 | ||
|
|
97839c06dc | ||
|
|
304f290e42 | ||
|
|
52a241f300 | ||
|
|
1c1ea0dcc4 | ||
|
|
d998b384e8 | ||
|
|
9184afa6de | ||
|
|
3e1b4d724f | ||
|
|
5b6f50b25b | ||
|
|
b757025359 | ||
|
|
52e97edbd5 | ||
|
|
def88db128 | ||
|
|
d04702e5d4 | ||
|
|
f6407771b5 | ||
|
|
f6a6e125b6 | ||
|
|
2303b8a89f | ||
|
|
93f286b6ac | ||
|
|
75e5f931eb | ||
|
|
b215e89572 | ||
|
|
07a7e8cf0a | ||
|
|
52000edd7d | ||
|
|
3e4c07c86f | ||
|
|
92ce69c603 | ||
|
|
a338e0a3f1 | ||
|
|
143e09b65f | ||
|
|
e5cc5abdc9 | ||
|
|
dfc96ebb99 | ||
|
|
9397d0121d | ||
|
|
d8a1f3c73a | ||
|
|
a07cb425a4 | ||
|
|
a89b33dfdf | ||
|
|
486d33448b | ||
|
|
2299d397cb | ||
|
|
0173c4709f | ||
|
|
42fdf8b61f | ||
|
|
0253723652 | ||
|
|
5ca51d3510 | ||
|
|
466dc0127d | ||
|
|
32f610485c | ||
|
|
429e1b54ee | ||
|
|
268c037487 | ||
|
|
c146f3105e | ||
|
|
81e0c04722 | ||
|
|
5d156695d2 | ||
|
|
f71438347c | ||
|
|
a3081d607f | ||
|
|
ca81f445b9 | ||
|
|
1c22ce6d76 | ||
|
|
a0d482ba88 | ||
|
|
0c050cc053 | ||
|
|
9645d624f2 | ||
|
|
f29cb94d9f | ||
|
|
e239206626 | ||
|
|
35d1065eaf | ||
|
|
8384d6f9d7 | ||
|
|
5b3282ba51 | ||
|
|
bd1043f034 | ||
|
|
c847dcec15 | ||
|
|
f66994f0b5 | ||
|
|
eba27f1823 | ||
|
|
ad1bbb2a00 | ||
|
|
42506ab37d | ||
|
|
6bae33826d | ||
|
|
914c2b89c5 | ||
|
|
e79926cf29 | ||
|
|
1f15d2c736 | ||
|
|
fadd27fd23 | ||
|
|
d5aeb8db55 | ||
|
|
d5dbdd9986 | ||
|
|
352c977dc7 | ||
|
|
bf008eba99 | ||
|
|
76b7777fff | ||
|
|
9292d990da | ||
|
|
87fe715823 | ||
|
|
25e32e0600 | ||
|
|
41c901a05c | ||
|
|
fdaba2faf4 | ||
|
|
0c73ad4f46 | ||
|
|
36c44bc3d4 | ||
|
|
d612598bd0 | ||
|
|
2d75b6086f | ||
|
|
3345674604 | ||
|
|
1eeaeeeca5 | ||
|
|
b0bea8b3ba | ||
|
|
0e3e5edd17 | ||
|
|
ec1287a2f4 | ||
|
|
9d2c857c59 | ||
|
|
077f4f201c | ||
|
|
9a2154a2ce | ||
|
|
f4c111c1c2 | ||
|
|
9483a06e8a | ||
|
|
df2a90dc1d | ||
|
|
246c190ccd | ||
|
|
4640817a14 | ||
|
|
9c7690d39b | ||
|
|
160805af05 | ||
|
|
39e85730f0 | ||
|
|
da692e1a92 | ||
|
|
23bc60f1ac | ||
|
|
ce0f759509 | ||
|
|
1793e5943a | ||
|
|
201b5db155 | ||
|
|
c588ac6777 | ||
|
|
28c01fd4e1 | ||
|
|
0715e7a31f | ||
|
|
b497c38e34 | ||
|
|
8a08dce405 | ||
|
|
a620c348bf | ||
|
|
fe750b7270 | ||
|
|
4aa9d56dfc | ||
|
|
4b2ebf2a3a | ||
|
|
6290446ea5 | ||
|
|
42f5d06960 | ||
|
|
85e8006137 | ||
|
|
7c29d4c644 | ||
|
|
7378bc852d | ||
|
|
67ed137cfa | ||
|
|
02e08e54a2 | ||
|
|
6b2dd24334 | ||
|
|
131d5becad | ||
|
|
157e0a83b1 | ||
|
|
d0d3abce3e | ||
|
|
b965c41a45 | ||
|
|
42f824e034 | ||
|
|
2768d9d49d | ||
|
|
c9a86dcae3 | ||
|
|
e1d307ea2c | ||
|
|
98ece12ae8 | ||
|
|
4cf3db7c2a | ||
|
|
7c2f79d980 | ||
|
|
fe064f8b6a | ||
|
|
2bad2f6b80 | ||
|
|
2ba9c5193f | ||
|
|
331695c10a | ||
|
|
f299193f05 | ||
|
|
de8130abc2 | ||
|
|
1cbde7f2e1 | ||
|
|
d1c796d9a7 | ||
|
|
af43061353 | ||
|
|
1b2ca8e69e | ||
|
|
a9e6679b08 | ||
|
|
9408760122 | ||
|
|
c25e804d61 | ||
|
|
b18b2262eb | ||
|
|
570440dc7d | ||
|
|
aef660fb2f | ||
|
|
17671c7282 | ||
|
|
8216ab44b4 | ||
|
|
5902d43a94 | ||
|
|
a6eb04d3f9 | ||
|
|
a71780e860 | ||
|
|
c2d815ef66 | ||
|
|
bf4679aa9b | ||
|
|
4dd74bbb16 | ||
|
|
10530146ca | ||
|
|
75d49da3d4 | ||
|
|
af026b0c52 | ||
|
|
bcd4f70d0e | ||
|
|
54cc31d1a0 | ||
|
|
3f0553861a | ||
|
|
e1b3c51d2c | ||
|
|
58a0e3fad6 | ||
|
|
6bb235650a | ||
|
|
0bf0bc4c33 | ||
|
|
3085749e92 | ||
|
|
de3abbf6b8 | ||
|
|
925469689f | ||
|
|
631e58a585 | ||
|
|
63571d06bf | ||
|
|
1979758fab | ||
|
|
6ef5a23000 | ||
|
|
ae4aa23d27 | ||
|
|
710d1f13cd | ||
|
|
57a4d366d7 | ||
|
|
8060c66c08 | ||
|
|
d5a58fbec2 | ||
|
|
059256de3e | ||
|
|
2c52d4c867 | ||
|
|
d9bfde2e47 | ||
|
|
757acd8d92 | ||
|
|
3e92252e2e | ||
|
|
ce7aeb1a27 | ||
|
|
236d2ad39a | ||
|
|
63b37714b1 | ||
|
|
dc81fd0622 | ||
|
|
ccec2bf7ee | ||
|
|
c93b93331e | ||
|
|
1f194f1680 | ||
|
|
2251123c1d | ||
|
|
1bfe1c3370 | ||
|
|
0044eeb6d1 | ||
|
|
669302d46b | ||
|
|
fce2f44197 | ||
|
|
64db1df248 | ||
|
|
9109d55019 | ||
|
|
412e13ccd5 | ||
|
|
0c7e0528b6 | ||
|
|
ccb22a2f40 | ||
|
|
2f3e463aca | ||
|
|
c548e08aea | ||
|
|
150e0171f0 | ||
|
|
bfcaca7bc0 | ||
|
|
fcb0482193 | ||
|
|
714ea7c236 | ||
|
|
cc30799f0d | ||
|
|
df71259a10 | ||
|
|
9a0ae5d4b9 | ||
|
|
eea4648ada | ||
|
|
2883398c2a | ||
|
|
84f6e14b89 | ||
|
|
30fb9ed65a | ||
|
|
a809f2d1f2 | ||
|
|
6b5a19983d | ||
|
|
2471f447b3 | ||
|
|
413e944d7a | ||
|
|
3b952819d6 | ||
|
|
2445c10c1c | ||
|
|
7e26593d04 | ||
|
|
73595c683b | ||
|
|
570f56a4cc | ||
|
|
f9e940871e | ||
|
|
c2b724a54a | ||
|
|
f52db472ed | ||
|
|
324fe98a5b | ||
|
|
2fd9833580 | ||
|
|
0ab4827d6f | ||
|
|
f0bd7d7eee | ||
|
|
8cd1209602 | ||
|
|
e2781adc81 | ||
|
|
b91ac2fe89 | ||
|
|
683a7a1851 | ||
|
|
8b05aa7b59 | ||
|
|
883f839bfd | ||
|
|
90dc00ac6b | ||
|
|
9c1cecbb7d | ||
|
|
1fce11bfba | ||
|
|
fba3ebdf49 | ||
|
|
e9585e08a4 | ||
|
|
24a89985fb | ||
|
|
33026e8281 | ||
|
|
50d9b832a9 | ||
|
|
1df82c3380 | ||
|
|
0a8db4ebbf | ||
|
|
928b19aef4 | ||
|
|
a7ec98cef6 | ||
|
|
67326a1859 | ||
|
|
631a8a5edf | ||
|
|
7e5e463ef2 | ||
|
|
add65e41da | ||
|
|
351b4571cd | ||
|
|
793258a91f | ||
|
|
ed3e1933c3 | ||
|
|
2f63b26458 | ||
|
|
f1d14da3dd | ||
|
|
4a3d90bdf3 | ||
|
|
f8f24fbc37 | ||
|
|
8f6c53e111 | ||
|
|
c23f55b1d4 | ||
|
|
88f94f5d6f | ||
|
|
3e99a179b7 | ||
|
|
c7271f94a5 | ||
|
|
5c0ced942c | ||
|
|
d9b07e76f9 | ||
|
|
c75793df20 | ||
|
|
1bee5121f0 | ||
|
|
932e7eb374 | ||
|
|
abdbcfe42b | ||
|
|
a62e888732 | ||
|
|
16b982b953 | ||
|
|
e92b87095b | ||
|
|
e5f1aa689b | ||
|
|
f39a05cd8d | ||
|
|
c5e22b785a | ||
|
|
11f93a125c | ||
|
|
38a9cb002d | ||
|
|
cf1a38a004 | ||
|
|
d6e823d19d | ||
|
|
763a23d9d0 | ||
|
|
f266577f2f | ||
|
|
1bb5e73668 | ||
|
|
b07bc755f6 | ||
|
|
db4b39c54b | ||
|
|
ffd95261c3 | ||
|
|
82f38040c1 | ||
|
|
7bb4f9f8e3 | ||
|
|
c2345df275 | ||
|
|
b0c341da3f | ||
|
|
b1ccc16da7 | ||
|
|
16856a5911 | ||
|
|
d4ee364349 | ||
|
|
db62ca7b4b | ||
|
|
d0b99b854d | ||
|
|
b3b13b3e01 | ||
|
|
5cb738b82b | ||
|
|
cfe3b15cbe | ||
|
|
a1bde80925 | ||
|
|
4c958dd584 | ||
|
|
a05d4d3d18 | ||
|
|
503b6dc914 | ||
|
|
141cbcd1c0 | ||
|
|
e340d2d3f5 | ||
|
|
436d5a3a66 | ||
|
|
3e04fd4790 | ||
|
|
3f6d149f9d | ||
|
|
977fc7832a | ||
|
|
2e2f0e2e3d | ||
|
|
5628beee72 | ||
|
|
a88fea560b | ||
|
|
5832345b96 | ||
|
|
d3d2daa12f | ||
|
|
f6f90982f4 | ||
|
|
98323b08f0 | ||
|
|
8f3112a5e2 | ||
|
|
3408bd41ad | ||
|
|
68e12e86c1 | ||
|
|
890e0b4906 | ||
|
|
3aca7c7ae5 | ||
|
|
e8077ddbc5 | ||
|
|
6e04907357 | ||
|
|
3d8c9a99fe | ||
|
|
730768705b | ||
|
|
13f75a37ab | ||
|
|
f74c69ea6f | ||
|
|
fe7be0f518 | ||
|
|
2f0e656c45 | ||
|
|
de489b799b | ||
|
|
6250ef49b6 | ||
|
|
afdab8dcde | ||
|
|
8e1d39f37f | ||
|
|
2a8c346a65 | ||
|
|
71e431e744 | ||
|
|
265cb75d70 | ||
|
|
4d0470838a | ||
|
|
d4ed3aeac0 | ||
|
|
89fa89fe98 | ||
|
|
2d663f0ac5 | ||
|
|
bc9b3f1c5c | ||
|
|
2f0a46a46d | ||
|
|
9559604d1e | ||
|
|
b779ab9bc5 | ||
|
|
481943051c | ||
|
|
cb49b7a906 | ||
|
|
090e4b3117 | ||
|
|
9a40d5f784 | ||
|
|
1485637c6d | ||
|
|
b0ec8e26e8 | ||
|
|
e4c12e08cb | ||
|
|
a1675745b5 | ||
|
|
40beb5b104 | ||
|
|
e49e2f51c2 | ||
|
|
99669f2678 | ||
|
|
9aa88b9dad | ||
|
|
10de29795a | ||
|
|
c4767e74f4 | ||
|
|
19f8666e1e | ||
|
|
2e2bbdf0d7 | ||
|
|
973bee0ffa | ||
|
|
9470cdf774 | ||
|
|
ab198ea60b | ||
|
|
c78f4bb6d2 | ||
|
|
ff054ca47f | ||
|
|
30cc804022 | ||
|
|
890d733bf8 | ||
|
|
a554a9c4a1 | ||
|
|
9ef212937d | ||
|
|
82d9c53f3e | ||
|
|
d9d91c4953 | ||
|
|
dd5f5282e0 | ||
|
|
cc116defc6 | ||
|
|
90c755e120 | ||
|
|
85d1c80581 | ||
|
|
5bb04d3857 | ||
|
|
dde8404242 | ||
|
|
7940bd2dcc | ||
|
|
da48d117a0 | ||
|
|
ffc74967fc | ||
|
|
c7388d5836 | ||
|
|
ee4b8fc66f | ||
|
|
c73f22ca45 | ||
|
|
bafa053fd1 | ||
|
|
a00406d2b3 | ||
|
|
df91e17dc6 | ||
|
|
7c6eeababc | ||
|
|
911a5067f9 | ||
|
|
bfa0fe9c51 | ||
|
|
7dafe31d51 | ||
|
|
1271ecedb4 | ||
|
|
80be18068f | ||
|
|
ecbf2c0958 | ||
|
|
cfb84b677f | ||
|
|
dfbd7a5d76 | ||
|
|
8e157f8ff7 | ||
|
|
c75580e852 | ||
|
|
e21934fd55 | ||
|
|
844f1609c8 | ||
|
|
6f7de28672 | ||
|
|
247212d768 | ||
|
|
6592b880ea | ||
|
|
e27a4bf119 | ||
|
|
dce5a83093 | ||
|
|
78c70f35a1 | ||
|
|
f4b84a0902 | ||
|
|
b16237b514 | ||
|
|
b0077539fa | ||
|
|
63602216eb | ||
|
|
2f93935009 | ||
|
|
c0a0b653c1 | ||
|
|
24d6354467 | ||
|
|
54a07469fd | ||
|
|
12f37bead1 | ||
|
|
8d76dc2511 | ||
|
|
8ef7d6defc | ||
|
|
2f2cbbe3f0 | ||
|
|
2662ac719b | ||
|
|
1b0fe6e847 | ||
|
|
9fa9e26324 | ||
|
|
cb434df099 | ||
|
|
e3cb3002fe | ||
|
|
8b2a09b522 | ||
|
|
03265d2545 | ||
|
|
9b25e07b5e | ||
|
|
60a21ca58d | ||
|
|
05172492ef | ||
|
|
a9c089a994 | ||
|
|
988d018c8e | ||
|
|
853c611fde | ||
|
|
0886f5335f | ||
|
|
024d481a4d | ||
|
|
b80de1af95 | ||
|
|
da44268f0d | ||
|
|
b921a3ed8e | ||
|
|
d2c9c824f9 | ||
|
|
21c255051c | ||
|
|
dccf09c5dd | ||
|
|
0772a17e4c | ||
|
|
644ee782d2 | ||
|
|
ea180cc415 | ||
|
|
68e3c4e6ed | ||
|
|
47b1c2d680 | ||
|
|
2e513043b7 | ||
|
|
ea942635f7 | ||
|
|
8a5f5cc673 | ||
|
|
6cc673035d | ||
|
|
02d717e5a8 | ||
|
|
77d83b06bd | ||
|
|
e3d8eabb05 | ||
|
|
0596b0106f | ||
|
|
765bbd90fc | ||
|
|
3799902a8a | ||
|
|
ca1dbb4556 | ||
|
|
df551d457c | ||
|
|
278d518d8f | ||
|
|
b06fa191f7 | ||
|
|
13d73f6f27 | ||
|
|
d59af117c0 | ||
|
|
69efd85ad6 | ||
|
|
ead51787a8 | ||
|
|
4bf0fee20d | ||
|
|
209693ee3f | ||
|
|
796d4f5b08 | ||
|
|
96914c9901 | ||
|
|
9e6073bf56 | ||
|
|
f8ad58159c | ||
|
|
ede4a02315 | ||
|
|
e25faba990 | ||
|
|
4ccf272e53 | ||
|
|
5ad0951db3 | ||
|
|
f72fcb76e3 | ||
|
|
8ca00c81b2 | ||
|
|
5c7f87b8ae | ||
|
|
9f4f7ec88c | ||
|
|
ba7676f778 | ||
|
|
ac248c32bb | ||
|
|
dd6a3e8535 | ||
|
|
69b538cfd6 | ||
|
|
e248c22f4b | ||
|
|
eff3c43483 | ||
|
|
1cbfc3ccd4 | ||
|
|
2fa72838f9 | ||
|
|
a559ec1fda | ||
|
|
8ddbc8b1fb | ||
|
|
c650a43d38 | ||
|
|
ab4cc20c8c | ||
|
|
30d613ff04 | ||
|
|
b93da5a281 | ||
|
|
9e2cf67a93 | ||
|
|
e87720ffdd | ||
|
|
5108ed7da5 | ||
|
|
77936d86f2 | ||
|
|
cbe8927d73 | ||
|
|
47a7e98da8 | ||
|
|
d4f27cd2e0 | ||
|
|
c355ad7a86 | ||
|
|
c8c9ec081d | ||
|
|
1289e46401 | ||
|
|
0b54292130 | ||
|
|
d1e2b91116 | ||
|
|
cd96b3e8f6 | ||
|
|
9119f4d06f | ||
|
|
4e6ccf2c81 | ||
|
|
6294e43762 | ||
|
|
feca78e55d | ||
|
|
0c86526ad2 | ||
|
|
9fd1d26067 | ||
|
|
5ec02078d1 | ||
|
|
012c6f3d41 | ||
|
|
8e89d492fc | ||
|
|
69fe2f0443 | ||
|
|
ebd7f4ae1b | ||
|
|
f9ca4f339e | ||
|
|
5919020e1c | ||
|
|
9d09c2356d | ||
|
|
3cfb597fc6 | ||
|
|
03f2da19e5 | ||
|
|
951e62d984 | ||
|
|
146039c4c5 | ||
|
|
98a216fdb9 | ||
|
|
28ea09a0c4 | ||
|
|
a98a772360 | ||
|
|
4f1da8a24b | ||
|
|
cb83e71f2b | ||
|
|
646d174616 | ||
|
|
34cf78fd33 | ||
|
|
457e1bee2f | ||
|
|
6b95c63c1e | ||
|
|
e965f222db | ||
|
|
d1591bc01c | ||
|
|
a72846be7a | ||
|
|
5072661369 | ||
|
|
5417a83662 | ||
|
|
23be006932 | ||
|
|
e28553767e | ||
|
|
c1f64c043d | ||
|
|
9e6d0183d4 | ||
|
|
8dc7f3fb9e | ||
|
|
ac7b3b9b67 | ||
|
|
af0ebba5db | ||
|
|
b454709e5e | ||
|
|
78684607bd | ||
|
|
3790cad5e5 | ||
|
|
77b7c091a8 | ||
|
|
450ad62fa9 | ||
|
|
1d138c33a4 | ||
|
|
fb2a0e4a1e | ||
|
|
da3db0b0f9 | ||
|
|
bc8aaadd90 | ||
|
|
e6054a4971 | ||
|
|
26548e1929 | ||
|
|
4424cf8190 | ||
|
|
67c1aacd54 | ||
|
|
39b046f18b | ||
|
|
d630f04872 | ||
|
|
35403c87bd | ||
|
|
f2b247e042 | ||
|
|
94022bd9f2 | ||
|
|
ccb2abb950 | ||
|
|
cbafc15292 | ||
|
|
e12c52294a | ||
|
|
63b529da00 | ||
|
|
e4be2fd19e | ||
|
|
269bf4feec | ||
|
|
9d4c4a1e2b | ||
|
|
2e41fdcb41 | ||
|
|
2e48218623 | ||
|
|
8576a54056 | ||
|
|
79d924f920 | ||
|
|
305beb3af8 | ||
|
|
06fceded14 | ||
|
|
0eadfd5a58 | ||
|
|
eea34a4f6c | ||
|
|
500ec36522 | ||
|
|
ca525bd08c | ||
|
|
ac2ffc4586 | ||
|
|
5781269557 | ||
|
|
e4422b9fe7 | ||
|
|
269f76d546 | ||
|
|
540e3f0aaa | ||
|
|
5f64ae28e0 | ||
|
|
ece364c823 | ||
|
|
f669f64fcb | ||
|
|
2a53ed93c4 | ||
|
|
2b731fb30c | ||
|
|
be2db2dd8e | ||
|
|
09c08df1b9 | ||
|
|
9ccd3438ad | ||
|
|
393bcbcca5 | ||
|
|
7fac0958b8 | ||
|
|
c6a0874b3b | ||
|
|
9c80470185 | ||
|
|
2034445f5b | ||
|
|
fd8da5ffba | ||
|
|
e987af87f6 | ||
|
|
0074cc3933 | ||
|
|
5f2ce89316 | ||
|
|
60492c48a6 | ||
|
|
eed2d70017 | ||
|
|
b859adaa8c | ||
|
|
89a587f9ae | ||
|
|
fb56bcff80 | ||
|
|
99eb6907dd | ||
|
|
3743fad899 | ||
|
|
c1e59a7e03 | ||
|
|
b34dee1f83 | ||
|
|
6edd65ad8f | ||
|
|
0959ca6a40 | ||
|
|
1287fa2cd0 | ||
|
|
a5a07f250d | ||
|
|
089fb526f5 | ||
|
|
af58b7593a | ||
|
|
d4508b25ce | ||
|
|
9edc218eaa | ||
|
|
3790f753aa | ||
|
|
82b30d8388 | ||
|
|
e9a0dc7826 | ||
|
|
8ce3a4f904 | ||
|
|
1d42e9c348 | ||
|
|
2340a6bc37 | ||
|
|
be0b9c7e53 | ||
|
|
6d75cd9025 | ||
|
|
345d6f369e | ||
|
|
959ea86d85 | ||
|
|
b67a99af3d | ||
|
|
fa3b848d40 | ||
|
|
0f971e9e7d | ||
|
|
c17f76c009 | ||
|
|
bf23b5d295 | ||
|
|
09c7256d42 | ||
|
|
eaee8a2fbb | ||
|
|
3b18dd67be | ||
|
|
c3f87b4248 | ||
|
|
f6b91ad652 | ||
|
|
1c79edc52f | ||
|
|
fe2dfd0e8f | ||
|
|
fa6056c1b1 | ||
|
|
d5762c7ad8 | ||
|
|
d9c9dd2a4f | ||
|
|
3a4d945c68 | ||
|
|
f4a364816b | ||
|
|
931bc03cab | ||
|
|
1abd4937cd | ||
|
|
0df8b51c62 | ||
|
|
e5b7190015 | ||
|
|
279b8aacf6 | ||
|
|
9eebaab2f4 | ||
|
|
16e9d60033 | ||
|
|
335b378e9a | ||
|
|
9c41bc33a3 | ||
|
|
7f7d6b4d5d | ||
|
|
cc0e3bbce0 | ||
|
|
2eead65fef | ||
|
|
e664be451f | ||
|
|
d2d8160096 | ||
|
|
3bd503c28d | ||
|
|
aa1df8eb33 | ||
|
|
c1aace45ae | ||
|
|
217a60aadc | ||
|
|
5654f528ca | ||
|
|
4da036a064 | ||
|
|
2256b3d262 | ||
|
|
d84ecc307d | ||
|
|
237313d5fb | ||
|
|
7caf766bca | ||
|
|
0a3f9f5ef1 | ||
|
|
e890b8f7c1 | ||
|
|
dc4d5f0ecb | ||
|
|
a2f0980731 | ||
|
|
0a5c029f8b | ||
|
|
85bb79e4fb | ||
|
|
f18d1e50f8 | ||
|
|
943b10dd5d | ||
|
|
0a48e17c88 | ||
|
|
da1381e14e | ||
|
|
bdffb0ee10 | ||
|
|
7bdb7d2ca8 | ||
|
|
92567561b8 | ||
|
|
335bdcd89d | ||
|
|
4c2fc13abb | ||
|
|
7f8f29daa2 | ||
|
|
8fac845ecb | ||
|
|
d8076e7630 | ||
|
|
b370bc27c4 | ||
|
|
334c3f4488 | ||
|
|
33822109c0 | ||
|
|
b1f18b0f5b | ||
|
|
c0d6284368 | ||
|
|
16e294f6fc | ||
|
|
5f7925b2b8 | ||
|
|
2e001b0ce4 | ||
|
|
c1ca3ff5b5 | ||
|
|
1de33cd4ca | ||
|
|
77b773388f | ||
|
|
3e668ee439 | ||
|
|
0d3ea9af36 | ||
|
|
0d81bc8056 | ||
|
|
e4b532a34d | ||
|
|
61f86c0ac3 | ||
|
|
66fad37116 | ||
|
|
c7bbd8c823 | ||
|
|
dc6f8baf1e | ||
|
|
0e76e65d65 | ||
|
|
877dbed999 | ||
|
|
668fd05fae | ||
|
|
3e49998f41 | ||
|
|
99b183ac17 | ||
|
|
bb04cddc48 | ||
|
|
ba3f095dd8 | ||
|
|
f8438421c8 | ||
|
|
334361860b | ||
|
|
23bd211758 | ||
|
|
4cc8fb9891 | ||
|
|
ed3cd690fe | ||
|
|
9a40c7cdc6 | ||
|
|
ae4c9ce819 | ||
|
|
2f8bae1356 | ||
|
|
2dfcd5b7ef | ||
|
|
739926f64e | ||
|
|
a18dde07de | ||
|
|
6480017a91 | ||
|
|
4467ec52f7 | ||
|
|
072d82a10e | ||
|
|
b50b759f4f | ||
|
|
c1477ad45f | ||
|
|
679d45399b | ||
|
|
0e15a789ff | ||
|
|
dff5b3f497 | ||
|
|
483e49a6ae | ||
|
|
a689b5b917 | ||
|
|
1363d98280 | ||
|
|
405f3dcbdd | ||
|
|
4b48408bc9 | ||
|
|
d6f1e2d7e2 | ||
|
|
c04c8e3aa4 | ||
|
|
ff5a08d3b0 | ||
|
|
d2049c759e | ||
|
|
f940cb0ace | ||
|
|
507f2f4af4 | ||
|
|
deca7099f3 | ||
|
|
46a741825a | ||
|
|
14fdcd3052 | ||
|
|
c76b01608a | ||
|
|
be709d6601 | ||
|
|
7dc8fac198 | ||
|
|
29ae7d57fd | ||
|
|
1bf9ce872b | ||
|
|
0071c9504f | ||
|
|
594a872c84 | ||
|
|
0d1f78e82e | ||
|
|
a12de51897 | ||
|
|
74fa084dd0 | ||
|
|
a1ee258da5 | ||
|
|
c1af171c5d | ||
|
|
e36c9560fa | ||
|
|
5cd19ddc8d | ||
|
|
ac243e5d11 | ||
|
|
b480d019f6 | ||
|
|
cc13ab97d6 | ||
|
|
fee1d2ed04 | ||
|
|
4144d5faa6 | ||
|
|
38a23c0bee | ||
|
|
8fcfebe170 | ||
|
|
f917fa8138 | ||
|
|
f60b611304 | ||
|
|
a54624e5c8 | ||
|
|
370b14b82e | ||
|
|
88205adeb2 | ||
|
|
351ce995d9 | ||
|
|
a9aa92de90 | ||
|
|
9ea665dea2 | ||
|
|
30b52527e7 | ||
|
|
bb4125153b | ||
|
|
f0d5b2b1da | ||
|
|
30c4048e4a | ||
|
|
93770ca9ce | ||
|
|
e788783d12 | ||
|
|
1314444d7c | ||
|
|
2a14664d34 | ||
|
|
07a03940a0 | ||
|
|
5f2f6fff56 | ||
|
|
353548660c | ||
|
|
912f07225c | ||
|
|
a23b7eeff1 | ||
|
|
574f0d71b2 | ||
|
|
a368312035 | ||
|
|
2f88b1ab65 | ||
|
|
f85f97e061 | ||
|
|
30dec13903 | ||
|
|
3057e5c997 | ||
|
|
6c413eb1ba | ||
|
|
dcdd9132e2 | ||
|
|
d9b1c36055 | ||
|
|
a593a247d7 | ||
|
|
155debc864 | ||
|
|
a5975ac38b | ||
|
|
204f1cfd6b | ||
|
|
2d22e043a0 | ||
|
|
c26cacaf4e | ||
|
|
f0048544e2 | ||
|
|
cf227dbfa2 | ||
|
|
a5f8bdbe61 | ||
|
|
e442553c6f | ||
|
|
7134acfcd6 | ||
|
|
82439f444e | ||
|
|
1a17908488 | ||
|
|
9af30e99f8 | ||
|
|
6f942c3417 | ||
|
|
57083c90cd | ||
|
|
e28bcdd978 | ||
|
|
0b4a5ab2eb | ||
|
|
034704a330 | ||
|
|
5c60eaf6ab | ||
|
|
f5709eac2c | ||
|
|
5a5e714aca | ||
|
|
747d48e4d9 | ||
|
|
07a0200f30 | ||
|
|
1c5313f2d9 | ||
|
|
05e08719fb | ||
|
|
ca0e616f88 | ||
|
|
a8d20caba4 | ||
|
|
0d4bbb0a48 | ||
|
|
b9cc219530 | ||
|
|
e204ab5871 | ||
|
|
16d0c05b4b | ||
|
|
6f8329d191 | ||
|
|
d751463b26 | ||
|
|
d3b66eff59 | ||
|
|
4257d0332a | ||
|
|
ccbcaa94fe | ||
|
|
b80442c061 | ||
|
|
3a0f6820ad | ||
|
|
1bc92f5363 | ||
|
|
818ddcf01e | ||
|
|
618ba361c7 | ||
|
|
599160a325 | ||
|
|
35fba6f4ed | ||
|
|
a14aad75fd | ||
|
|
3513e85b0b | ||
|
|
66c0390fc7 | ||
|
|
a6549ccb08 | ||
|
|
15d2878014 | ||
|
|
d271be8723 | ||
|
|
6f9d2d99dd | ||
|
|
5d62664ee3 | ||
|
|
7124d9f2f8 | ||
|
|
0459744771 | ||
|
|
417544b781 | ||
|
|
f9028cb366 | ||
|
|
0ffa9d4225 | ||
|
|
9a264719a9 | ||
|
|
96c213dcc4 | ||
|
|
dec1a8e204 | ||
|
|
a17fd697e2 | ||
|
|
a6ab66e799 | ||
|
|
17095ec3c6 | ||
|
|
82687147b8 | ||
|
|
ba76422c1f | ||
|
|
083b3c4ece | ||
|
|
5ecfdf38a8 | ||
|
|
dd1acf3c2a | ||
|
|
76e9c2d196 | ||
|
|
15f046959d | ||
|
|
bf3ba04624 | ||
|
|
d997894d9a | ||
|
|
c1059db6e5 | ||
|
|
8ad29a2836 | ||
|
|
93a454b835 | ||
|
|
da899386ec | ||
|
|
05d22903ea | ||
|
|
33945520f1 | ||
|
|
40284809cf | ||
|
|
efc18aaaec | ||
|
|
348441b046 | ||
|
|
66601b2e7c | ||
|
|
724c5e4b73 | ||
|
|
7eff29bc65 | ||
|
|
ca002003c2 | ||
|
|
f0675f1f3c | ||
|
|
976186c525 | ||
|
|
89d5777e52 | ||
|
|
8dbb69809c | ||
|
|
7348bd5d15 | ||
|
|
9a46a466f7 | ||
|
|
fafc5c8553 | ||
|
|
4ffdfaa506 | ||
|
|
e3989840ee | ||
|
|
b3e6f531a1 | ||
|
|
4f6ee34592 | ||
|
|
3ae58a323e | ||
|
|
26b958c270 | ||
|
|
12a4af5900 | ||
|
|
69479d538c | ||
|
|
829397dd5a | ||
|
|
2bc89026db | ||
|
|
ebbc44d181 | ||
|
|
2228a1e36b | ||
|
|
a8cbf3e8ff | ||
|
|
fa32e3d734 | ||
|
|
0d17148ff0 | ||
|
|
aa38411cf7 | ||
|
|
4913c8699d | ||
|
|
1035a11487 | ||
|
|
15c2efe706 | ||
|
|
d7fd71bb62 | ||
|
|
b11ee993fa | ||
|
|
614aa7873c | ||
|
|
1adf31fe15 | ||
|
|
824ffd7b5b | ||
|
|
c31c6fdebb | ||
|
|
83f3276429 | ||
|
|
d21f68ce54 | ||
|
|
18b1e1b133 | ||
|
|
0edaa40052 | ||
|
|
627077c8f3 | ||
|
|
a897b1798d | ||
|
|
50e39993bf | ||
|
|
5e397dd01e | ||
|
|
f57ff5d5e0 | ||
|
|
5c3e40917c | ||
|
|
90a2dc4581 | ||
|
|
b64243fdd6 | ||
|
|
42db87d305 | ||
|
|
e7ab1b589a | ||
|
|
e9979c9887 | ||
|
|
3bb9bb56f0 | ||
|
|
5a99474c55 | ||
|
|
182ee6c25f | ||
|
|
4d3f0a06db | ||
|
|
0e182c519b | ||
|
|
b1ee30ce7d | ||
|
|
93ba764e23 | ||
|
|
433e17bb81 | ||
|
|
61c09083ad | ||
|
|
018377e724 | ||
|
|
b76f9513ba | ||
|
|
40ebb7ba75 | ||
|
|
a9e52e8954 | ||
|
|
3c8876cac7 | ||
|
|
b7e005f9c7 | ||
|
|
e6fe0a19fa | ||
|
|
fba11b6a44 | ||
|
|
c270e7f5dd | ||
|
|
9ee00d345e | ||
|
|
0379fbc4eb | ||
|
|
9748a3ae91 | ||
|
|
1881944748 | ||
|
|
3721fa194c | ||
|
|
8c3fcad20b | ||
|
|
decf373d0b | ||
|
|
ff1d50f993 | ||
|
|
ef34204b59 | ||
|
|
270b636d80 | ||
|
|
ac01da2ae9 | ||
|
|
0136310c54 | ||
|
|
ecf4cf852e | ||
|
|
c66384adfb | ||
|
|
98bdda629d | ||
|
|
a8286f9cba | ||
|
|
fa3db4fcf6 | ||
|
|
ddac0cfee1 | ||
|
|
9368673459 | ||
|
|
43dc999ab5 | ||
|
|
3b7333e866 | ||
|
|
bc0ddbaf16 | ||
|
|
45f0ae7e1c | ||
|
|
a521c4ae01 | ||
|
|
8e5dee79e0 | ||
|
|
5b8238adeb | ||
|
|
ec330474fa | ||
|
|
ece28904a8 | ||
|
|
4f1c495afb | ||
|
|
5fdd27b7e6 | ||
|
|
91f449af9a | ||
|
|
efc0a0dfe3 | ||
|
|
fee47baa66 | ||
|
|
9abd9d20ec | ||
|
|
0ad7bfc7e7 | ||
|
|
bd64143ae1 | ||
|
|
ec982ba9a3 | ||
|
|
6280f6ff98 | ||
|
|
35d20390a9 | ||
|
|
c487c5042f | ||
|
|
aaf7927aa2 | ||
|
|
3c677f3d21 | ||
|
|
94eb76b3a6 | ||
|
|
a921cb2d0d | ||
|
|
f3aaa363d8 | ||
|
|
45a79e1920 | ||
|
|
6fd9b2a453 | ||
|
|
01d8e89a71 | ||
|
|
c89fa63910 | ||
|
|
9fc5c49dbf | ||
|
|
7dfc269df9 | ||
|
|
76d0b397db | ||
|
|
5413f887af | ||
|
|
b3d0c61f0e | ||
|
|
4ce0441d68 | ||
|
|
72be34e18d | ||
|
|
d2961b7650 | ||
|
|
fdca1bbf72 | ||
|
|
ab7a2f9dee | ||
|
|
7b72857a3b | ||
|
|
4787146658 | ||
|
|
430f9356c3 | ||
|
|
70a3b3518f | ||
|
|
c0944c17e0 | ||
|
|
da1b2a91e7 | ||
|
|
aa27492713 | ||
|
|
afe589dec3 | ||
|
|
978d140c8f | ||
|
|
2ce213b62c | ||
|
|
7748266078 | ||
|
|
83783d07a1 | ||
|
|
49a1f2c7c5 | ||
|
|
ddfc0151fc | ||
|
|
81c508e13c | ||
|
|
7195cfc3cf | ||
|
|
93fe5e2cf7 | ||
|
|
a2bf795d12 | ||
|
|
c8d78f39e0 | ||
|
|
d9ab8a1c8b | ||
|
|
5125ad4889 | ||
|
|
951e85b04b | ||
|
|
711d922695 | ||
|
|
3692ffcde7 | ||
|
|
b049420c59 | ||
|
|
241103c369 | ||
|
|
2128367113 | ||
|
|
f555c8190d | ||
|
|
d5df633def | ||
|
|
fe7dc859e3 | ||
|
|
569c5046c6 | ||
|
|
e0210ae2d8 | ||
|
|
f85dc3b7e7 | ||
|
|
92d4363120 | ||
|
|
6c69220de2 | ||
|
|
3a1229b072 | ||
|
|
45538c9f62 | ||
|
|
0c173fde53 | ||
|
|
77db2bbfec | ||
|
|
2987185a9d | ||
|
|
7a7baf7175 | ||
|
|
5645cc0af2 | ||
|
|
63a6756fed | ||
|
|
9f408c98a6 | ||
|
|
26d6998d82 | ||
|
|
6bee44acb7 | ||
|
|
441992cf37 | ||
|
|
490c224ac3 | ||
|
|
f5c55048de | ||
|
|
8139e124c2 | ||
|
|
cba73a5139 | ||
|
|
de0c9367e5 | ||
|
|
630060860c | ||
|
|
dccd11fb7b | ||
|
|
b8a4b2cf16 | ||
|
|
e52f55bfba | ||
|
|
1cb99e02a9 | ||
|
|
c9c5c35e56 | ||
|
|
bc7c9f9c01 | ||
|
|
e4bfc4cb8d | ||
|
|
d64995c4a1 | ||
|
|
2952ea7404 | ||
|
|
f34c807a2c | ||
|
|
b1f9704055 | ||
|
|
9382829ba5 | ||
|
|
373a6ea912 | ||
|
|
72eb4e7b3b | ||
|
|
315c0870ed | ||
|
|
200fa621bf | ||
|
|
80348c1980 | ||
|
|
856dd8403c | ||
|
|
0bb9acd4c1 | ||
|
|
047a80917f | ||
|
|
e6e8fe4763 | ||
|
|
6cd212398e | ||
|
|
44ad6d4247 | ||
|
|
7302782853 | ||
|
|
7fa1aba6b8 | ||
|
|
2fed4e3e8b | ||
|
|
bd343c51a3 | ||
|
|
8a05f06efa | ||
|
|
27b049eada | ||
|
|
2d1afb5291 | ||
|
|
63c17a3abf | ||
|
|
94f9bc5fca | ||
|
|
ab273430d2 | ||
|
|
0b3dc315ad | ||
|
|
f26a2d2f13 | ||
|
|
8edf9bf508 | ||
|
|
415381cebd | ||
|
|
59accec1c0 | ||
|
|
dd2699fcc1 | ||
|
|
0986a5f985 | ||
|
|
fa77229b72 | ||
|
|
78b5080c9a | ||
|
|
0641da786c | ||
|
|
3291f3bb3c | ||
|
|
a0cfef8bda | ||
|
|
4d033f4a03 | ||
|
|
562820180c | ||
|
|
a29832207e | ||
|
|
2afd549826 | ||
|
|
8a71b2c6dd | ||
|
|
d633d2691d | ||
|
|
f9b1608fd2 | ||
|
|
4d8ed3f00e | ||
|
|
359e7d0543 | ||
|
|
07bf93e022 | ||
|
|
57e27da0c4 | ||
|
|
9ecbf62d25 | ||
|
|
48ffa591ca | ||
|
|
b7dd617bb1 | ||
|
|
392f9d0775 | ||
|
|
4361b11c68 | ||
|
|
28bccad010 | ||
|
|
29d31a0deb | ||
|
|
1d9960d349 | ||
|
|
2747b241ab | ||
|
|
6b8035b116 | ||
|
|
33ad5f4d45 | ||
|
|
af7ad9251a | ||
|
|
4ec9975aa3 | ||
|
|
c6b0841583 | ||
|
|
9312435076 | ||
|
|
49427f1c54 | ||
|
|
82e7e06dc4 | ||
|
|
76cf82bb19 | ||
|
|
4f8ad2962e | ||
|
|
fe4be48bff | ||
|
|
66fc0b421b | ||
|
|
c0b4867659 | ||
|
|
95253fbc76 | ||
|
|
df519b3042 | ||
|
|
9ed963ef70 | ||
|
|
1dd7993e7a | ||
|
|
386b33b65d | ||
|
|
416deb8711 | ||
|
|
3c7fdac55e | ||
|
|
392a6af47f | ||
|
|
6b03e3f603 | ||
|
|
9397f6beda | ||
|
|
d17b386fd6 | ||
|
|
f6d2e1a447 | ||
|
|
bd95ef5db6 | ||
|
|
6e76c621b8 | ||
|
|
3e58bee0eb | ||
|
|
8c2ed1f581 | ||
|
|
500dd3bfaf | ||
|
|
bc7bacd654 | ||
|
|
fa16b990b6 | ||
|
|
9d5e9dd9e5 | ||
|
|
4046f9dde1 | ||
|
|
fcd82ada14 | ||
|
|
d616da7f67 | ||
|
|
0c81202bbb | ||
|
|
6284bd3f17 | ||
|
|
7adc9d94b4 | ||
|
|
73e030d2f5 | ||
|
|
3a3b45aa04 | ||
|
|
81c19e9964 | ||
|
|
df856bc4a0 | ||
|
|
8558dc7ee4 | ||
|
|
087d5f6e58 | ||
|
|
9540bc350a | ||
|
|
09153c6c30 | ||
|
|
f66d81f147 | ||
|
|
75925143b6 | ||
|
|
e7dc00991e | ||
|
|
dd9da82ed3 | ||
|
|
c995b8fa81 | ||
|
|
e7c2994ea3 | ||
|
|
106997b26c | ||
|
|
fa842c1566 | ||
|
|
be03dd82f9 | ||
|
|
39c46995e1 | ||
|
|
97adc01e8d | ||
|
|
36be881741 | ||
|
|
955cc6dff5 | ||
|
|
2be1399eda | ||
|
|
ef5d2a7654 | ||
|
|
1cd00cab62 | ||
|
|
7652c231f6 | ||
|
|
e17cdf0ca7 | ||
|
|
3317791e68 | ||
|
|
c3871bc2ec | ||
|
|
ebba86f741 | ||
|
|
5ac84a6a02 | ||
|
|
cf3e9704e8 | ||
|
|
37eac8afcf | ||
|
|
692a0535ff | ||
|
|
0f0d804052 | ||
|
|
d189e6b53d | ||
|
|
5da31691a9 | ||
|
|
4059a4fd9a | ||
|
|
e56da71856 | ||
|
|
91e10f627f | ||
|
|
338c686e8d | ||
|
|
0007e9ea2b | ||
|
|
1e09e9b322 | ||
|
|
43c358119a | ||
|
|
7a0f251ebd | ||
|
|
e989321c5f | ||
|
|
56f27e6aaa | ||
|
|
7c4e5aa667 | ||
|
|
d253d826bb | ||
|
|
888fa20ca3 | ||
|
|
598dbb2b7a | ||
|
|
71eb1eab14 | ||
|
|
62a6bd99e8 | ||
|
|
174cdf2149 | ||
|
|
5267fbce7b | ||
|
|
dd814e7e95 | ||
|
|
f806b45d3d | ||
|
|
8575e9eabe | ||
|
|
6ff9e22049 | ||
|
|
caa86fdab7 | ||
|
|
48246716cc | ||
|
|
6e07429e8a | ||
|
|
1a271c32b6 | ||
|
|
3fddec8f20 | ||
|
|
ac3944aece | ||
|
|
958cc2a50c | ||
|
|
058dfcfa9f | ||
|
|
9dbb1ca386 | ||
|
|
4d1def6e9d | ||
|
|
ff763b0278 | ||
|
|
74f611e0fd | ||
|
|
61f8be1c60 | ||
|
|
96b18dab00 | ||
|
|
0a21b239bc | ||
|
|
9c3589aea4 | ||
|
|
2433485d13 | ||
|
|
6b5f254e0a | ||
|
|
e1b41b1e26 | ||
|
|
bd7a265df0 | ||
|
|
039cca9529 | ||
|
|
963347dbc2 | ||
|
|
697cc9f758 | ||
|
|
3892225584 | ||
|
|
a7880a0ef5 | ||
|
|
dd574830f5 | ||
|
|
71a0d15c35 | ||
|
|
8a33c4f09a | ||
|
|
d223ce062a | ||
|
|
39c8f12963 | ||
|
|
bd4ba8c826 | ||
|
|
3d38c8e088 | ||
|
|
ce58c80c6d | ||
|
|
19b3a0781c | ||
|
|
b42c1971b9 | ||
|
|
02440941e0 | ||
|
|
cd2f986c50 | ||
|
|
e7cbd6b31f | ||
|
|
a7f6d5eebd | ||
|
|
4f3b8c0906 | ||
|
|
7126c71943 | ||
|
|
489cf16af8 | ||
|
|
b012f44259 | ||
|
|
5ce765bd27 | ||
|
|
2450edd070 | ||
|
|
fc2a61835c | ||
|
|
d06d73d5f7 | ||
|
|
2c10996bb3 | ||
|
|
b9c85bb1bf | ||
|
|
f436afb9aa | ||
|
|
750efe4152 | ||
|
|
99cee95cd5 | ||
|
|
ec10b84fa6 | ||
|
|
f0442fe2ce | ||
|
|
7907969696 | ||
|
|
8dbab73d2b | ||
|
|
ade8acb4e2 | ||
|
|
9cb78e6954 | ||
|
|
e9127bdbb3 | ||
|
|
9b750bc756 | ||
|
|
c3349e8cc7 | ||
|
|
f4d7c7f739 | ||
|
|
19d51cb1a4 | ||
|
|
cc9983aa16 | ||
|
|
81f8467f66 | ||
|
|
df6b23e3c7 | ||
|
|
4dd81e7d59 | ||
|
|
54ce7067b4 | ||
|
|
2673f790e6 | ||
|
|
69bea2ec9b | ||
|
|
37037cf797 | ||
|
|
2a1586aab3 | ||
|
|
915281d138 | ||
|
|
b53a179ea0 | ||
|
|
7077e8b50e | ||
|
|
d9181277d9 | ||
|
|
810c2d6a1a | ||
|
|
f31113fb90 | ||
|
|
1702b9dd37 | ||
|
|
f8e61c713c | ||
|
|
643132754a | ||
|
|
4f0a131bd2 | ||
|
|
52aa0350cf | ||
|
|
c9884f32fe | ||
|
|
aea3ae3d37 | ||
|
|
10f7c3045f | ||
|
|
bf4e158c30 | ||
|
|
4a3155ed22 | ||
|
|
29f1c89fa5 | ||
|
|
4a92e8a99f | ||
|
|
c6cffb1156 | ||
|
|
f6e4e59905 | ||
|
|
3a0736c4bf | ||
|
|
47455b2029 | ||
|
|
05eb7d765f | ||
|
|
5e786686d0 | ||
|
|
5cb8e3a7c3 | ||
|
|
84bd077eac | ||
|
|
db39b27119 | ||
|
|
f2d2b35543 | ||
|
|
5dfd5766f2 | ||
|
|
0e4c3e1e92 | ||
|
|
221f42606c | ||
|
|
742f092ed3 | ||
|
|
36bffa1475 | ||
|
|
936abc1b1a | ||
|
|
584bdd545f | ||
|
|
99debc18d7 | ||
|
|
56b896eda0 | ||
|
|
cab102528d | ||
|
|
1875cdcda2 | ||
|
|
386dcc8d43 | ||
|
|
e6d59a47b1 | ||
|
|
2659393f33 | ||
|
|
916b2363d9 | ||
|
|
bb513790b5 | ||
|
|
e5ab15a727 | ||
|
|
bbaa750fda | ||
|
|
a46e19210a | ||
|
|
9d6772edd1 | ||
|
|
3aeaa564a2 | ||
|
|
8baad494e3 | ||
|
|
fb04753d52 | ||
|
|
ea37c4ccd8 | ||
|
|
80f02b52e1 | ||
|
|
55464845d6 | ||
|
|
8aef4bb455 | ||
|
|
d8a2adbca2 | ||
|
|
02e56da08a | ||
|
|
9d9f857e73 | ||
|
|
b140c1e65f | ||
|
|
81ff03462e | ||
|
|
2ea7c09c84 | ||
|
|
4163ed212c | ||
|
|
c83722c2df | ||
|
|
700db655e6 | ||
|
|
226428f631 | ||
|
|
e2b293e49b | ||
|
|
1f6e9d4660 | ||
|
|
ecf2ee888f | ||
|
|
1e8eeadb1d | ||
|
|
e7ceccdd83 | ||
|
|
5390b4a4b3 | ||
|
|
f25ec34bc3 | ||
|
|
53ece86816 | ||
|
|
9b128cdfcc | ||
|
|
c047386755 | ||
|
|
b0ffb272b3 | ||
|
|
956e21c8db | ||
|
|
95057a2b09 | ||
|
|
788aa24a80 | ||
|
|
0a72ef91cc | ||
|
|
f0f4e0985a | ||
|
|
a6bedea4b6 | ||
|
|
055316e1ca | ||
|
|
25fece5947 | ||
|
|
5ec3cdbcc5 | ||
|
|
b530ab2ef6 | ||
|
|
5164f60c98 | ||
|
|
19ba3015f7 | ||
|
|
fb00b2672c | ||
|
|
3e0f936d2a | ||
|
|
f5be146dbb | ||
|
|
95431a0874 | ||
|
|
f76b130ca4 | ||
|
|
d19f8259d0 | ||
|
|
68cd62d756 | ||
|
|
ea4a55d3dd | ||
|
|
788bdcd35b | ||
|
|
ebd7e24830 | ||
|
|
0b7fbffa0a | ||
|
|
2afda9a055 | ||
|
|
28dd275bd8 | ||
|
|
9a3cf182ac | ||
|
|
dd278c28be | ||
|
|
37572122b0 | ||
|
|
5348f75b5e | ||
|
|
6b8873d385 | ||
|
|
8db18d87e5 | ||
|
|
444920d15c | ||
|
|
8aa2fab603 | ||
|
|
9ded4386cc | ||
|
|
cc6b1b5aa1 | ||
|
|
74f20da82f | ||
|
|
4c9c838b70 | ||
|
|
9a9d27d37a | ||
|
|
0347d4cec4 | ||
|
|
b1b189288e | ||
|
|
b3220f981b | ||
|
|
a5573e62c6 | ||
|
|
c4ccee8df6 | ||
|
|
fbcf803d06 | ||
|
|
5247b7a9af | ||
|
|
dc218cc992 | ||
|
|
c21761d9d4 | ||
|
|
36c0bf06d7 | ||
|
|
ccdab2b083 | ||
|
|
15a8a56807 | ||
|
|
385d42281b | ||
|
|
b0dc933021 | ||
|
|
b73ee1f638 | ||
|
|
51c2a54179 | ||
|
|
2d71a916a2 | ||
|
|
5f9cf47003 | ||
|
|
16b39185dc | ||
|
|
6f0410f26e | ||
|
|
0b86845852 | ||
|
|
d5f914bd2f | ||
|
|
663c65e42e | ||
|
|
b9de86f96c | ||
|
|
bd9c0ffb25 | ||
|
|
9d763773cf | ||
|
|
540f977146 | ||
|
|
d16b09d3fc | ||
|
|
6f8a8d3b90 | ||
|
|
00ef4166c7 | ||
|
|
b88414735e | ||
|
|
af092fae9b | ||
|
|
b889efc913 | ||
|
|
877dd68a6b | ||
|
|
2805a1c7bc | ||
|
|
c5c000ea6f | ||
|
|
673f1072bf | ||
|
|
d0e93b9b9f | ||
|
|
f239088bcb | ||
|
|
32642f105c | ||
|
|
4adaf6d355 | ||
|
|
291e2a5e40 | ||
|
|
05bdb56203 | ||
|
|
cb71df8345 | ||
|
|
c6c5f5188b | ||
|
|
a7b6ca0914 | ||
|
|
30ba062695 | ||
|
|
a595af7b3f | ||
|
|
b2ee3172d8 | ||
|
|
73992ee8e9 | ||
|
|
78885fd00a | ||
|
|
ce55ac6ccb | ||
|
|
8ce0951e5f | ||
|
|
7788fa9d3e | ||
|
|
7f36552c71 | ||
|
|
16f9437b29 | ||
|
|
0beaff718e | ||
|
|
9b6f37b5d0 | ||
|
|
3d6d4aea6a | ||
|
|
2356a21e54 |
@@ -46,7 +46,7 @@ indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Less
|
||||
[*.js]
|
||||
[*.less]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
|
||||
9
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
/.nyc_output/
|
||||
/bower_components/
|
||||
/dist/
|
||||
|
||||
/node_modules/*
|
||||
!/node_modules/*.js
|
||||
/node_modules/*.js/
|
||||
npm-debug.log
|
||||
npm-debug.log.*
|
||||
|
||||
!node_modules/*
|
||||
node_modules/*/
|
||||
|
||||
10
.npmignore
Normal file
@@ -0,0 +1,10 @@
|
||||
/examples/
|
||||
example.js
|
||||
example.js.map
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
12
.travis.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 'stable'
|
||||
#- '4' # Disabled for now because npm 2 cannot properly handled broken peer dependencies.
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
sudo: false
|
||||
557
CHANGELOG.md
@@ -1,10 +1,553 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.0.0** (2016-06-24)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Handle failed quiesce in snapshots [\#1088](https://github.com/vatesfr/xo-web/issues/1088)
|
||||
- Sparklines stats [\#1061](https://github.com/vatesfr/xo-web/issues/1061)
|
||||
- Task view [\#1060](https://github.com/vatesfr/xo-web/issues/1060)
|
||||
- Improved import system [\#1048](https://github.com/vatesfr/xo-web/issues/1048)
|
||||
- Backup restore view improvements [\#1021](https://github.com/vatesfr/xo-web/issues/1021)
|
||||
- Restore VM - Wrong VLAN on the VMs interface [\#1016](https://github.com/vatesfr/xo-web/issues/1016)
|
||||
- Fast Disk Cloning [\#960](https://github.com/vatesfr/xo-web/issues/960)
|
||||
- Disaster recovery job should target SRs, not pools [\#955](https://github.com/vatesfr/xo-web/issues/955)
|
||||
- Improve Header/Content interaction in a page [\#926](https://github.com/vatesfr/xo-web/issues/926)
|
||||
- New default view [\#912](https://github.com/vatesfr/xo-web/issues/912)
|
||||
- Xen Patching - Restart Pending [\#883](https://github.com/vatesfr/xo-web/issues/883)
|
||||
- Hide About page for user that are not admin [\#877](https://github.com/vatesfr/xo-web/issues/877)
|
||||
- ACL: Ability to view/sort/group by User/Group, Objects or Role [\#875](https://github.com/vatesfr/xo-web/issues/875)
|
||||
- ACL: Ability to select multiple users & group when creating a rule [\#874](https://github.com/vatesfr/xo-web/issues/874)
|
||||
- Translation [\#839](https://github.com/vatesfr/xo-web/issues/839)
|
||||
- XO offer useless network interfaces for XenMontion [\#833](https://github.com/vatesfr/xo-web/issues/833)
|
||||
- Show HVM, PVM, PVHVM modes in guest details [\#806](https://github.com/vatesfr/xo-web/issues/806)
|
||||
- Tree view: display cpu available/total for each host [\#696](https://github.com/vatesfr/xo-web/issues/696)
|
||||
- Greenkeeper integration [\#667](https://github.com/vatesfr/xo-web/issues/667)
|
||||
- Clarify vCPUs and RAM editor [\#658](https://github.com/vatesfr/xo-web/issues/658)
|
||||
- Backup LZ4 compression [\#647](https://github.com/vatesfr/xo-web/issues/647)
|
||||
- Support enum in plugins configuration [\#638](https://github.com/vatesfr/xo-web/issues/638)
|
||||
- Add configuration option to disable xoa-updater [\#535](https://github.com/vatesfr/xo-web/issues/535)
|
||||
- Use cursors to add more context to actions [\#523](https://github.com/vatesfr/xo-web/issues/523)
|
||||
- Review UI for flat view [\#354](https://github.com/vatesfr/xo-web/issues/354)
|
||||
- Review UI for the tree view [\#353](https://github.com/vatesfr/xo-web/issues/353)
|
||||
- Tag filtering [\#233](https://github.com/vatesfr/xo-web/issues/233)
|
||||
- GUI review [\#230](https://github.com/vatesfr/xo-web/issues/230)
|
||||
- Review UI for VM creation [\#214](https://github.com/vatesfr/xo-web/issues/214)
|
||||
- Ability to collapse pools/hosts in main view [\#173](https://github.com/vatesfr/xo-web/issues/173)
|
||||
- Issue importing .xva VM via xo-web [\#1022](https://github.com/vatesfr/xo-web/issues/1022)
|
||||
- Enhancement Proposal - Cancel In Progress Backups [\#1003](https://github.com/vatesfr/xo-web/issues/1003)
|
||||
- Can't create VM with CloudConfigDrive [\#917](https://github.com/vatesfr/xo-web/issues/917)
|
||||
- Auth: LDAP User causes problems [\#893](https://github.com/vatesfr/xo-web/issues/893)
|
||||
- No tags in Continuous Replication [\#838](https://github.com/vatesfr/xo-web/issues/838)
|
||||
- Delta backup Depth not working [\#802](https://github.com/vatesfr/xo-web/issues/802)
|
||||
- Update Section - Running version info missing - gui enhancement [\#795](https://github.com/vatesfr/xo-web/issues/795)
|
||||
- On reboot, vnc console wrongly scaled [\#722](https://github.com/vatesfr/xo-web/issues/722)
|
||||
- Make the object name \(title\) "sticky" at the top of the page [\#705](https://github.com/vatesfr/xo-web/issues/705)
|
||||
- pool view: display Local SR from hosts in the current pool [\#692](https://github.com/vatesfr/xo-web/issues/692)
|
||||
- tree view: display all IPs [\#689](https://github.com/vatesfr/xo-web/issues/689)
|
||||
- XO5 parallel distribution [\#462](https://github.com/vatesfr/xo-web/issues/462)
|
||||
- Load balancing with XO [\#423](https://github.com/vatesfr/xo-web/issues/423)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- vCPUs number when no tools installed [\#1089](https://github.com/vatesfr/xo-web/issues/1089)
|
||||
- Config Drive textbox disappears when content is deleted [\#1012](https://github.com/vatesfr/xo-web/issues/1012)
|
||||
- storage status not changed in host view page after disconnect/connect [\#1009](https://github.com/vatesfr/xo-web/issues/1009)
|
||||
- Cannot Delete Logs From Backup Overview [\#1004](https://github.com/vatesfr/xo-web/issues/1004)
|
||||
- \[v5.x\] Plugins configuration: optional non-used objects are sent [\#1000](https://github.com/vatesfr/xo-web/issues/1000)
|
||||
- "@" char in remote password break the remote view [\#997](https://github.com/vatesfr/xo-web/issues/997)
|
||||
- Handle MEMORY\_CONSTRAINT\_VIOLATION correctly [\#970](https://github.com/vatesfr/xo-web/issues/970)
|
||||
- VM creation error on XenServer Dundee [\#964](https://github.com/vatesfr/xo-web/issues/964)
|
||||
- Copy VMs doesn't display all SRs [\#945](https://github.com/vatesfr/xo-web/issues/945)
|
||||
- Autopower\_on wrong value [\#937](https://github.com/vatesfr/xo-web/issues/937)
|
||||
- Correctly handle unknown users in group view [\#900](https://github.com/vatesfr/xo-web/issues/900)
|
||||
- Importing into Dundee [\#887](https://github.com/vatesfr/xo-web/issues/887)
|
||||
- update status - gui resize issue [\#803](https://github.com/vatesfr/xo-web/issues/803)
|
||||
- Backup Remote Stores Problem [\#751](https://github.com/vatesfr/xo-web/issues/751)
|
||||
- VM view is broken when changing a disk SR twice [\#670](https://github.com/vatesfr/xo-web/issues/670)
|
||||
- console mouse sync [\#280](https://github.com/vatesfr/xo-web/issues/280)
|
||||
|
||||
## **4.16.0** (2016-04-29)
|
||||
|
||||
Maintenance release
|
||||
|
||||
### Enhancements
|
||||
|
||||
- TOO\_MANY\_PENDING\_TASKS [\#861](https://github.com/vatesfr/xo-web/issues/861)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Incorrect VM target name with continuous replication [\#904](https://github.com/vatesfr/xo-web/issues/904)
|
||||
- Error while deleting users [\#901](https://github.com/vatesfr/xo-web/issues/901)
|
||||
- Use an available path to the SR to create a config drive [\#882](https://github.com/vatesfr/xo-web/issues/882)
|
||||
- VM autoboot don't set the right pool parameter [\#879](https://github.com/vatesfr/xo-web/issues/879)
|
||||
- BUG: ACL with NFS ISO Library not working! [\#870](https://github.com/vatesfr/xo-web/issues/870)
|
||||
- Broken paths in backups in SMB [\#865](https://github.com/vatesfr/xo-web/issues/865)
|
||||
- Plugins page loads users/groups multiple times [\#829](https://github.com/vatesfr/xo-web/issues/829)
|
||||
- "Ghost" VM remains after migration [\#769](https://github.com/vatesfr/xo-web/issues/769)
|
||||
|
||||
## **4.15.0** (2016-03-21)
|
||||
|
||||
Load balancing, SMB delta support, advanced network operations...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add the job name inside the backup email report [\#819](https://github.com/vatesfr/xo-web/issues/819)
|
||||
- Delta backup with quiesce [\#812](https://github.com/vatesfr/xo-web/issues/812)
|
||||
- Hosts: No user feedback when error occurs with SR connect / disconnect [\#810](https://github.com/vatesfr/xo-web/issues/810)
|
||||
- Expose components versions [\#807](https://github.com/vatesfr/xo-web/issues/807)
|
||||
- Rework networks/PIFs management [\#805](https://github.com/vatesfr/xo-web/issues/805)
|
||||
- Displaying all SRs and a list of available hosts for creating VM from a pool [\#790](https://github.com/vatesfr/xo-web/issues/790)
|
||||
- Add "Source network" on "VM migration" screen [\#785](https://github.com/vatesfr/xo-web/issues/785)
|
||||
- Migration queue [\#783](https://github.com/vatesfr/xo-web/issues/783)
|
||||
- Match network names for VM migration [\#782](https://github.com/vatesfr/xo-web/issues/782)
|
||||
- Disk names [\#780](https://github.com/vatesfr/xo-web/issues/780)
|
||||
- Self service: should the user be able to set the CPU weight? [\#767](https://github.com/vatesfr/xo-web/issues/767)
|
||||
- host & pool Citrix license status [\#763](https://github.com/vatesfr/xo-web/issues/763)
|
||||
- pool view: Provide "updates" section [\#762](https://github.com/vatesfr/xo-web/issues/762)
|
||||
- XOA ISO image: ambigious root disk label [\#761](https://github.com/vatesfr/xo-web/issues/761)
|
||||
- Host info: provide system serial number [\#760](https://github.com/vatesfr/xo-web/issues/760)
|
||||
- CIFS ISO SR Creation [\#731](https://github.com/vatesfr/xo-web/issues/731)
|
||||
- MAC address not preserved on VM restore [\#707](https://github.com/vatesfr/xo-web/issues/707)
|
||||
- Failing replication job should send reports [\#659](https://github.com/vatesfr/xo-web/issues/659)
|
||||
- Display networks in the Pool view [\#226](https://github.com/vatesfr/xo-web/issues/226)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Broken link to backup remote [\#821](https://github.com/vatesfr/xo-web/issues/821)
|
||||
- Issue with self-signed cert for email plugin [\#817](https://github.com/vatesfr/xo-web/issues/817)
|
||||
- Plugins view, reset form and errors [\#815](https://github.com/vatesfr/xo-web/issues/815)
|
||||
- HVM recovery mode is broken [\#794](https://github.com/vatesfr/xo-web/issues/794)
|
||||
- Disk bug when creating vm from template [\#778](https://github.com/vatesfr/xo-web/issues/778)
|
||||
- Can't mount NFS shares in remote stores [\#775](https://github.com/vatesfr/xo-web/issues/775)
|
||||
- VM disk name and description not passed during creation [\#774](https://github.com/vatesfr/xo-web/issues/774)
|
||||
- NFS mount problem for Windows share [\#771](https://github.com/vatesfr/xo-web/issues/771)
|
||||
- lodash.pluck not installed [\#757](https://github.com/vatesfr/xo-web/issues/757)
|
||||
- this.\_getAuthenticationTokensForUser is not a function [\#755](https://github.com/vatesfr/xo-web/issues/755)
|
||||
- CentOS 6.x 64bit template creates a VM that won't boot [\#733](https://github.com/vatesfr/xo-web/issues/733)
|
||||
- Lot of xo:perf leading to XO crash [\#575](https://github.com/vatesfr/xo-web/issues/575)
|
||||
- New collection checklist [\#262](https://github.com/vatesfr/xo-web/issues/262)
|
||||
|
||||
## **4.14.0** (2016-02-23)
|
||||
|
||||
Self service, custom CloudInit...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- VM creation self service with quotas [\#285](https://github.com/vatesfr/xo-web/issues/285)
|
||||
- Cloud config custom user data [\#706](https://github.com/vatesfr/xo-web/issues/706)
|
||||
- Patches behind a proxy [\#737](https://github.com/vatesfr/xo-web/issues/737)
|
||||
- Remote store status indicator [\#728](https://github.com/vatesfr/xo-web/issues/728)
|
||||
- Patch list order [\#724](https://github.com/vatesfr/xo-web/issues/724)
|
||||
- Enable reporting on additional backup types [\#717](https://github.com/vatesfr/xo-web/issues/717)
|
||||
- Tooltip name for cancel [\#703](https://github.com/vatesfr/xo-web/issues/703)
|
||||
- Portable VHD merging [\#646](https://github.com/vatesfr/xo-web/issues/646)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid merge between two delta vdi backups [\#702](https://github.com/vatesfr/xo-web/issues/702)
|
||||
- Text in table is not cut anymore [\#713](https://github.com/vatesfr/xo-web/issues/713)
|
||||
- Disk size edition issue with float numbers [\#719](https://github.com/vatesfr/xo-web/issues/719)
|
||||
- Create vm, summary is not refreshed [\#721](https://github.com/vatesfr/xo-web/issues/721)
|
||||
- Boot order problem [\#726](https://github.com/vatesfr/xo-web/issues/726)
|
||||
|
||||
## **4.13.0** (2016-02-05)
|
||||
|
||||
Backup checksum, SMB remotes...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add SMB mount for remote [\#338](https://github.com/vatesfr/xo-web/issues/338)
|
||||
- Centralize Perm in a lib [\#345](https://github.com/vatesfr/xo-web/issues/345)
|
||||
- Expose interpool migration details [\#567](https://github.com/vatesfr/xo-web/issues/567)
|
||||
- Add checksum for delta backup [\#617](https://github.com/vatesfr/xo-web/issues/617)
|
||||
- Redirect from HTTP to HTTPS [\#626](https://github.com/vatesfr/xo-web/issues/626)
|
||||
- Expose vCPU weight [\#633](https://github.com/vatesfr/xo-web/issues/633)
|
||||
- Avoid metadata in delta backup [\#651](https://github.com/vatesfr/xo-web/issues/651)
|
||||
- Button to clear logs [\#661](https://github.com/vatesfr/xo-web/issues/661)
|
||||
- Units for RAM and disks [\#666](https://github.com/vatesfr/xo-web/issues/666)
|
||||
- Remove multiple VDIs at once [\#676](https://github.com/vatesfr/xo-web/issues/676)
|
||||
- Find orphaned VDI snapshots [\#679](https://github.com/vatesfr/xo-web/issues/679)
|
||||
- New health view in Dashboard [\#680](https://github.com/vatesfr/xo-web/issues/680)
|
||||
- Use physical usage for VDI and SR [\#682](https://github.com/vatesfr/xo-web/issues/682)
|
||||
- TLS configuration [\#685](https://github.com/vatesfr/xo-web/issues/685)
|
||||
- Better VM info on tree view [\#688](https://github.com/vatesfr/xo-web/issues/688)
|
||||
- Absolute values in tooltips for tree view [\#690](https://github.com/vatesfr/xo-web/issues/690)
|
||||
- Absolute values for host memory [\#691](https://github.com/vatesfr/xo-web/issues/691)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Issues on host console screen [\#672](https://github.com/vatesfr/xo-web/issues/672)
|
||||
- NFS remote mount fails in particular case [\#665](https://github.com/vatesfr/xo-web/issues/665)
|
||||
- Unresponsive pages [\#662](https://github.com/vatesfr/xo-web/issues/662)
|
||||
- Live migration fail in the same pool with local SR fails [\#655](https://github.com/vatesfr/xo-web/issues/655)
|
||||
|
||||
## **4.12.0** (2016-01-18)
|
||||
|
||||
Continuous Replication, Continuous Delta backup...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Continuous VM replication [\#582](https://github.com/vatesfr/xo-web/issues/582)
|
||||
- Continuous Delta Backup [\#576](https://github.com/vatesfr/xo-web/issues/576)
|
||||
- Scheduler should not run job again if previous instance is not finished [\#642](https://github.com/vatesfr/xo-web/issues/642)
|
||||
- Boot VM automatically after creation [\#635](https://github.com/vatesfr/xo-web/issues/635)
|
||||
- Manage existing VIFs in templates [\#630](https://github.com/vatesfr/xo-web/issues/630)
|
||||
- Support templates with existing install repository [\#627](https://github.com/vatesfr/xo-web/issues/627)
|
||||
- Remove running VMs [\#616](https://github.com/vatesfr/xo-web/issues/616)
|
||||
- Prevent a VM to start before delta import is finished [\#613](https://github.com/vatesfr/xo-web/issues/613)
|
||||
- Spawn multiple VMs at once [\#606](https://github.com/vatesfr/xo-web/issues/606)
|
||||
- Fixed `suspendVM` in tree view. [\#619](https://github.com/vatesfr/xo-web/pull/619) ([pdonias](https://github.com/pdonias))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- User defined MAC address is not fetch in VM install [\#643](https://github.com/vatesfr/xo-web/issues/643)
|
||||
- CoreOsCloudConfig is not shown with CoreOS [\#639](https://github.com/vatesfr/xo-web/issues/639)
|
||||
- Plugin activation/deactivation in web UI seems broken [\#637](https://github.com/vatesfr/xo-web/issues/637)
|
||||
- Issue when creating CloudConfig drive [\#636](https://github.com/vatesfr/xo-web/issues/636)
|
||||
- CloudConfig hostname shouldn't have space [\#634](https://github.com/vatesfr/xo-web/issues/634)
|
||||
- Cloned VIFs are not properly deleted on VM creation [\#632](https://github.com/vatesfr/xo-web/issues/632)
|
||||
- Default PV args missing during VM creation [\#628](https://github.com/vatesfr/xo-web/issues/628)
|
||||
- VM creation problems from custom templates [\#625](https://github.com/vatesfr/xo-web/issues/625)
|
||||
- Emergency shutdown race condition [\#622](https://github.com/vatesfr/xo-web/issues/622)
|
||||
- `vm.delete\(\)` should not delete VDIs attached to other VMs [\#621](https://github.com/vatesfr/xo-web/issues/621)
|
||||
- VM creation error from template with a disk [\#581](https://github.com/vatesfr/xo-web/issues/581)
|
||||
- Only delete VDI exports when VM backup is successful [\#644](https://github.com/vatesfr/xo-web/issues/644)
|
||||
- Change the name of an imported VM during the import process [\#641](https://github.com/vatesfr/xo-web/issues/641)
|
||||
- Creating a new VIF in view is partially broken [\#652](https://github.com/vatesfr/xo-web/issues/652)
|
||||
- Grey out the "create button" during VM creation [\#654](https://github.com/vatesfr/xo-web/issues/654)
|
||||
|
||||
## **4.11.0** (2015-12-22)
|
||||
|
||||
Delta backup, CloudInit...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Visible list of SR inside a VM [\#601](https://github.com/vatesfr/xo-web/issues/601)
|
||||
- VDI move [\#591](https://github.com/vatesfr/xo-web/issues/591)
|
||||
- Edit pre-existing disk configuration during VM creation [\#589](https://github.com/vatesfr/xo-web/issues/589)
|
||||
- Allow disk size edition [\#587](https://github.com/vatesfr/xo-web/issues/587)
|
||||
- Better VDI resize support [\#585](https://github.com/vatesfr/xo-web/issues/585)
|
||||
- Remove manual VM export metadata in UI [\#580](https://github.com/vatesfr/xo-web/issues/580)
|
||||
- Support import VM metadata [\#579](https://github.com/vatesfr/xo-web/issues/579)
|
||||
- Set a default pool SR [\#572](https://github.com/vatesfr/xo-web/issues/572)
|
||||
- ISOs should be sorted by name [\#565](https://github.com/vatesfr/xo-web/issues/565)
|
||||
- Button to boot a VM from a disc once [\#564](https://github.com/vatesfr/xo-web/issues/564)
|
||||
- Ability to boot a PV VM from a disc [\#563](https://github.com/vatesfr/xo-web/issues/563)
|
||||
- Add an option to manually run backup jobs [\#562](https://github.com/vatesfr/xo-web/issues/562)
|
||||
- backups to unmounted storage [\#561](https://github.com/vatesfr/xo-web/issues/561)
|
||||
- Root integer properties cannot be edited in plugins configuration form [\#550](https://github.com/vatesfr/xo-web/issues/550)
|
||||
- Generic CloudConfig drive [\#549](https://github.com/vatesfr/xo-web/issues/549)
|
||||
- Auto-discovery of installed xo-server plugins [\#546](https://github.com/vatesfr/xo-web/issues/546)
|
||||
- Hide info on flat view [\#545](https://github.com/vatesfr/xo-web/issues/545)
|
||||
- Config plugin boolean properties must have a default value \(undefined prohibited\) [\#543](https://github.com/vatesfr/xo-web/issues/543)
|
||||
- Present detailed errors on plugin configuration failures [\#530](https://github.com/vatesfr/xo-web/issues/530)
|
||||
- Do not reset form on failures in plugins configuration [\#529](https://github.com/vatesfr/xo-web/issues/529)
|
||||
- XMPP alert plugin [\#518](https://github.com/vatesfr/xo-web/issues/518)
|
||||
- Hide tag adders depending on ACLs [\#516](https://github.com/vatesfr/xo-web/issues/516)
|
||||
- Choosing a framework for xo-web 5 [\#514](https://github.com/vatesfr/xo-web/issues/514)
|
||||
- Prevent adding a host in an existing XAPI connection [\#466](https://github.com/vatesfr/xo-web/issues/466)
|
||||
- Read only connection to Xen servers/pools [\#439](https://github.com/vatesfr/xo-web/issues/439)
|
||||
- generic notification system [\#391](https://github.com/vatesfr/xo-web/issues/391)
|
||||
- Data architecture review [\#384](https://github.com/vatesfr/xo-web/issues/384)
|
||||
- Make filtering easier to understand/add some "default" filters [\#207](https://github.com/vatesfr/xo-web/issues/207)
|
||||
- Improve performance [\#148](https://github.com/vatesfr/xo-web/issues/148)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- VM metadata export should not require a snapshot [\#615](https://github.com/vatesfr/xo-web/issues/615)
|
||||
- Missing patch for all hosts is continuously refreshed [\#609](https://github.com/vatesfr/xo-web/issues/609)
|
||||
- Backup import memory issue [\#608](https://github.com/vatesfr/xo-web/issues/608)
|
||||
- Host list missing patch is buggy [\#604](https://github.com/vatesfr/xo-web/issues/604)
|
||||
- Servers infos should not been refreshed while a field is being edited [\#595](https://github.com/vatesfr/xo-web/issues/595)
|
||||
- Servers list should not been re-order while a field is being edited [\#594](https://github.com/vatesfr/xo-web/issues/594)
|
||||
- Correctly display size in interface \(binary scale\) [\#592](https://github.com/vatesfr/xo-web/issues/592)
|
||||
- Display failures on VM boot order modification [\#560](https://github.com/vatesfr/xo-web/issues/560)
|
||||
- `vm.setBootOrder\(\)` should throw errors on failures \(non-HVM VMs\) [\#559](https://github.com/vatesfr/xo-web/issues/559)
|
||||
- Hide boot order form for non-HVM VMs [\#558](https://github.com/vatesfr/xo-web/issues/558)
|
||||
- Allow editing PV args even when empty \(but only for PV VMs\) [\#557](https://github.com/vatesfr/xo-web/issues/557)
|
||||
- Crashes when using legacy event system [\#556](https://github.com/vatesfr/xo-web/issues/556)
|
||||
- XenServer patches check error for 6.1 [\#555](https://github.com/vatesfr/xo-web/issues/555)
|
||||
- activation plugin xo-server-transport-email [\#553](https://github.com/vatesfr/xo-web/issues/553)
|
||||
- Server error with JSON on 32 bits Dom0 [\#552](https://github.com/vatesfr/xo-web/issues/552)
|
||||
- Cloud Config drive shouldn't be created on default SR [\#548](https://github.com/vatesfr/xo-web/issues/548)
|
||||
- Deep properties cannot be edited in plugins configuration form [\#521](https://github.com/vatesfr/xo-web/issues/521)
|
||||
- Aborted VM export should cancel the operation [\#490](https://github.com/vatesfr/xo-web/issues/490)
|
||||
- VM missing with same UUID after an inter-pool migration [\#284](https://github.com/vatesfr/xo-web/issues/284)
|
||||
|
||||
## **4.10.0** (2015-11-27)
|
||||
|
||||
Job management, email notifications, CoreOS/Docker, Quiesce snapshots...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Job management ([xo-web#487](https://github.com/vatesfr/xo-web/issues/487))
|
||||
- Patch upload on all connected servers ([xo-web#168](https://github.com/vatesfr/xo-web/issues/168))
|
||||
- Emergency shutdown ([xo-web#185](https://github.com/vatesfr/xo-web/issues/185))
|
||||
- CoreOS/docker template install ([xo-web#246](https://github.com/vatesfr/xo-web/issues/246))
|
||||
- Email for backups ([xo-web#308](https://github.com/vatesfr/xo-web/issues/308))
|
||||
- Console Clipboard ([xo-web#408](https://github.com/vatesfr/xo-web/issues/408))
|
||||
- Logs from CLI ([xo-web#486](https://github.com/vatesfr/xo-web/issues/486))
|
||||
- Save disconnected servers ([xo-web#489](https://github.com/vatesfr/xo-web/issues/489))
|
||||
- Snapshot with quiesce ([xo-web#491](https://github.com/vatesfr/xo-web/issues/491))
|
||||
- Start VM in reovery mode ([xo-web#495](https://github.com/vatesfr/xo-web/issues/495))
|
||||
- Username in logs ([xo-web#498](https://github.com/vatesfr/xo-web/issues/498))
|
||||
- Delete associated tokens with user ([xo-web#500](https://github.com/vatesfr/xo-web/issues/500))
|
||||
- Validate plugin configuration ([xo-web#503](https://github.com/vatesfr/xo-web/issues/503))
|
||||
- Avoid non configured plugins to be loaded ([xo-web#504](https://github.com/vatesfr/xo-web/issues/504))
|
||||
- Verbose API logs if configured ([xo-web#505](https://github.com/vatesfr/xo-web/issues/505))
|
||||
- Better backup overview ([xo-web#512](https://github.com/vatesfr/xo-web/issues/512))
|
||||
- VM auto power on ([xo-web#519](https://github.com/vatesfr/xo-web/issues/519))
|
||||
- Title property supported in config schema ([xo-web#522](https://github.com/vatesfr/xo-web/issues/522))
|
||||
- Start VM export only when necessary ([xo-web#534](https://github.com/vatesfr/xo-web/issues/534))
|
||||
- Input type should be number ([xo-web#538](https://github.com/vatesfr/xo-web/issues/538))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Numbers/int support in plugins config ([xo-web#531](https://github.com/vatesfr/xo-web/issues/531))
|
||||
- Boolean support in plugins config ([xo-web#528](https://github.com/vatesfr/xo-web/issues/528))
|
||||
- Keyboard unusable outside console ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
|
||||
- UsernameField for SAML ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
|
||||
- Wrong display of "no plugin found" ([xo-web#508](https://github.com/vatesfr/xo-web/issues/508))
|
||||
- Bower build error ([xo-web#488](https://github.com/vatesfr/xo-web/issues/488))
|
||||
- VM cloning should require SR permission ([xo-web#472](https://github.com/vatesfr/xo-web/issues/472))
|
||||
- Xen tools status ([xo-web#471](https://github.com/vatesfr/xo-web/issues/471))
|
||||
- Can't delete ghost user ([xo-web#464](https://github.com/vatesfr/xo-web/issues/464))
|
||||
- Stats with old versions of Node ([xo-web#463](https://github.com/vatesfr/xo-web/issues/463))
|
||||
|
||||
## **4.9.0** (2015-11-13)
|
||||
|
||||
Automated DR, restore backup, VM copy
|
||||
|
||||
### Enhancements
|
||||
|
||||
- DR: schedule VM export on other host ([xo-web#447](https://github.com/vatesfr/xo-web/issues/447))
|
||||
- Scheduler logs ([xo-web#390](https://github.com/vatesfr/xo-web/issues/390) and [xo-web#477](https://github.com/vatesfr/xo-web/issues/477))
|
||||
- Restore backups ([xo-web#450](https://github.com/vatesfr/xo-web/issues/350))
|
||||
- Disable backup compression ([xo-web#467](https://github.com/vatesfr/xo-web/issues/467))
|
||||
- Copy VM to another SR (even remote) ([xo-web#475](https://github.com/vatesfr/xo-web/issues/475))
|
||||
- VM stats without time sync ([xo-web#460](https://github.com/vatesfr/xo-web/issues/460))
|
||||
- Stats perfs for high CPU numbers ([xo-web#461](https://github.com/vatesfr/xo-web/issues/461))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Rolling backup bug ([xo-web#484](https://github.com/vatesfr/xo-web/issues/484))
|
||||
- vCPUs/CPUs inversion in dashboard ([xo-web#481](https://github.com/vatesfr/xo-web/issues/481))
|
||||
- Machine to template ([xo-web#459](https://github.com/vatesfr/xo-web/issues/459))
|
||||
|
||||
### Misc
|
||||
|
||||
- Console fix in XenServer ([xo-web#406](https://github.com/vatesfr/xo-web/issues/406))
|
||||
|
||||
## **4.8.0** (2015-10-29)
|
||||
|
||||
Fully automated patch system, ACLs inheritance, stats performance improved.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- ACLs inheritance ([xo-web#279](https://github.com/vatesfr/xo-web/issues/279))
|
||||
- Patch automatically all missing updates ([xo-web#281](https://github.com/vatesfr/xo-web/issues/281))
|
||||
- Intelligent stats polling ([xo-web#432](https://github.com/vatesfr/xo-web/issues/432))
|
||||
- Cache latest result of stats request ([xo-web#431](https://github.com/vatesfr/xo-web/issues/431))
|
||||
- Improve stats polling on multiple objects ([xo-web#433](https://github.com/vatesfr/xo-web/issues/433))
|
||||
- Patch upload task should display the patch name ([xo-web#449](https://github.com/vatesfr/xo-web/issues/449))
|
||||
- Backup filename for Windows ([xo-web#448](https://github.com/vatesfr/xo-web/issues/448))
|
||||
- Specific distro icons ([xo-web#446](https://github.com/vatesfr/xo-web/issues/446))
|
||||
- PXE boot for HVM ([xo-web#436](https://github.com/vatesfr/xo-web/issues/436))
|
||||
- Favicon display before sign in ([xo-web#428](https://github.com/vatesfr/xo-web/issues/428))
|
||||
- Registration renewal ([xo-web#424](https://github.com/vatesfr/xo-web/issues/424))
|
||||
- Reconnect to the host if pool merge fails ([xo-web#403](https://github.com/vatesfr/xo-web/issues/403))
|
||||
- Avoid brute force login ([xo-web#339](https://github.com/vatesfr/xo-web/issues/339))
|
||||
- Missing FreeBSD icon ([xo-web#136](https://github.com/vatesfr/xo-web/issues/136))
|
||||
- Hide halted objects in the Health view ([xo-web#457](https://github.com/vatesfr/xo-web/issues/457))
|
||||
- Click on "Remember me" label ([xo-web#438](https://github.com/vatesfr/xo-web/issues/438))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Pool patches in multiple pools not displayed ([xo-web#442](https://github.com/vatesfr/xo-web/issues/442))
|
||||
- VM Import crashes with Chrome ([xo-web#427](https://github.com/vatesfr/xo-web/issues/427))
|
||||
- Cannot open a direct link ([xo-web#371](https://github.com/vatesfr/xo-web/issues/371))
|
||||
- Patch display edge case ([xo-web#309](https://github.com/vatesfr/xo-web/issues/309))
|
||||
- VM snapshot should require user permission on SR ([xo-web#429](https://github.com/vatesfr/xo-web/issues/429))
|
||||
|
||||
## **4.7.0** (2015-10-12)
|
||||
|
||||
Plugin config management and browser notifications.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Plugin management in the web interface ([xo-web#352](https://github.com/vatesfr/xo-web/issues/352))
|
||||
- Browser notifications ([xo-web#402](https://github.com/vatesfr/xo-web/issues/402))
|
||||
- Graph selector ([xo-web#400](https://github.com/vatesfr/xo-web/issues/400))
|
||||
- Circle packing visualization ([xo-web#374](https://github.com/vatesfr/xo-web/issues/374))
|
||||
- Password generation ([xo-web#397](https://github.com/vatesfr/xo-web/issues/397))
|
||||
- Password reveal during user creation ([xo-web#396](https://github.com/vatesfr/xo-web/issues/396))
|
||||
- Add host to a pool ([xo-web#62](https://github.com/vatesfr/xo-web/issues/62))
|
||||
- Better modal when removing a host from a pool ([xo-web#405](https://github.com/vatesfr/xo-web/issues/405))
|
||||
- Drop focus on CD/ISO selector ([xo-web#290](https://github.com/vatesfr/xo-web/issues/290))
|
||||
- Allow non persistent session ([xo-web#243](https://github.com/vatesfr/xo-web/issues/243))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- VM export permission corrected ([xo-web#410](https://github.com/vatesfr/xo-web/issues/410))
|
||||
- Proper host removal in a pool ([xo-web#402](https://github.com/vatesfr/xo-web/issues/402))
|
||||
- Sub-optimal tooltip placement ([xo-web#421](https://github.com/vatesfr/xo-web/issues/421))
|
||||
- VM migrate host incorrect target ([xo-web#419](https://github.com/vatesfr/xo-web/issues/419))
|
||||
- Alone host can't leave its pool ([xo-web#414](https://github.com/vatesfr/xo-web/issues/414))
|
||||
|
||||
## **4.6.0** (2015-09-25)
|
||||
|
||||
Tags management and new visualization.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Multigraph for correlation ([xo-web#358](https://github.com/vatesfr/xo-web/issues/358))
|
||||
- Tags management ([xo-web#367](https://github.com/vatesfr/xo-web/issues/367))
|
||||
- Google Provider for authentication ([xo-web#363](https://github.com/vatesfr/xo-web/issues/363))
|
||||
- Password change for users ([xo-web#362](https://github.com/vatesfr/xo-web/issues/362))
|
||||
- Better live migration process ([xo-web#237](https://github.com/vatesfr/xo-web/issues/237))
|
||||
- VDI search filter in SR view ([xo-web#222](https://github.com/vatesfr/xo-web/issues/222))
|
||||
- PV args during VM creation ([xo-web#112](https://github.com/vatesfr/xo-web/issues/330))
|
||||
- PV args management ([xo-web#394](https://github.com/vatesfr/xo-web/issues/394))
|
||||
- Confirmation dialog on important actions ([xo-web#350](https://github.com/vatesfr/xo-web/issues/350))
|
||||
- New favicon ([xo-web#369](https://github.com/vatesfr/xo-web/issues/369))
|
||||
- Filename of VM for exports ([xo-web#370](https://github.com/vatesfr/xo-web/issues/370))
|
||||
- ACLs rights edited on the fly ([xo-web#323](https://github.com/vatesfr/xo-web/issues/323))
|
||||
- Heatmap values now human readable ([xo-web#342](https://github.com/vatesfr/xo-web/issues/342))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Export backup fails if no tags specified ([xo-web#383](https://github.com/vatesfr/xo-web/issues/383))
|
||||
- Wrong login give an obscure error message ([xo-web#373](https://github.com/vatesfr/xo-web/issues/373))
|
||||
- Update view is broken during updates ([xo-web#356](https://github.com/vatesfr/xo-web/issues/356))
|
||||
- Settings/dashboard menu incorrect display ([xo-web#357](https://github.com/vatesfr/xo-web/issues/357))
|
||||
- Console View Not refreshing if the VM restart ([xo-web#107](https://github.com/vatesfr/xo-web/issues/107))
|
||||
|
||||
## **4.5.1** (2015-09-16)
|
||||
|
||||
An issue in `xo-web` with the VM view.
|
||||
|
||||
### Bug fix
|
||||
|
||||
- Attach disk/new disk/create interface is broken ([xo-web#378](https://github.com/vatesfr/xo-web/issues/378))
|
||||
|
||||
## **4.5.0** (2015-09-11)
|
||||
|
||||
A new dataviz (parallel coord), a new provider (GitHub) and faster consoles.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Parallel coordinates view ([xo-web#333](https://github.com/vatesfr/xo-web/issues/333))
|
||||
- Faster consoles ([xo-web#337](https://github.com/vatesfr/xo-web/issues/337))
|
||||
- Disable/hide button ([xo-web#268](https://github.com/vatesfr/xo-web/issues/268))
|
||||
- More details on missing-guest-tools ([xo-web#304](https://github.com/vatesfr/xo-web/issues/304))
|
||||
- Scheduler meta data export ([xo-web#315](https://github.com/vatesfr/xo-web/issues/315))
|
||||
- Better heatmap ([xo-web#330](https://github.com/vatesfr/xo-web/issues/330))
|
||||
- Faster dashboard ([xo-web#331](https://github.com/vatesfr/xo-web/issues/331))
|
||||
- Faster sunburst ([xo-web#332](https://github.com/vatesfr/xo-web/issues/332))
|
||||
- GitHub provider for auth ([xo-web#334](https://github.com/vatesfr/xo-web/issues/334))
|
||||
- Filter networks for users ([xo-web#347](https://github.com/vatesfr/xo-web/issues/347))
|
||||
- Add networks in ACLs ([xo-web#348](https://github.com/vatesfr/xo-web/issues/348))
|
||||
- Better looking login page ([xo-web#341](https://github.com/vatesfr/xo-web/issues/341))
|
||||
- Real time dataviz (dashboard) ([xo-web#349](https://github.com/vatesfr/xo-web/issues/349))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Typo in dashboard ([xo-web#355](https://github.com/vatesfr/xo-web/issues/355))
|
||||
- Global RAM usage fix ([xo-web#356](https://github.com/vatesfr/xo-web/issues/356))
|
||||
- Re-allowing XO behind a reverse proxy ([xo-web#361](https://github.com/vatesfr/xo-web/issues/361))
|
||||
|
||||
## **4.4.0** (2015-08-28)
|
||||
|
||||
SSO and Dataviz are the main features for this release.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Dataviz storage usage ([xo-web#311](https://github.com/vatesfr/xo-web/issues/311))
|
||||
- Heatmap in health view ([xo-web#329](https://github.com/vatesfr/xo-web/issues/329))
|
||||
- SSO for SAML and other providers ([xo-web#327](https://github.com/vatesfr/xo-web/issues/327))
|
||||
- Better UI for ACL objects attribution ([xo-web#320](https://github.com/vatesfr/xo-web/issues/320))
|
||||
- Refresh the browser after an update ([xo-web#318](https://github.com/vatesfr/xo-web/issues/318))
|
||||
- Clean CSS and Flexbox usage ([xo-web#239](https://github.com/vatesfr/xo-web/issues/239))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Admin only accessible views ([xo-web#328](https://github.com/vatesfr/xo-web/issues/328))
|
||||
- Hide "base copy" VDIs ([xo-web#324](https://github.com/vatesfr/xo-web/issues/324))
|
||||
- ACLs on VIFs for non-admins ([xo-web#322](https://github.com/vatesfr/xo-web/issues/322))
|
||||
- Updater display problems ([xo-web#313](https://github.com/vatesfr/xo-web/issues/313))
|
||||
|
||||
## **4.3.0** (2015-07-22)
|
||||
|
||||
Scheduler for rolling backups
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Rolling backup scheduler ([xo-web#278](https://github.com/vatesfr/xo-web/issues/278))
|
||||
- Clean snapshots of removed VMs ([xo-web#301](https://github.com/vatesfr/xo-web/issues/301))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- VM export ([xo-web#307](https://github.com/vatesfr/xo-web/issues/307))
|
||||
- Remove VM VDIs ([xo-web#303](https://github.com/vatesfr/xo-web/issues/303))
|
||||
- Pagination fails ([xo-web#302](https://github.com/vatesfr/xo-web/issues/302))
|
||||
|
||||
## **4.2.0** (2015-06-29)
|
||||
|
||||
Huge performance boost, scheduler for rolling snapshots and backward compatibility for XS 5.x series
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Rolling snapshots scheduler ([xo-web#176](https://github.com/vatesfr/xo-web/issues/176))
|
||||
- Huge perf boost ([xen-api#1](https://github.com/julien-f/js-xen-api/issues/1))
|
||||
- Backward compatibility ([xo-web#296](https://github.com/vatesfr/xo-web/issues/296))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- VDI attached on a VM missing in SR view ([xo-web#294](https://github.com/vatesfr/xo-web/issues/294))
|
||||
- Better VM creation process ([xo-web#292](https://github.com/vatesfr/xo-web/issues/292))
|
||||
|
||||
## **4.1.0** (2015-06-10)
|
||||
|
||||
Add the drag'n drop support from VM live migration, better ACLs groups UI.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Drag'n drop VM in tree view for live migration ([xo-web#277](https://github.com/vatesfr/xo-web/issues/277))
|
||||
- Better group view with objects ACLs ([xo-web#276](https://github.com/vatesfr/xo-web/issues/276))
|
||||
- Hide non-visible objects ([xo-web#272](https://github.com/vatesfr/xo-web/issues/272))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Convert to template displayed when the VM is not halted ([xo-web#286](https://github.com/vatesfr/xo-web/issues/286))
|
||||
- Lost some data when refresh some views ([xo-web#271](https://github.com/vatesfr/xo-web/issues/271))
|
||||
- Suspend button don't trigger any permission message ([xo-web#270](https://github.com/vatesfr/xo-web/issues/270))
|
||||
- Create network interfaces shouldn't call xoApi directly ([xo-web#269](https://github.com/vatesfr/xo-web/issues/269))
|
||||
- Don't plug automatically a disk or a VIF if the VM is not running ([xo-web#287](https://github.com/vatesfr/xo-web/issues/287))
|
||||
|
||||
## **4.0.2** (2015-06-01)
|
||||
|
||||
An issue in `xo-server` with the password of default admin account and also a UI fix.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Cannot modify admin account ([xo-web#265](https://github.com/vatesfr/xo-web/issues/265))
|
||||
- Password field seems to keep empty/reset itself after 1-2 seconds ([xo-web#264](https://github.com/vatesfr/xo-web/issues/264))
|
||||
|
||||
## **4.0.1** (2015-05-30)
|
||||
|
||||
An issue with the updater in HTTPS was left in the *4.0.0*. This patch release fixed
|
||||
it.
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- allow updater to work in HTTPS ([xo-web#266](https://github.com/vatesfr/xo-web/issues/266))
|
||||
|
||||
## **4.0.0** (2015-05-29)
|
||||
|
||||
[Blog post of this release](https://xen-orchestra.com/blog/xen-orchestra-4-0).
|
||||
|
||||
## Enhancements
|
||||
### Enhancements
|
||||
|
||||
- advanced ACLs ([xo-web#209](https://github.com/vatesfr/xo-web/issues/209))
|
||||
- xenserver update management ([xo-web#174](https://github.com/vatesfr/xo-web/issues/174) & [xo-web#259](https://github.com/vatesfr/xo-web/issues/259))
|
||||
@@ -18,7 +561,7 @@
|
||||
- new mapped collection([xo-server#47](https://github.com/vatesfr/xo-server/issues/47))
|
||||
- resource location in ACL view ([xo-web#245](https://github.com/vatesfr/xo-web/issues/245))
|
||||
|
||||
## Bug fixes
|
||||
### Bug fixes
|
||||
|
||||
- wrong calulation of RAM amounts ([xo-web#51](https://github.com/vatesfr/xo-web/issues/51))
|
||||
- checkbox not aligned ([xo-web#253](https://github.com/vatesfr/xo-web/issues/253))
|
||||
@@ -31,7 +574,7 @@
|
||||
A few bugs hve made their way into *3.9.0*, this minor release fixes
|
||||
them.
|
||||
|
||||
## Bug fixes
|
||||
### Bug fixes
|
||||
|
||||
- correctly keep the VM guest metrics up to date ([xo-web#172](https://github.com/vatesfr/xo-web/issues/172))
|
||||
- fix edition of a VM snapshot ([b04111c](https://github.com/vatesfr/xo-server/commit/b04111c79ba8937778b84cb861bb7c2431162c11))
|
||||
@@ -44,7 +587,7 @@ them.
|
||||
|
||||
[Blog post of this release](https://xen-orchestra.com/blog/xen-orchestra-3-9).
|
||||
|
||||
## Enhancements
|
||||
### Enhancements
|
||||
|
||||
- ability to manually connect/disconnect a server ([xo-web#88](https://github.com/vatesfr/xo-web/issues/88) & [xo-web#234](https://github.com/vatesfr/xo-web/issues/234))
|
||||
- display the connection status of a server ([xo-web#103](https://github.com/vatesfr/xo-web/issues/103))
|
||||
@@ -58,7 +601,7 @@ them.
|
||||
- XO-Server sources are compiled to JS prior distribution: less bugs & faster startups ([xo-server#50](https://github.com/vatesfr/xo-server/issues/50))
|
||||
- use XAPI `event.from()` instead of `event.next()` which leads to faster connection ([xo-server#52](https://github.com/vatesfr/xo-server/issues/52))
|
||||
|
||||
## Bug fixes
|
||||
### Bug fixes
|
||||
|
||||
- removed servers are properly disconnected ([xo-web#61](https://github.com/vatesfr/xo-web/issues/61))
|
||||
- fix VM creation with multiple interfaces ([xo-wb#229](https://github.com/vatesfr/xo-wb/issues/229))
|
||||
@@ -68,7 +611,7 @@ them.
|
||||
|
||||
[Blog post of this release](https://xen-orchestra.com/blog/xen-orchestra-3-8).
|
||||
|
||||
## Enhancements
|
||||
### Enhancements
|
||||
|
||||
- initial plugin system ([xo-server#37](https://github.com/vatesfr/xo-server/issues/37))
|
||||
- new authentication system based on providers ([xo-server#39](https://github.com/vatesfr/xo-server/issues/39))
|
||||
@@ -77,7 +620,7 @@ them.
|
||||
- network creation on the VM page ([xo-web#216](https://github.com/vatesfr/xo-web/issues/216))
|
||||
- charts on the host and SR pages ([xo-web#217](https://github.com/vatesfr/xo-web/issues/217))
|
||||
|
||||
## Bug fixes
|
||||
### Bug fixes
|
||||
|
||||
- fix *Invalid parameter(s)* message on the settings page ([xo-server#49](https://github.com/vatesfr/xo-server/issues/49))
|
||||
- fix mouse clicks in console ([xo-web#205](https://github.com/vatesfr/xo-web/issues/205))
|
||||
|
||||
28
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,28 @@
|
||||
<!--
|
||||
Welcome to the issue section of Xen Orchestra!
|
||||
|
||||
Here you can:
|
||||
- report an issue
|
||||
- propose an enhancement
|
||||
- ask a question
|
||||
|
||||
The template below is only a proposition for your ticket, feel free to
|
||||
change it as appropriate :)
|
||||
-->
|
||||
|
||||
### Context
|
||||
|
||||
- **XO version**: XO appliance / `stable` branch / `next-release` branch
|
||||
|
||||
If from the sources:
|
||||
|
||||
- **Component**: xo-web / xo-server / *unknown*
|
||||
- **Node/npm version**: *just execute `npm version`*
|
||||
|
||||
### Expected behavior
|
||||
|
||||
<!-- What you expect to happen -->
|
||||
|
||||
### Current behavior
|
||||
|
||||
<!-- What is actually happening -->
|
||||
43
README.md
@@ -1,4 +1,4 @@
|
||||
# Xen Orchestra Web
|
||||
# Xen Orchestra Web [](https://travis-ci.org/vatesfr/xo-web)
|
||||
|
||||

|
||||
|
||||
@@ -6,9 +6,6 @@ XO-Web is part of [Xen Orchestra](https://github.com/vatesfr/xo), a web interfac
|
||||
|
||||
It is a web client for [XO-Server](https://github.com/vatesfr/xo-server).
|
||||
|
||||
[](https://david-dm.org/vatesfr/xo-web)
|
||||
[](https://david-dm.org/vatesfr/xo-web#info=devDependencies)
|
||||
|
||||
___
|
||||
|
||||
## Installation
|
||||
@@ -29,6 +26,31 @@ Development build:
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
### Environment
|
||||
|
||||
#### `NODE_ENV`
|
||||
|
||||
Set to *production* it disables many checks which result in increased
|
||||
performance.
|
||||
|
||||
#### `XOA_PLAN`
|
||||
|
||||
- 1: Free
|
||||
- 2: Starter
|
||||
- 3: Enterprise
|
||||
- 4: Premium
|
||||
- 5: Sources
|
||||
|
||||
```js
|
||||
if (process.env.XOA_PLAN < 5) {
|
||||
console.log('included only in XOA')
|
||||
}
|
||||
|
||||
if (process.env.XOA_PLAN > 3) {
|
||||
console.log('included only in Premium and Sources')
|
||||
}
|
||||
```
|
||||
|
||||
## How to report a bug?
|
||||
|
||||
If you are certain the bug is exclusively related to XO-Web, you may use the [bugtracker of this repository](https://github.com/vatesfr/xo-web/issues).
|
||||
@@ -38,8 +60,11 @@ Otherwise, please consider using the [bugtracker of the general repository](http
|
||||
## Process for new release
|
||||
|
||||
```bash
|
||||
# Switch to the master branch.
|
||||
git checkout master
|
||||
# Switch to the stable branch.
|
||||
git checkout stable
|
||||
|
||||
# Fetches latest changes.
|
||||
git pull --ff-only
|
||||
|
||||
# Merge changes of the next-release branch.
|
||||
git merge next-release
|
||||
@@ -50,12 +75,12 @@ npm version minor
|
||||
# Go back to the next-release branch.
|
||||
git checkout next-release
|
||||
|
||||
# Fetches the last changes (the merge and version bump) from master to
|
||||
# Fetches the last changes (the merge and version bump) from stable to
|
||||
# next-release.
|
||||
git pull --fast-forward master
|
||||
git merge --ff-only stable
|
||||
|
||||
# Push the changes on git.
|
||||
git push origin master:master next-release:next-release
|
||||
git push --follow-tags origin stable next-release
|
||||
|
||||
# Publish this release to npm.
|
||||
npm publish
|
||||
|
||||
180
app/app.js
@@ -1,180 +0,0 @@
|
||||
// Must be loaded before angular.
|
||||
import 'angular-file-upload'
|
||||
|
||||
import angular from 'angular'
|
||||
import uiBootstrap from'angular-ui-bootstrap'
|
||||
import uiIndeterminate from'angular-ui-indeterminate'
|
||||
import uiRouter from'angular-ui-router'
|
||||
import uiSelect from'angular-ui-select'
|
||||
|
||||
import naturalSort from 'angular-natural-sort'
|
||||
import xeditable from 'angular-xeditable'
|
||||
|
||||
import xoDirectives from 'xo-directives'
|
||||
import xoFilters from 'xo-filters'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import aboutState from './modules/about'
|
||||
import consoleState from './modules/console'
|
||||
import deleteVmsState from './modules/delete-vms'
|
||||
import genericModalState from './modules/generic-modal'
|
||||
import hostState from './modules/host'
|
||||
import listState from './modules/list'
|
||||
import loginState from './modules/login'
|
||||
import navbarState from './modules/navbar'
|
||||
import newSrState from './modules/new-sr'
|
||||
import newVmState from './modules/new-vm'
|
||||
import poolState from './modules/pool'
|
||||
import settingsState from './modules/settings'
|
||||
import srState from './modules/sr'
|
||||
import treeState from './modules/tree'
|
||||
import updater from './modules/updater'
|
||||
import vmState from './modules/vm'
|
||||
|
||||
import '../dist/bower_components/angular-chart.js/dist/angular-chart.js'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp', [
|
||||
uiBootstrap,
|
||||
uiIndeterminate,
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
|
||||
naturalSort,
|
||||
xeditable,
|
||||
|
||||
xoDirectives,
|
||||
xoFilters,
|
||||
xoServices,
|
||||
|
||||
aboutState,
|
||||
consoleState,
|
||||
deleteVmsState,
|
||||
genericModalState,
|
||||
hostState,
|
||||
listState,
|
||||
loginState,
|
||||
navbarState,
|
||||
newSrState,
|
||||
newVmState,
|
||||
poolState,
|
||||
settingsState,
|
||||
srState,
|
||||
treeState,
|
||||
updater,
|
||||
vmState,
|
||||
'chart.js'
|
||||
])
|
||||
|
||||
// Prevent Angular.js from mangling exception stack (interfere with
|
||||
// source maps).
|
||||
.factory('$exceptionHandler', () => function (exception) {
|
||||
throw exception
|
||||
})
|
||||
|
||||
.config(function (
|
||||
$compileProvider,
|
||||
$stateProvider,
|
||||
$urlRouterProvider,
|
||||
$tooltipProvider,
|
||||
uiSelectConfig
|
||||
) {
|
||||
// Disable debug data to improve performance.
|
||||
//
|
||||
// In case of a bug, simply use `angular.reloadWithDebugInfo()` in
|
||||
// the console.
|
||||
//
|
||||
// See https://docs.angularjs.org/guide/production
|
||||
$compileProvider.debugInfoEnabled(false)
|
||||
|
||||
// Redirect to default state.
|
||||
$stateProvider.state('index', {
|
||||
url: '/',
|
||||
controller: function ($state, xoApi) {
|
||||
let isAdmin = xoApi.user && (xoApi.user.permission === 'admin')
|
||||
|
||||
$state.go(isAdmin ? 'tree' : 'list')
|
||||
}
|
||||
})
|
||||
|
||||
// Redirects unmatched URLs to `/`.
|
||||
$urlRouterProvider.otherwise('/')
|
||||
|
||||
// Changes the default settings for the tooltips.
|
||||
$tooltipProvider.options({
|
||||
appendToBody: true,
|
||||
placement: 'bottom'
|
||||
})
|
||||
|
||||
uiSelectConfig.theme = 'bootstrap'
|
||||
uiSelectConfig.resetSearchInput = true
|
||||
})
|
||||
.run(function (
|
||||
$anchorScroll,
|
||||
$rootScope,
|
||||
$state,
|
||||
editableOptions,
|
||||
editableThemes,
|
||||
notify,
|
||||
updater,
|
||||
xoApi
|
||||
) {
|
||||
$rootScope.$on('$stateChangeStart', function (event, state, stateParams) {
|
||||
let {user} = xoApi
|
||||
let loggedIn = !!user
|
||||
|
||||
if (state.name === 'login') {
|
||||
if (loggedIn) {
|
||||
event.preventDefault()
|
||||
$state.go('index')
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!loggedIn) {
|
||||
event.preventDefault()
|
||||
|
||||
// FIXME: find a better way to pass info to the login controller.
|
||||
$rootScope._login = { state, stateParams }
|
||||
|
||||
$state.go('login')
|
||||
return
|
||||
}
|
||||
|
||||
if (user.permission === 'admin') {
|
||||
return
|
||||
}
|
||||
|
||||
// The user must have the `admin` permission to access the
|
||||
// settings pages.
|
||||
if (/^settings\..*|tree$/.test(state.name)) {
|
||||
event.preventDefault()
|
||||
notify.error({
|
||||
title: 'Restricted area',
|
||||
message: 'You do not have the permission to view this page'
|
||||
})
|
||||
}
|
||||
|
||||
let {id} = stateParams
|
||||
if (id && !xoApi.canAccess(id)) {
|
||||
event.preventDefault()
|
||||
notify.error({
|
||||
title: 'Restricted area',
|
||||
message: 'You do not have the permission to view this page'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Work around UI Router bug (https://github.com/angular-ui/ui-router/issues/1509)
|
||||
$rootScope.$on('$stateChangeSuccess', function () {
|
||||
$anchorScroll()
|
||||
})
|
||||
|
||||
editableThemes.bs3.inputClass = 'input-sm'
|
||||
editableThemes.bs3.buttonsClass = 'btn-sm'
|
||||
editableOptions.theme = 'bs3'
|
||||
})
|
||||
|
||||
.name
|
||||
BIN
app/favicon.ico
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 91 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 34 KiB |
@@ -1,62 +0,0 @@
|
||||
//- HTML 5 Doctype
|
||||
doctype html
|
||||
|
||||
//- The “no-js” class will be automatically removed if JavaScript is
|
||||
//- available.
|
||||
html.no-js(lang="en", dir="ltr")
|
||||
head
|
||||
meta(charset="utf-8")
|
||||
//- This file is a part of Xen Orchestra Web.
|
||||
//-
|
||||
//- Xen Orchestra Web is free software: you can redistribute it and/or
|
||||
//- modify it under the terms of the GNU Affero General Public License
|
||||
//- as published by the Free Software Foundation, either version 3 of
|
||||
//- the License, or (at your option) any later version.
|
||||
//-
|
||||
//- Xen Orchestra Web is distributed in the hope that it will be
|
||||
//- useful, but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
//- of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
//- Affero General Public License for more details.
|
||||
//-
|
||||
//- You should have received a copy of the GNU Affero General Public License
|
||||
//- along with Xen Orchestra Web. If not, see
|
||||
//- <http://www.gnu.org/licenses/>.
|
||||
//-
|
||||
//- @author Olivier Lambert <olivier.lambert@vates.fr>
|
||||
//- @license http://www.gnu.org/licenses/agpl-3.0-standalone.html GNU AGPLv3
|
||||
//-
|
||||
//- @package Xen Orchestra Web
|
||||
|
||||
//- Makes sure IE is using the last engine available.
|
||||
meta(http-equiv="X-UA-Compatible", content="IE=edge,chrome=1")
|
||||
|
||||
//- Replaces the “no-js” class by the “js” class if JavaScript is
|
||||
//- available.
|
||||
script.
|
||||
!function(d){d.className=d.className.replace(/\\bno-js\b/,'js')}(document.documentElement)
|
||||
|
||||
//- (To confirm.) For smartphones and tablets: sets the page
|
||||
//- width to the device width and prevents the page from being
|
||||
//- zoomed in when going to landscape mode.
|
||||
meta(name="viewport", content="width=device-width, initial-scale=1.0")
|
||||
|
||||
title Xen Orchestra
|
||||
meta(name="description", content="Web interface for XenServer/XAPI Hosts")
|
||||
meta(name="author", content="Vates SAS")
|
||||
|
||||
//- Place favicon.ico and apple-touch-icon.png in the root directory
|
||||
link(rel="stylesheet", href="styles/main.css")
|
||||
body(
|
||||
ng-app = 'xoWebApp'
|
||||
)
|
||||
|
||||
toaster-container
|
||||
|
||||
//- Navigation bar.
|
||||
navbar
|
||||
|
||||
//- Main content (managed by the router).
|
||||
.view-main(ui-view = "")
|
||||
|
||||
script(src="bower_components/Chart.js/Chart.min.js")
|
||||
script(src="app.js")
|
||||
@@ -1,22 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import pkg from '../../../package'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.about', [
|
||||
uiRouter
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('about', {
|
||||
url: '/about',
|
||||
controller: 'AboutCtrl',
|
||||
template: require('./view')
|
||||
})
|
||||
})
|
||||
.controller('AboutCtrl', function ($scope) {
|
||||
$scope.pkg = pkg
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
@@ -1,50 +0,0 @@
|
||||
//- TODO: lots of stuff.
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title About Xen Orchestra
|
||||
p.text-center ({{pkg.name}} {{pkg.version}})
|
||||
.grid
|
||||
//- Vates
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-lightbulb-o(style="color: #e25440;")
|
||||
| Vates
|
||||
.panel-body
|
||||
p.text-center
|
||||
| We are the team behind Xen Orchestra, we are Vates! We create Open Source products and we offer commercial support for Xen and Xen Orchestra. Want to know more about us? Go to our website!
|
||||
p.text-center
|
||||
img(src="images/arrow.png")
|
||||
br
|
||||
p.text-center
|
||||
a.btn.btn-success(href="https://vates.fr")
|
||||
i.fa.fa-hand-o-right
|
||||
| Our website
|
||||
//- Open Source
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-thumbs-up(style="color: #e25440;")
|
||||
| Open Source
|
||||
.panel-body
|
||||
p.text-center
|
||||
| This project is Open Source (AGPL), everyone is welcome aboard! You want a specific feature in XO? Report a bug? Go to our project website, read the FAQ and get involved in the project!
|
||||
p.text-center
|
||||
img(src="images/opensource.png")
|
||||
br
|
||||
p.text-center
|
||||
a.btn.btn-info(href="https://xen-orchestra.com")
|
||||
i.fa.fa-flask
|
||||
| Project website
|
||||
//- Pro support
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-truck(style="color: #e25440;")
|
||||
| Pro Support Delivered
|
||||
.panel-body
|
||||
p.text-center
|
||||
| Our XO Appliance can be delivered with professional support: stay relaxed, we got your back! You can also have assitance for deploying or upgrade your virtualized infrastructure through our deep understanding of Xen.
|
||||
p.text-center
|
||||
img(src="images/support.png")
|
||||
p.text-center
|
||||
a.btn.btn-primary(href="https://xen-orchestra.com/services/")
|
||||
i.fa.fa-envelope
|
||||
| Get services
|
||||
@@ -1,91 +0,0 @@
|
||||
angular = require 'angular'
|
||||
forEach = require('lodash.foreach')
|
||||
includes = require('lodash.includes')
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.console', [
|
||||
require 'angular-ui-router'
|
||||
|
||||
require 'angular-no-vnc'
|
||||
]
|
||||
.config ($stateProvider) ->
|
||||
$stateProvider.state 'consoles_view',
|
||||
url: '/consoles/:id'
|
||||
controller: 'ConsoleCtrl'
|
||||
template: require './view'
|
||||
.controller 'ConsoleCtrl', ($scope, $stateParams, xoApi, xo) ->
|
||||
{id} = $stateParams
|
||||
{get} = xoApi
|
||||
push = Array::push.apply.bind Array::push
|
||||
merge = do ->
|
||||
(args...) ->
|
||||
result = []
|
||||
for arg in args
|
||||
push result, arg if arg?
|
||||
result
|
||||
|
||||
srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
|
||||
$scope.$watch(
|
||||
-> xoApi.get id
|
||||
(VM) ->
|
||||
$scope.consoleUrl = null
|
||||
|
||||
unless xoApi.user
|
||||
$scope.VDIs = []
|
||||
return
|
||||
|
||||
$scope.VM = VM
|
||||
return unless (
|
||||
VM? and
|
||||
VM.power_state is 'Running' and
|
||||
not includes(VM.current_operations, 'clean_reboot')
|
||||
)
|
||||
|
||||
pool = get VM.$poolId
|
||||
return unless pool
|
||||
|
||||
$scope.consoleUrl = "./api/consoles/#{id}"
|
||||
|
||||
host = get VM.$container # host because the VM is running.
|
||||
return unless host
|
||||
|
||||
# FIXME: We should filter on connected SRs (PBDs)!
|
||||
SRs = []
|
||||
forEach(srsByContainer[host.id], (template) =>
|
||||
SRs.push(template)
|
||||
return
|
||||
)
|
||||
forEach(srsByContainer[pool.id], (template) =>
|
||||
SRs.push(template)
|
||||
return
|
||||
)
|
||||
$scope.VDIs = do ->
|
||||
VDIs = []
|
||||
for SR in SRs
|
||||
push VDIs, SR.VDIs if SR.content_type is 'iso'
|
||||
get VDIs
|
||||
|
||||
cdDrive = do ->
|
||||
return VBD for VBD in (get VM.$VBDs) when VBD.is_cd_drive
|
||||
null
|
||||
|
||||
$scope.mountedIso =
|
||||
if cdDrive and cdDrive.VDI and (VDI = get cdDrive.VDI)
|
||||
VDI.id
|
||||
else
|
||||
''
|
||||
)
|
||||
|
||||
$scope.startVM = xo.vm.start
|
||||
$scope.stopVM = xo.vm.stop
|
||||
$scope.rebootVM = xo.vm.restart
|
||||
|
||||
$scope.eject = ->
|
||||
xo.vm.ejectCd id
|
||||
$scope.insert = (disc_id) ->
|
||||
xo.vm.insertCd id, disc_id, true
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
@@ -1,65 +0,0 @@
|
||||
.container: .panel.panel-default
|
||||
|
||||
//- Title
|
||||
p.page-title
|
||||
span.fa-stack
|
||||
i.fa.fa-square-o.fa-stack-2x
|
||||
i.xo-icon-console.fa-stack-1x(class = 'xo-color-{{VM.power_state | lowercase}}')
|
||||
|
|
||||
a(
|
||||
class = 'xo-color-{{VM.power_state | lowercase}}'
|
||||
ui-sref = 'VMs_view({id: VM.id})'
|
||||
) {{VM.name_label}}
|
||||
|
||||
.list-group
|
||||
|
||||
//- Toolbar
|
||||
.list-group-item: .row.text-center
|
||||
.col-sm-6: .input-group
|
||||
select.form-control(
|
||||
ng-model = 'mountedIso'
|
||||
ng-change = 'insert(mountedIso)'
|
||||
ng-options = 'VDI.id as VDI.name_label group by (VDI.$SR | resolve).name_label for VDI in VDIs | orderBy:natural("name_label")'
|
||||
)
|
||||
.input-group-btn
|
||||
button.btn.btn-default(
|
||||
ng-click = 'eject()'
|
||||
ng-disabled = '!mountedIso'
|
||||
)
|
||||
i.fa.fa-eject
|
||||
.col-sm-3: button.btn.btn-default(
|
||||
ng-click = 'vncRemote.sendCtrlAltDel()'
|
||||
)
|
||||
i.fa.fa-keyboard-o
|
||||
|
|
||||
| Ctrl+Alt+Del
|
||||
//- Action panel
|
||||
.col-sm-3
|
||||
.btn-group
|
||||
button.btn.btn-default.inversed(
|
||||
ng-if = "VM.power_state == ('Running' || 'Paused')"
|
||||
tooltip = "Stop VM"
|
||||
type = "button"
|
||||
xo-click = "stopVM(VM.id)"
|
||||
)
|
||||
i.fa.fa-stop.fa-fw
|
||||
button.btn.btn-default.inversed(
|
||||
ng-if = "VM.power_state == ('Halted')"
|
||||
tooltip = "Start VM"
|
||||
type = "button"
|
||||
xo-click = "startVM(VM.id)"
|
||||
)
|
||||
i.fa.fa-play.fa-fw
|
||||
button.btn.btn-default.inversed(
|
||||
ng-if = "VM.power_state == ('Running' || 'Paused')"
|
||||
tooltip = "Reboot VM"
|
||||
type = "button"
|
||||
xo-click = "rebootVM(VM.id)"
|
||||
)
|
||||
i.fa.fa-refresh.fa-fw
|
||||
//- Console
|
||||
.list-group-item
|
||||
no-vnc(
|
||||
url = '{{consoleUrl}}'
|
||||
remote-control = 'vncRemote'
|
||||
)
|
||||
@@ -1,63 +0,0 @@
|
||||
// TODO: should be integrated xo.deleteVms()
|
||||
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.deleteVms', [
|
||||
uiBootstrap,
|
||||
|
||||
xoServices
|
||||
])
|
||||
.controller('DeleteVmsCtrl', function (
|
||||
$scope,
|
||||
$modalInstance,
|
||||
xoApi,
|
||||
VMsIds
|
||||
) {
|
||||
$scope.$watchCollection(() => xoApi.get(VMsIds), function (VMs) {
|
||||
$scope.VMs = VMs
|
||||
})
|
||||
|
||||
// Do disks have to be deleted for a given VM.
|
||||
let disks = $scope.disks = {}
|
||||
forEach(VMsIds, id => {
|
||||
disks[id] = true
|
||||
})
|
||||
|
||||
$scope.delete = function () {
|
||||
let value = []
|
||||
forEach(VMsIds, id => {
|
||||
value.push([id, disks[id]])
|
||||
})
|
||||
|
||||
$modalInstance.close(value)
|
||||
}
|
||||
})
|
||||
.service('deleteVmsModal', function ($modal, xo) {
|
||||
return function (ids) {
|
||||
return $modal.open({
|
||||
controller: 'DeleteVmsCtrl',
|
||||
template: view,
|
||||
resolve: {
|
||||
VMsIds: () => ids
|
||||
}
|
||||
}).result.then(function (toDelete) {
|
||||
let promises = []
|
||||
|
||||
forEach(toDelete, ([id, deleteDisks]) => {
|
||||
promises.push(xo.vm.delete(id, deleteDisks))
|
||||
})
|
||||
|
||||
return promises
|
||||
})
|
||||
}
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
@@ -1,24 +0,0 @@
|
||||
form(ng-submit="delete()")
|
||||
.modal-header
|
||||
h3 VMs deletion
|
||||
.modal-body
|
||||
p
|
||||
| You are going to delete the following VMs, this is a
|
||||
strong dangerous action
|
||||
| !
|
||||
table.table
|
||||
tr
|
||||
th.col-sm-3 Name
|
||||
th.col-sm-6 Description
|
||||
th.col-sm-3 Delete disks?
|
||||
tbody
|
||||
tr(ng-repeat="VM in VMs | orderBy:natural('name_label') track by VM.id")
|
||||
td {{VM.name_label}}
|
||||
td {{VM.name_description}}
|
||||
td
|
||||
input(type="checkbox", ng-model="disks[VM.id]")
|
||||
.modal-footer
|
||||
button.btn.btn-primary(type="submit")
|
||||
| Delete
|
||||
button.btn.btn-warning(type="button", ng-click="$dismiss()")
|
||||
| Cancel
|
||||
@@ -1,39 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.genericModal', [
|
||||
uiBootstrap
|
||||
])
|
||||
.controller('GenericModalCtrl', function ($scope, $modalInstance, options) {
|
||||
$scope.title = options.title
|
||||
$scope.message = options.message
|
||||
|
||||
$scope.yesButtonLabel = options.yesButtonLabel || 'Ok'
|
||||
$scope.noButtonLabel = options.noButtonLabel
|
||||
})
|
||||
.service('modal', function ($modal) {
|
||||
return {
|
||||
confirm: function (opts) {
|
||||
const modal = $modal.open({
|
||||
controller: 'GenericModalCtrl',
|
||||
template: require('./view'),
|
||||
resolve: {
|
||||
options: function () {
|
||||
return {
|
||||
title: opts.title,
|
||||
message: opts.message,
|
||||
noButtonLabel: 'Cancel'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return modal.result
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
@@ -1,11 +0,0 @@
|
||||
.modal-header
|
||||
h3
|
||||
i.fa.fa-exclamation-triangle.text-danger
|
||||
| {{title}}
|
||||
.modal-body
|
||||
| {{message}}
|
||||
.modal-footer
|
||||
button.btn.btn-primary(type="button", ng-click="$close()")
|
||||
| {{yesButtonLabel}}
|
||||
button.btn.btn-warning(ng-if="noButtonLabel", type="button", ng-click="$dismiss()")
|
||||
| {{noButtonLabel}}
|
||||
@@ -1,358 +0,0 @@
|
||||
angular = require 'angular'
|
||||
forEach = require 'lodash.foreach'
|
||||
intersection = require 'lodash.intersection'
|
||||
map = require 'lodash.map'
|
||||
omit = require 'lodash.omit'
|
||||
sum = require 'lodash.sum'
|
||||
throttle = require 'lodash.throttle'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.host', [
|
||||
require 'angular-file-upload'
|
||||
require 'angular-ui-router'
|
||||
]
|
||||
.config ($stateProvider) ->
|
||||
$stateProvider.state 'hosts_view',
|
||||
url: '/hosts/:id'
|
||||
controller: 'HostCtrl'
|
||||
template: require './view'
|
||||
.controller 'HostCtrl', (
|
||||
$scope, $stateParams, $http
|
||||
$upload
|
||||
$window
|
||||
$timeout
|
||||
dateFilter
|
||||
xoApi, xo, modal, notify, bytesToSizeFilter
|
||||
) ->
|
||||
do (
|
||||
hostId = $stateParams.id
|
||||
controllers = xoApi.getIndex('vmControllersByContainer')
|
||||
poolPatches = xoApi.getIndex('poolPatchesByPool')
|
||||
srs = xoApi.getIndex('srsByContainer')
|
||||
tasks = xoApi.getIndex('runningTasksByHost')
|
||||
vms = xoApi.getIndex('vmsByContainer')
|
||||
) ->
|
||||
Object.defineProperties($scope, {
|
||||
controller: {
|
||||
get: () => controllers[hostId]
|
||||
},
|
||||
poolPatches: {
|
||||
get: () => $scope.host && poolPatches[$scope.host.$poolId]
|
||||
},
|
||||
sharedSrs: {
|
||||
get: () => $scope.host && srs[$scope.host.$poolId]
|
||||
},
|
||||
srs: {
|
||||
get: () => srs[hostId]
|
||||
},
|
||||
tasks: {
|
||||
get: () => tasks[hostId]
|
||||
},
|
||||
vms: {
|
||||
get: () => vms[hostId]
|
||||
}
|
||||
})
|
||||
|
||||
$window.bytesToSize = bytesToSizeFilter # FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
host = null
|
||||
|
||||
$scope.currentPatchPage = 1
|
||||
$scope.currentLogPage = 1
|
||||
$scope.currentPCIPage = 1
|
||||
$scope.currentGPUPage = 1
|
||||
|
||||
$scope.refreshStatControl = refreshStatControl = {
|
||||
baseStatInterval: 5000
|
||||
baseTimeOut: 10000
|
||||
period: null
|
||||
running: false
|
||||
attempt: 0
|
||||
|
||||
start: () ->
|
||||
return if this.running
|
||||
this.stop()
|
||||
this.running = true
|
||||
this._reset()
|
||||
$scope.$on('$destroy', () => this.stop())
|
||||
return this._trig(Date.now())
|
||||
_trig: (t1) ->
|
||||
if this.running
|
||||
timeoutSecurity = $timeout(
|
||||
() => this.stop(),
|
||||
this.baseTimeOut
|
||||
)
|
||||
return $scope.refreshStats($scope.host.id)
|
||||
.then () => this._reset()
|
||||
.catch (err) =>
|
||||
if !this.running || this.attempt >= 2 || $scope.host.power_state isnt 'Running' || $scope.isVMWorking($scope.host)
|
||||
return this.stop()
|
||||
else
|
||||
this.attempt++
|
||||
.finally () =>
|
||||
$timeout.cancel(timeoutSecurity)
|
||||
if this.running
|
||||
t2 = Date.now()
|
||||
return this.period = $timeout(
|
||||
() => this._trig(t2),
|
||||
Math.max(this.baseStatInterval - (t2 - t1), 0)
|
||||
)
|
||||
_reset: () ->
|
||||
this.attempt = 0
|
||||
stop: () ->
|
||||
if this.period
|
||||
$timeout.cancel(this.period)
|
||||
this.running = false
|
||||
return
|
||||
}
|
||||
$scope.$watch(
|
||||
-> xoApi.get $stateParams.id
|
||||
(host) ->
|
||||
$scope.host = host
|
||||
return unless host?
|
||||
|
||||
pool = $scope.pool = xoApi.get host.$poolId
|
||||
|
||||
SRsToPBDs = $scope.SRsToPBDs = Object.create null
|
||||
for PBD in host.$PBDs
|
||||
PBD = xoApi.get PBD
|
||||
|
||||
# If this PBD is unknown, just skips it.
|
||||
continue unless PBD
|
||||
|
||||
SRsToPBDs[PBD.SR] = PBD
|
||||
$scope.listMissingPatches($scope.host.id)
|
||||
|
||||
if host.power_state is 'Running'
|
||||
refreshStatControl.start()
|
||||
else
|
||||
refreshStatControl.stop()
|
||||
)
|
||||
|
||||
$scope.$watch('vms', (vms) =>
|
||||
$scope.vCPUs = sum(vms, (vm) => vm.CPUs.number)
|
||||
)
|
||||
|
||||
$scope.cancelTask = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Cancel task'
|
||||
message: 'Are you sure you want to cancel this task?'
|
||||
}).then ->
|
||||
xo.task.cancel id
|
||||
|
||||
$scope.destroyTask = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Destroy task'
|
||||
message: 'Are you sure you want to destroy this task?'
|
||||
}).then ->
|
||||
xo.task.destroy id
|
||||
|
||||
$scope.disconnectPBD = xo.pbd.disconnect
|
||||
$scope.removePBD = xo.pbd.delete
|
||||
|
||||
$scope.new_sr = xo.pool.new_sr
|
||||
|
||||
$scope.pool_addHost = (id) ->
|
||||
xo.host.attach id
|
||||
|
||||
$scope.pool_removeHost = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Remove host from pool'
|
||||
message: 'Are you sure you want to detach this host from its pool? It will be automatically rebooted'
|
||||
}).then ->
|
||||
xo.host.detach id
|
||||
$scope.rebootHost = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Reboot host'
|
||||
message: 'Are you sure you want to reboot this host? It will be disabled then rebooted'
|
||||
}).then ->
|
||||
xo.host.restart id
|
||||
|
||||
$scope.enableHost = (id) ->
|
||||
xo.host.enable id
|
||||
notify.info {
|
||||
title: 'Host action'
|
||||
message: 'Host is enabled'
|
||||
}
|
||||
|
||||
$scope.disableHost = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Disable host'
|
||||
message: 'Are you sure you want to disable this host? In disabled state, no new VMs can be started and currently active VMs on the host continue to execute.'
|
||||
}).then ->
|
||||
xo.host.disable id
|
||||
.then ->
|
||||
notify.info {
|
||||
title: 'Host action'
|
||||
message: 'Host is disabled'
|
||||
}
|
||||
|
||||
$scope.restartToolStack = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Restart XAPI'
|
||||
message: 'Are you sure you want to restart the XAPI toolstack?'
|
||||
}).then ->
|
||||
xo.host.restartToolStack id
|
||||
|
||||
$scope.shutdownHost = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Shutdown host'
|
||||
message: 'Are you sure you want to shutdown this host?'
|
||||
}).then ->
|
||||
xo.host.stop id
|
||||
|
||||
$scope.saveHost = ($data) ->
|
||||
{host} = $scope
|
||||
{name_label, name_description, enabled} = $data
|
||||
|
||||
$data = {
|
||||
id: host.id
|
||||
}
|
||||
if name_label isnt host.name_label
|
||||
$data.name_label = name_label
|
||||
if name_description isnt host.name_description
|
||||
$data.name_description = name_description
|
||||
if enabled isnt host.enabled
|
||||
if host.enabled
|
||||
$scope.disableHost($data.id)
|
||||
else
|
||||
$scope.enableHost($data.id)
|
||||
# enabled is not set via the "set" method, so we remove it before send it
|
||||
delete $data.enabled
|
||||
|
||||
xoApi.call 'host.set', $data
|
||||
|
||||
$scope.deleteAllLog = ->
|
||||
modal.confirm({
|
||||
title: 'Log deletion'
|
||||
message: 'Are you sure you want to delete all the logs?'
|
||||
}).then ->
|
||||
for log in $scope.host.messages
|
||||
console.log "Remove log #{log.id}"
|
||||
xo.log.delete log.id
|
||||
|
||||
$scope.deleteLog = (id) ->
|
||||
console.log "Remove log #{id}"
|
||||
xo.log.delete id
|
||||
|
||||
$scope.connectPBD = (id) ->
|
||||
console.log "Connect PBD #{id}"
|
||||
|
||||
xoApi.call 'pbd.connect', {id: id}
|
||||
|
||||
$scope.disconnectPBD = (id) ->
|
||||
console.log "Disconnect PBD #{id}"
|
||||
|
||||
xoApi.call 'pbd.disconnect', {id: id}
|
||||
|
||||
$scope.removePBD = (id) ->
|
||||
console.log "Remove PBD #{id}"
|
||||
|
||||
xoApi.call 'pbd.delete', {id: id}
|
||||
|
||||
$scope.connectPIF = (id) ->
|
||||
console.log "Connect PIF #{id}"
|
||||
|
||||
xoApi.call 'pif.connect', {id: id}
|
||||
|
||||
$scope.disconnectPIF = (id) ->
|
||||
console.log "Disconnect PIF #{id}"
|
||||
|
||||
xoApi.call 'pif.disconnect', {id: id}
|
||||
|
||||
$scope.removePIF = (id) ->
|
||||
console.log "Remove PIF #{id}"
|
||||
|
||||
xoApi.call 'pif.delete', {id: id}
|
||||
|
||||
$scope.importVm = ($files, id) ->
|
||||
file = $files[0]
|
||||
notify.info {
|
||||
title: 'VM import started'
|
||||
message: "Starting the VM import"
|
||||
}
|
||||
|
||||
xo.vm.import id
|
||||
.then ({ $sendTo: url }) ->
|
||||
return $upload.http {
|
||||
method: 'POST'
|
||||
url
|
||||
data: file
|
||||
}
|
||||
.then (result) ->
|
||||
throw result.status if result.status isnt 200
|
||||
notify.info
|
||||
title: 'VM import'
|
||||
message: 'Success'
|
||||
|
||||
$scope.createNetwork = (name, description, pif, mtu, vlan) ->
|
||||
|
||||
$scope.createNetworkWaiting = true # disables form fields
|
||||
notify.info {
|
||||
title: 'Network creation...'
|
||||
message: 'Creating the network'
|
||||
}
|
||||
|
||||
params = {
|
||||
host: $scope.host.id
|
||||
name,
|
||||
}
|
||||
|
||||
if mtu then params.mtu = mtu
|
||||
if pif then params.pif = pif
|
||||
if vlan then params.vlan = vlan
|
||||
if description then params.description = description
|
||||
|
||||
xoApi.call 'host.createNetwork', params
|
||||
.then ->
|
||||
$scope.creatingNetwork = false
|
||||
$scope.createNetworkWaiting = false
|
||||
|
||||
$scope.isPoolPatchApplied = (patch) ->
|
||||
return true if patch.applied
|
||||
hostPatch = intersection(patch.$host_patches, $scope.host.patches)
|
||||
return false if not hostPatch.length
|
||||
hostPatch = xoApi.get(hostPatch[0])
|
||||
return hostPatch.applied
|
||||
|
||||
$scope.listMissingPatches = (id) ->
|
||||
return xo.host.listMissingPatches id
|
||||
.then (result) ->
|
||||
$scope.updates = omit(result,map($scope.poolPatches,'id'))
|
||||
|
||||
$scope.installPatch = (id, patchUid) ->
|
||||
console.log("Install patch "+patchUid+" on "+id)
|
||||
notify.info {
|
||||
title: 'Patch host'
|
||||
message: "Patching the host, please wait..."
|
||||
}
|
||||
xo.host.installPatch id, patchUid
|
||||
|
||||
$scope.refreshStats = (id) ->
|
||||
return xo.host.refreshStats id
|
||||
|
||||
.then (result) ->
|
||||
result.cpuSeries = []
|
||||
forEach result.cpus, (v,k) ->
|
||||
result.cpuSeries.push 'CPU ' + k
|
||||
return
|
||||
result.pifSeries = []
|
||||
forEach result.pifs, (v,k) ->
|
||||
result.pifSeries.push '#' + Math.floor(k/2) + ' ' + if k % 2 then 'out' else 'in'
|
||||
return
|
||||
forEach result.date, (v,k) ->
|
||||
result.date[k] = new Date(v*1000).toLocaleTimeString()
|
||||
forEach result.memoryUsed, (v, k) ->
|
||||
result.memoryUsed[k] = v*1024
|
||||
forEach result.memory, (v, k) ->
|
||||
result.memory[k] = v*1024
|
||||
$scope.stats = result
|
||||
|
||||
$scope.statView = {
|
||||
cpuOnly: false,
|
||||
ramOnly: false,
|
||||
netOnly: false,
|
||||
loadOnly: false
|
||||
}
|
||||
# A module exports its name.
|
||||
.name
|
||||
@@ -1,522 +0,0 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-host(class="xo-color-{{host.power_state | lowercase}}")
|
||||
| {{host.name_label}}
|
||||
small(ng-if="pool.name_label")
|
||||
| (
|
||||
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
|
||||
| )
|
||||
p.center {{host.bios_strings["system-manufacturer"]}} {{host.bios_strings["system-product-name"]}}
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs(style="color: #e25440;")
|
||||
| General
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="hostSettings.$show()")
|
||||
i.fa.fa-edit.fa-fw
|
||||
.panel-body
|
||||
form(editable-form="", name="hostSettings", onbeforesave="saveHost($data)")
|
||||
dl.dl-horizontal
|
||||
dt Name
|
||||
dd
|
||||
span(editable-text="host.name_label", e-name="name_label", e-form="hostSettings")
|
||||
| {{host.name_label}}
|
||||
dt Description
|
||||
dd
|
||||
span(editable-text="host.name_description", e-name="name_description", e-form="hostSettings")
|
||||
| {{host.name_description}}
|
||||
dt Enabled
|
||||
dd
|
||||
span(editable-select="host.enabled", e-ng-options="ap.v as ap.t for ap in [{v: true, t:'Yes'}, {v: false, t:'No'}]", e-name="enabled", e-form="hostSettings")
|
||||
| {{host.enabled ? 'Yes' : 'No'}}
|
||||
dt Tags
|
||||
dd(ng-if="host.tags.length")
|
||||
span(ng-repeat="tag in host.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
dd(ng-if="!host.tags.length")
|
||||
em No tags.
|
||||
dt CPUs
|
||||
dd {{host.CPUs["cpu_count"]}}x {{host.CPUs["modelname"]}}
|
||||
dt Hostname
|
||||
dd
|
||||
| {{host.hostname}}
|
||||
dt UUID
|
||||
dd {{host.UUID}}
|
||||
dt iQN
|
||||
dd {{host.iSCSI_name}}
|
||||
dt(ng-if="refreshStatControl.running && stats") vCPUs/CPUs:
|
||||
dd(ng-if="refreshStatControl.running && stats") {{vCPUs}}/{{host.CPUs['cpu_count']}}
|
||||
dt(ng-if="refreshStatControl.running && stats") Running VMs:
|
||||
dd(ng-if="refreshStatControl.running && stats") {{vms | count}}
|
||||
dt(ng-if="refreshStatControl.running && stats") RAM (used/free):
|
||||
dd(ng-if="refreshStatControl.running && stats") {{host.memory.usage | bytesToSize}}/{{host.memory.size | bytesToSize}}
|
||||
.btn-form(ng-show="hostSettings.$visible")
|
||||
p.center
|
||||
button.btn.btn-default(type="button", ng-disabled="hostSettings.$waiting", ng-click="hostSettings.$cancel()")
|
||||
i.fa.fa-times
|
||||
| Cancel
|
||||
|
|
||||
button.btn.btn-primary(type="submit", ng-disabled="hostSettings.$waiting")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-stats(style="color: #e25440;")
|
||||
| Stats
|
||||
.panel-body(ng-if="refreshStatControl.running && stats")
|
||||
div(ng-if="statView.cpuOnly", ng-click="statView.cpuOnly = false")
|
||||
p.stat-name
|
||||
i.fa.fa-tachometer
|
||||
| CPU usage
|
||||
canvas.chart.chart-line.chart-stat-full(
|
||||
id="bigCpu"
|
||||
data="stats.cpus"
|
||||
labels="stats.date"
|
||||
series="stats.cpuSeries"
|
||||
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
|
||||
legend="true"
|
||||
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= Math.round(10*value)/10 %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= Math.round(10*value)/10 %>", pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="statView.ramOnly", ng-click="statView.ramOnly = false")
|
||||
p.stat-name
|
||||
//- i.fa.fa-bar-chart
|
||||
i.fa.fa-tasks
|
||||
//- i.fa.fa-server
|
||||
| RAM usage
|
||||
canvas.chart.chart-line.chart-stat-full(
|
||||
id="bigRam"
|
||||
data="[stats.memoryUsed,stats.memory]"
|
||||
labels="stats.date"
|
||||
series="['Used RAM', 'Total RAM']"
|
||||
colours="['#ff0000', '#ffbbbb']"
|
||||
legend="true"
|
||||
options=' {responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="statView.netOnly", ng-click="statView.netOnly = false")
|
||||
p.stat-name
|
||||
i.fa.fa-sitemap
|
||||
| Network I/O
|
||||
canvas.chart.chart-line.chart-stat-full(
|
||||
id="bigNet"
|
||||
data="stats.pifs"
|
||||
labels="stats.date"
|
||||
series="stats.pifSeries"
|
||||
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
|
||||
legend="true"
|
||||
options=' {responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="statView.loadOnly", ng-click="statView.loadOnly = false")
|
||||
p.stat-name
|
||||
i.fa.fa-cogs
|
||||
| Load Average
|
||||
canvas.chart.chart-line.chart-stat-full(
|
||||
id="bigLoad"
|
||||
data="[stats.load]"
|
||||
labels="stats.date"
|
||||
series="['Load']"
|
||||
colours="['#960094']"
|
||||
legend="true"
|
||||
options=' {responsive: true, maintainAspectRatio: false, multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="!statView.netOnly && !statView.loadOnly && !statView.cpuOnly && !statView.ramOnly")
|
||||
.row
|
||||
.col-sm-6(ng-click="statView.cpuOnly=true")
|
||||
p.stat-name
|
||||
i.fa.fa-tachometer
|
||||
| CPU usage
|
||||
canvas.chart.chart-line.chart-stat-preview(
|
||||
id="smallCpu"
|
||||
data="stats.cpus"
|
||||
labels="stats.date"
|
||||
series="stats.cpuSeries"
|
||||
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
|
||||
options='{responsive: true, maintainAspectRatio: false, showTooltips: false, pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1}'
|
||||
)
|
||||
.col-sm-6(ng-click="statView.ramOnly=true")
|
||||
p.stat-name
|
||||
//- i.fa.fa-bar-chart
|
||||
i.fa.fa-tasks
|
||||
//- i.fa.fa-server
|
||||
| RAM usage
|
||||
canvas.chart.chart-line.chart-stat-preview(
|
||||
id="smallRam"
|
||||
data="[stats.memoryUsed,stats.memory]"
|
||||
labels="stats.date"
|
||||
series="['Used RAM', 'Total RAM']"
|
||||
colours="['#ff0000', '#ffbbbb']"
|
||||
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
|
||||
)
|
||||
.row
|
||||
.col-sm-6(ng-click="statView.netOnly=true")
|
||||
p.stat-name
|
||||
i.fa.fa-sitemap
|
||||
| Network I/O
|
||||
canvas.chart.chart-line.chart-stat-preview(
|
||||
id="smallNet"
|
||||
data="stats.pifs"
|
||||
labels="stats.date"
|
||||
series="stats.pifSeries"
|
||||
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
|
||||
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
|
||||
)
|
||||
.col-sm-6(ng-click="statView.loadOnly=true")
|
||||
p.stat-name
|
||||
i.fa.fa-cogs
|
||||
| Load Average
|
||||
canvas.chart.chart-line.chart-stat-preview(
|
||||
id="smallDisk"
|
||||
data="[stats.load]"
|
||||
labels="stats.date"
|
||||
series="['Load']"
|
||||
colours="['#960094']"
|
||||
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
|
||||
)
|
||||
.panel-body(ng-if="!refreshStatControl.running || !stats")
|
||||
.row
|
||||
.col-sm-6.col-lg-4
|
||||
p.stat-name CPU usage:
|
||||
p.center.mid-stat {{vCPUs}}/{{host.CPUs['cpu_count']}}
|
||||
.col-sm-6.col-lg-4
|
||||
p.stat-name RAM used:
|
||||
p.center.mid-stat {{host.memory.usage | bytesToSize}}
|
||||
.col-sm-4.visible-lg
|
||||
p.stat-name Running VMs:
|
||||
p.center.mid-stat {{vms | count}}
|
||||
.row
|
||||
p.center(ng-if="refreshStatControl.running")
|
||||
i.fa.fa-circle-o-notch.fa-spin.fa-2x
|
||||
| Fetching stats...
|
||||
.row.hidden-lg
|
||||
.col-sm-12
|
||||
br
|
||||
p.stat-name {{vms | count}} running VMs
|
||||
//- Action panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flash(style="color: #e25440;")
|
||||
| Actions
|
||||
.panel-body.text-center
|
||||
.grid
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add SR", type="button", style="width: 90%", xo-sref="SRs_new({container: host.id})")
|
||||
i.xo-icon-sr.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add VM", type="button", style="width: 90%", xo-sref="VMs_new({container: host.id})")
|
||||
i.xo-icon-vm.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Reboot host", type="button", style="width: 90%", xo-click="rebootHost(host.id)")
|
||||
i.fa.fa-refresh.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Shutdown host", type="button", style="width: 90%", xo-click="shutdownHost(host.id)")
|
||||
i.fa.fa-power-off.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="host.enabled")
|
||||
button.btn(tooltip="Disable host", type="button", style="width: 90%", xo-click="disableHost(host.id)")
|
||||
i.fa.fa-times-circle.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="!host.enabled")
|
||||
button.btn(tooltip="Enable host", type="button", style="width: 90%", xo-click="enableHost(host.id)")
|
||||
i.fa.fa-check-circle.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Restart toolstack", type="button", style="width: 90%", xo-click="restartToolStack(host.id)")
|
||||
i.fa.fa-retweet.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="pool.name_label")
|
||||
button.btn(tooltip="Remove from pool", style="width: 90%", type="button", xo-click="pool_removeHost(host.id)")
|
||||
i.fa.fa-cloud-upload.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="!pool.name_label")
|
||||
button.btn(tooltip="Add to pool", style="width: 90%", type="button", xo-click="pool_addHost(host.id)")
|
||||
i.fa.fa-cloud-download.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(style="margin-bottom: 0.5em")
|
||||
button.btn(
|
||||
tooltip="Import VM"
|
||||
type="button"
|
||||
style="width: 90%"
|
||||
ng-file-select = 'importVm($files, host.id)'
|
||||
)
|
||||
i.fa.fa-upload.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(style="margin-bottom: 0.5em")
|
||||
button.btn(
|
||||
tooltip="Host console"
|
||||
type="button"
|
||||
style="width: 90%"
|
||||
xo-sref="consoles_view({id: controller.id})"
|
||||
)
|
||||
i.xo-icon-console.fa-2x.fa-fw
|
||||
|
||||
//- TODO: Memory panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-memory(style="color: #e25440;")
|
||||
| Memory
|
||||
.panel-body.text-center
|
||||
.progress
|
||||
.progress-bar-host(role="progressbar", aria-valuemin="0", aria-valuenow="{{controller.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[controller.memory.size, host.memory.size] | percentage}}", tooltip="{{host.name_label}}: {{[controller.memory.size, host.memory.size] | percentage}}")
|
||||
small {{host.name_label}}
|
||||
.progress-bar.progress-bar-vm(ng-repeat="VM in vms | map | orderBy:natural('name_label') track by VM.id", role="progressbar", aria-valuemin="0", aria-valuenow="{{VM.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[VM.memory.size, host.memory.size] | percentage}}", xo-sref="VMs_view({id: VM.id})", tooltip="{{VM.name_label}}: {{[VM.memory.size, host.memory.size] | percentage}}")
|
||||
small {{VM.name_label}}
|
||||
ul.list-inline.text-center
|
||||
li Total: {{host.memory.size | bytesToSize}}
|
||||
li Currently used: {{host.memory.usage | bytesToSize}}
|
||||
li Available: {{host.memory.size-host.memory.usage | bytesToSize}}
|
||||
//- SR panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-sr(style="color: #e25440;")
|
||||
| Storage
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th Name
|
||||
th Format
|
||||
th Size
|
||||
th Physical/Allocated usage
|
||||
th Type
|
||||
th Status
|
||||
//- TODO: display PBD status for each SR of this host (connected or not)
|
||||
//- Shared SR
|
||||
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in sharedSrs | map | orderBy:natural('name_label') track by SR.id")
|
||||
td.oneliner
|
||||
| {{SR.name_label}}
|
||||
td {{SR.SR_type}}
|
||||
td {{SR.size | bytesToSize}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
|
||||
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
|
||||
td
|
||||
span.label.label-primary Shared
|
||||
td(ng-if="SRsToPBDs[SR.id].attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)")
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!SRsToPBDs[SR.id].attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)")
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
|
||||
i.fa.fa-ban.fa-lg
|
||||
//- Local SR
|
||||
//- TODO: migrate to SRs and not PBDs when implemented in xo-server spec
|
||||
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in srs | map | orderBy:natural('name_label') track by SR.id")
|
||||
td
|
||||
| {{SR.name_label}}
|
||||
td {{SR.SR_type}}
|
||||
td {{SR.size | bytesToSize}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
|
||||
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
|
||||
td
|
||||
span.label.label-info Local
|
||||
td(ng-if="SRsToPBDs[SR.id].attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)")
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!SRsToPBDs[SR.id].attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)")
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
|
||||
i.fa.fa-ban.fa-lg
|
||||
//- Networks/Interfaces panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-network(style="color: #e25440;")
|
||||
| Interfaces
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
th.col-md-1 Device
|
||||
th.col-md-1 VLAN
|
||||
th.col-md-1 Address
|
||||
th.col-md-2 MAC
|
||||
th.col-md-1 MTU
|
||||
th.col-md-1 Link status
|
||||
tr(ng-repeat="PIF in host.$PIFs | resolve | orderBy:natural('name_label') track by PIF.id")
|
||||
td
|
||||
| {{PIF.device}}
|
||||
span.label.label-primary(ng-if="PIF.management") XAPI
|
||||
td
|
||||
span(ng-if="PIF.vlan > -1")
|
||||
| {{PIF.vlan}}
|
||||
span(ng-if="PIF.vlan == -1")
|
||||
| -
|
||||
td.oneliner {{PIF.IP}} ({{PIF.mode}})
|
||||
td.oneliner {{PIF.MAC}}
|
||||
td {{PIF.MTU}}
|
||||
td(ng-if="PIF.attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this interface", xo-click="disconnectPIF(PIF.id)")
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!PIF.attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Connect this interface", xo-click="connectPIF(PIF.id)")
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)")
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
.text-right
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingNetwork, "btn-primary": !creatingNetwork}', ng-click="creatingNetwork = !creatingNetwork")
|
||||
i.fa.fa-plus(ng-if = '!creatingNetwork')
|
||||
i.fa.fa-minus(ng-if = 'creatingNetwork')
|
||||
| Create Network
|
||||
br
|
||||
form.form-inline.text-right#createNetworkForm(ng-if = 'creatingNetwork', name = 'createNetworkForm', ng-submit = 'createNetwork(newNetworkName, newNetworkDescription, newNetworkPIF, newNetworkMTU, newNetworkVlan)')
|
||||
fieldset(ng-attr-disabled = '{{ createNetworkWaiting ? true : undefined }}')
|
||||
.form-group
|
||||
label(for = 'newNetworkPIF') Interface
|
||||
select.form-control(ng-model = 'newNetworkPIF', ng-change = 'updateMTU(newNetworkPIF)', ng-options='(PIF | resolve).device for PIF in host.$PIFs')
|
||||
option(value = '', disabled) None
|
||||
|
|
||||
.form-group
|
||||
label.control-label(for = 'newNetworkName') Name
|
||||
input#newNetworkName.form-control(type = 'text', ng-model = 'newNetworkName', required)
|
||||
|
|
||||
.form-group
|
||||
label.control-label(for = 'newNetworkDescription') Description
|
||||
input#newNetworkDescription.form-control(type = 'text', ng-model = 'newNetworkDescription', placeholder= 'Network created with Xen Orchestra')
|
||||
|
|
||||
.form-group
|
||||
label.control-label(for = 'newNetworkVlan') VLAN
|
||||
input#newNetworkVlan.form-control(type = 'text', ng-model = 'newNetworkVlan', placeholder = 'Defaut: no VLAN')
|
||||
|
|
||||
.form-group
|
||||
label(for = 'newNetworkMTU') MTU
|
||||
input#newNetworkMTU.form-control(type = 'text', ng-model = 'newNetworkMTU', placeholder = 'Default: 1500')
|
||||
|
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
i.fa.fa-plus-square
|
||||
| Create
|
||||
span(ng-if = 'createNetworkWaiting')
|
||||
|
|
||||
i.fa.fa-spin.fa-circle-o-notch
|
||||
br
|
||||
//- CPU and Logs panels
|
||||
.grid
|
||||
//- Task panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title(ng-if="tasks | isNotEmpty")
|
||||
i.fa.fa-spinner.fa-pulse(style="color: #e25440;")
|
||||
| Pending tasks
|
||||
.panel-heading.panel-title(ng-if="tasks | isEmpty")
|
||||
i.fa.fa-spinner(style="color: #e25440;")
|
||||
| Pending tasks
|
||||
.panel-body
|
||||
p.center(ng-if="tasks | isEmpty") No recent tasks
|
||||
table.table.table-hover(ng-if="tasks | isNotEmpty")
|
||||
th Date
|
||||
th Progress
|
||||
th Name
|
||||
//- TODO: working reverse order, from recent to oldest
|
||||
tr(ng-repeat="task in tasks | map | orderBy:'created':true track by task.id")
|
||||
td.oneliner {{task.created * 1e3 | date:'medium'}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar.progress-bar-success.progress-bar-striped.active.progress-bar-black(role="progressbar", aria-valuemin="0", aria-valuenow="{{task.progress*100}}", aria-valuemax="100", style="width: {{task.progress*100}}%", tooltip="Progress: {{task.progress*100 | number:1}}%")
|
||||
| {{task.progress*100 | number:1}}%
|
||||
td.oneliner
|
||||
| {{task.name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="cancelTask(task.id)")
|
||||
i.fa.fa-times.fa-lg(tooltip="Cancel this task")
|
||||
a(xo-click="destroyTask(task.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this task")
|
||||
|
||||
|
||||
//- Logs panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments(style="color: #e25440;")
|
||||
| Logs
|
||||
span.quick-edit(ng-if="host.messages | isNotEmpty", tooltip="Remove all logs", ng-click="deleteAllLog()")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="host.messages | isEmpty") No recent logs
|
||||
table.table.table-hover(ng-if="host.messages | isNotEmpty")
|
||||
th Date
|
||||
th Name
|
||||
tr(ng-repeat="message in host.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="deleteLog(message.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
|
||||
.center(ng-if = '(host.messages | count) > 5')
|
||||
pagination(boundary-links="true", total-items="host.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.grid
|
||||
//- Patches panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-file-code-o(style="color: #e25440;")
|
||||
| Patches
|
||||
span.quick-edit(ng-click="listMissingPatches(host.id)", tooltip="Check for updates")
|
||||
i.fa.fa-question-circle
|
||||
.panel-body
|
||||
table.table.table-hover(ng-if="poolPatches || updates")
|
||||
th.col-sm-2 Name
|
||||
th.col-sm-5 Description
|
||||
th.col-sm-3 Applied/Released date
|
||||
th.col-sm-1 Size
|
||||
th.col-sm-1 Status
|
||||
tr(ng-repeat="patch in updates")
|
||||
td.oneliner {{patch.name}}
|
||||
td.oneliner
|
||||
a(href="{{patch.documentationUrl}}", target="_blank") {{patch.description}}
|
||||
td.oneliner {{patch.date | date:"medium"}}
|
||||
td -
|
||||
td
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to install the patch on this host")
|
||||
span.label.label-danger Missing
|
||||
tr(ng-repeat="patch in poolPatches | map | slice:(5*(currentPatchPage-1)):(5*currentPatchPage)")
|
||||
td.oneliner {{patch.name_label}}
|
||||
td.oneliner {{patch.name_description}}
|
||||
//- TODO: use a proper function for patch date, like poolPatchToHostPatch
|
||||
td.oneliner {{((patch.$host_patches[0]) | resolve).time*1e3 | date:"medium"}}
|
||||
td {{patch.size | bytesToSize}}
|
||||
td
|
||||
span(ng-if="isPoolPatchApplied(patch)")
|
||||
span.label.label-success Applied
|
||||
span(ng-click="installPatch(host.id, patch.id)", ng-if="!isPoolPatchApplied(patch)", tooltip="Click to apply the patch on this host")
|
||||
span.label.label-warning Not applied
|
||||
//- span.pull-right.btn-group.quick-buttons
|
||||
//- a(xo-click="deletePatch(patch.id)")
|
||||
//- i.fa.fa-trash-o.fa-lg(tooltip="Remove this patch")
|
||||
.center(ng-if = '(poolPatches | count) > 5')
|
||||
pagination(boundary-links="true", total-items="poolPatches | count", ng-model="$parent.currentPatchPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-plug(style="color: #e25440;")
|
||||
| PCI Devices
|
||||
.panel-body
|
||||
p.center(ng-if="!host.$PCIs") No PCI devices available
|
||||
table.table.table-hover(ng-if="host.$PCIs")
|
||||
th PCI Info
|
||||
th Device Name
|
||||
tr(ng-repeat="pci in host.$PCIs | resolve | orderBy:'pci_id' | slice:(5*(currentPCIPage-1)):(5*currentPCIPage) track by pci.id")
|
||||
td.oneliner {{pci.pci_id}} ({{pci.class_name}})
|
||||
td.oneliner {{pci.device_name}}
|
||||
.center(ng-if = '(host.$PCIs | resolve).length > 5')
|
||||
pagination(boundary-links="true", total-items="(host.$PCIs | resolve).length", ng-model="$parent.currentPCIPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-desktop(style="color: #e25440;")
|
||||
| GPUs
|
||||
.panel-body
|
||||
p.center(ng-if="host.$PGPUs.length === 0") No GPUs available
|
||||
table.table.table-hover(ng-if="host.$PGPUs.length !== 0")
|
||||
th Device
|
||||
tr(ng-repeat="pgpu in host.$PGPUs | resolve | orderBy:'device' | slice:(5*(currentGPUPage-1)):(5*currentGPUPage) track by pgpu.id")
|
||||
td.oneliner {{pgpu.device}}
|
||||
.center(ng-if = '(host.$PGPUs | resolve).length > 5')
|
||||
pagination(boundary-links="true", total-items="(host.$PGPUs | resolve).length", ng-model="$parent.currentGPUPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
@@ -1,28 +0,0 @@
|
||||
angular = require 'angular'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.isoDevice', []
|
||||
|
||||
.directive 'isoDevice', -> {
|
||||
restrict: 'E'
|
||||
template: require './view'
|
||||
scope: {
|
||||
isos: '='
|
||||
vm: '='
|
||||
}
|
||||
controller: 'IsoDevice as isoDevice'
|
||||
bindToController: true
|
||||
}
|
||||
|
||||
.controller 'IsoDevice', (xo) ->
|
||||
this.eject = (VM) ->
|
||||
xo.vm.ejectCd VM.id
|
||||
|
||||
this.insert = (VM, disc_id) ->
|
||||
xo.vm.insertCd VM.id, disc_id, true
|
||||
|
||||
return
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
@@ -1,15 +0,0 @@
|
||||
.form-inline
|
||||
.form-group
|
||||
.input-group
|
||||
select.form-control(
|
||||
ng-model = 'isoDevice.isos.mounted'
|
||||
ng-change = 'isoDevice.insert(isoDevice.vm, isoDevice.isos.mounted)'
|
||||
ng-options = 'iso.iso.id as iso.label group by iso.sr for iso in isoDevice.isos.opts'
|
||||
)
|
||||
option(value = '', disabled) -- CD Drive (empty) --
|
||||
.input-group-btn
|
||||
button.btn.btn-default(
|
||||
ng-click = 'isoDevice.eject(isoDevice.vm)'
|
||||
ng-disabled = '!isoDevice.isos.mounted'
|
||||
)
|
||||
i.fa.fa-eject
|
||||
@@ -1,33 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.list', [
|
||||
uiRouter,
|
||||
xoApi
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('list', {
|
||||
url: '/list',
|
||||
controller: 'ListCtrl as list',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('ListCtrl', function (xoApi) {
|
||||
this.hosts = xoApi.getView('host')
|
||||
this.pools = xoApi.getView('pool')
|
||||
this.SRs = xoApi.getView('SR')
|
||||
this.VMs = xoApi.getView('VM')
|
||||
|
||||
this.hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
this.runningHostsByPool = xoApi.getIndex('runningHostsByPool')
|
||||
this.vmsByContainer = xoApi.getIndex('vmsByContainer')
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
@@ -1,164 +0,0 @@
|
||||
//- TODO: print a message when no entries.
|
||||
|
||||
//- If it's a (named) pool.
|
||||
.grid.flat-object(ng-repeat="pool in list.pools.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by pool.id", ng-if="pool.name_label", xo-sref="pools_view({id: pool.id})")
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-pool
|
||||
//- Properties & tags.
|
||||
.grid-cell
|
||||
//- Properties.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid
|
||||
.grid-cell.flat-cell.flat-cell-name
|
||||
| {{pool.name_label}}
|
||||
.grid-cell.flat-cell.flat-cell-description
|
||||
i {{pool.name_description}}
|
||||
.grid-cell.flat-cell(ng-init="default_SR = (pool.default_SR | resolve)")
|
||||
div(ng-if="default_SR")
|
||||
| Default SR:
|
||||
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
|
||||
div(ng-if="!default_SR")
|
||||
em No default SR.
|
||||
.grid-cell.flat-cell(ng-init="master = (pool.master | resolve)")
|
||||
div(ng-if="master")
|
||||
| Master:
|
||||
a(ui-sref="hosts_view({id: master.id})") {{master.name_label}}
|
||||
div(ng-if="!master")
|
||||
em Unknown master.
|
||||
.grid-cell.flat-cell
|
||||
div(ng-if="pool.HA_enabled")
|
||||
| HA enabled
|
||||
div(ng-if="!pool.HA_enabled")
|
||||
| HA disabled
|
||||
.grid-cell.flat-cell
|
||||
| {{list.runningHostsByPool[pool.id] | count}}/{{list.hostsByPool[pool.id] | count}} hosts
|
||||
//- /Properties.
|
||||
//- Tags.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid-cell.flat-cell-tag
|
||||
i.fa.fa-tag
|
||||
span(ng-repeat="tag in pool.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
//- /Tags.
|
||||
//- /Properties & tags.
|
||||
//- /Pool.
|
||||
//- If it's a host.
|
||||
.grid.flat-object(ng-repeat="host in list.hosts.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by host.id", xo-sref="hosts_view({id: host.id})")
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-host(class="xo-color-{{host.power_state | lowercase}}")
|
||||
//- Properties & tags.
|
||||
.grid-cell
|
||||
//- Properties.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid
|
||||
.grid-cell.flat-cell.flat-cell-name
|
||||
| {{host.name_label}}
|
||||
.grid-cell.flat-cell.flat-cell-description
|
||||
i {{host.name_description}}
|
||||
.grid-cell.flat-cell
|
||||
| Address: {{host.address}}
|
||||
//- .grid-cell.flat-cell
|
||||
//- | {{host.$vCPUs}} vCPUs used on {{host.CPUs["cpu_count"]}} cores
|
||||
.grid-cell.flat-cell
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{100*host.memory.usage/host.memory.size}}", aria-valuemax="100", style="width: {{[host.memory.usage, host.memory.size] | percentage}}", tooltip="RAM: {{[host.memory.usage, host.memory.size] | percentage}} allocated")
|
||||
| {{[host.memory.usage, host.memory.size] | percentage}}
|
||||
.grid-cell.flat-cell
|
||||
| {{list.vmsByContainer[host.id] | count}} VMs running
|
||||
//- /Properties.
|
||||
//- Tags.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid-cell.flat-cell-tag
|
||||
i.fa.fa-tag
|
||||
span(ng-repeat="tag in host.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
//- /Tags.
|
||||
//- /Properties & tags.
|
||||
//- /Host.
|
||||
//- If it's a VM.
|
||||
.grid.flat-object(ng-repeat="VM in list.VMs.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by VM.id", xo-sref="VMs_view({id: VM.id})")
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-vm(class="xo-color-{{VM.power_state | lowercase}}")
|
||||
//- Properties & tags.
|
||||
.grid-cell
|
||||
//- Properties.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid
|
||||
.grid-cell.flat-cell.flat-cell-name
|
||||
| {{VM.name_label}}
|
||||
.grid-cell.flat-cell.flat-cell-description
|
||||
i {{VM.name_description}}
|
||||
.grid-cell.flat-cell
|
||||
| Address: {{VM.addresses["0/ip"]}}
|
||||
.grid-cell.flat-cell
|
||||
| {{VM.CPUs.number}} vCPUs
|
||||
.grid-cell.flat-cell
|
||||
| {{VM.memory.size | bytesToSize}} RAM
|
||||
.grid-cell.flat-cell(ng-init="container = (VM.$container | resolve)")
|
||||
div(ng-if="'pool' === container.type")
|
||||
| Resident on:
|
||||
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
|
||||
div(ng-if="'host' === container.type", ng-init="pool = (container.$poolId | resolve)")
|
||||
| Resident on:
|
||||
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
|
||||
small(ng-if="pool.name_label")
|
||||
| (
|
||||
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
|
||||
| )
|
||||
//- /Properties.
|
||||
//- Tags.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid-cell.flat-cell-tag
|
||||
i.fa.fa-tag
|
||||
span(ng-repeat="tag in VM.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
//- /Tags.
|
||||
//- /Properties & tags.
|
||||
//- /VM.
|
||||
//- If it's a SR.
|
||||
.grid.flat-object(ng-repeat="SR in list.SRs.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by SR.id", xo-sref="SRs_view({id: SR.id})")
|
||||
//- Icon.
|
||||
.grid-cell.flat-cell.flat-cell-type
|
||||
i.xo-icon-sr
|
||||
//- Properties & tags.
|
||||
.grid-cell
|
||||
//- Properties.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid
|
||||
.grid-cell.flat-cell.flat-cell-name
|
||||
| {{SR.name_label}}
|
||||
.grid-cell.flat-cell.flat-cell-description
|
||||
i {{SR.name_description}}
|
||||
.grid-cell.flat-cell
|
||||
| Usage: {{[SR.usage, SR.size] | percentage}} ({{SR.usage | bytesToSize}}/{{SR.size | bytesToSize}})
|
||||
.grid-cell.flat-cell
|
||||
| Type: {{SR.SR_type}}
|
||||
.grid-cell.flat-cell(ng-init="container = (SR.$container | resolve)")
|
||||
div(ng-if="'pool' === container.type")
|
||||
strong
|
||||
| Shared on
|
||||
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
|
||||
div(ng-if="'host' === container.type")
|
||||
| Connected to
|
||||
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
|
||||
//- /Properties.
|
||||
//- Tags.
|
||||
.grid
|
||||
.grid-cell
|
||||
.grid-cell.flat-cell-tag
|
||||
i.fa.fa-tag
|
||||
span(ng-repeat="tag in SR.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
//- /Tags.
|
||||
//- /Properties & tags.
|
||||
//- /SR.
|
||||
@@ -1,65 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.login', [
|
||||
uiRouter
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('login', {
|
||||
url: '/login',
|
||||
controller: 'LoginCtrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('LoginCtrl', function ($scope, $state, $rootScope, xoApi, notify) {
|
||||
const {toState, toStateParams} = (function (login) {
|
||||
if (login) {
|
||||
delete $rootScope._login
|
||||
|
||||
return {
|
||||
toState: login.state.name,
|
||||
toStateParams: login.stateParams
|
||||
}
|
||||
}
|
||||
|
||||
return { toState: 'index' }
|
||||
})($rootScope._login)
|
||||
|
||||
$scope.$watch(() => xoApi.user, function (user) {
|
||||
// When the user is logged in, go the wanted view, fallbacks on
|
||||
// the index view if necessary.
|
||||
if (user) {
|
||||
$state.go(toState, toStateParams).catch(function () {
|
||||
$state.go('index')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Object.defineProperties($scope, {
|
||||
user: {
|
||||
get () {
|
||||
return xoApi.user
|
||||
}
|
||||
},
|
||||
status: {
|
||||
get () {
|
||||
return xoApi.status
|
||||
}
|
||||
}
|
||||
})
|
||||
$scope.logIn = (...args) => {
|
||||
xoApi.logIn(...args).catch(error => {
|
||||
notify.warning({
|
||||
title: 'Authentication failed',
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
@@ -1,54 +0,0 @@
|
||||
//- Hide the navbar for this view.
|
||||
style.
|
||||
.navbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.container
|
||||
div.row-login
|
||||
div.page-header
|
||||
img(src = 'images/logo_small.png')
|
||||
h2 Xen Orchestra
|
||||
form.form-horizontal(
|
||||
ng-submit = '$broadcast("fixAutofill"); logIn(email, password, true)'
|
||||
)
|
||||
fieldset
|
||||
legend.login: h3 Sign in
|
||||
div.form-group
|
||||
div.col-sm-12
|
||||
.input-group
|
||||
span.input-group-addon: i.fa.fa-user.fa-fw
|
||||
input.form-control.input-sm(
|
||||
name = 'email'
|
||||
type = 'text'
|
||||
placeholder = 'Username'
|
||||
ng-model = 'email'
|
||||
required
|
||||
fix-autofill
|
||||
)
|
||||
div.form-group
|
||||
div.col-sm-12
|
||||
.input-group
|
||||
span.input-group-addon: i.fa.fa-key.fa-fw
|
||||
input.form-control.input-sm(
|
||||
name = 'password'
|
||||
type = 'password'
|
||||
placeholder = 'Password'
|
||||
ng-model = 'password'
|
||||
required
|
||||
fix-autofill
|
||||
)
|
||||
div.form-group
|
||||
div.col-sm-12
|
||||
button.btn.btn-login.btn-block.btn-success(
|
||||
id = 'login'
|
||||
name = 'login'
|
||||
)
|
||||
i.fa.fa-sign-in
|
||||
| Login
|
||||
p.status(ng-if = '"disconnected" === status')
|
||||
i.xo-icon-error.fa-2x(tooltip = 'You are not connected to XO-Server')
|
||||
p.status(ng-if = '"connecting" === status')
|
||||
i.fa.fa-refresh.fa-spin.fa-2x(tooltip = 'Connecting to XO-Server')
|
||||
p.status(ng-if = '"connected" === status')
|
||||
i.xo-icon-success.fa-2x(tooltip = 'You are connected to XO-Server')
|
||||
@@ -1,52 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import updater from '../updater'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.navbar', [
|
||||
uiRouter,
|
||||
|
||||
updater,
|
||||
xoServices
|
||||
])
|
||||
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope, updater) {
|
||||
this.updater = updater
|
||||
// TODO: It would make sense to inject xoApi in the scope.
|
||||
Object.defineProperties(this, {
|
||||
status: {
|
||||
get: () => xoApi.status
|
||||
},
|
||||
user: {
|
||||
get: () => xoApi.user
|
||||
}
|
||||
})
|
||||
this.logIn = xoApi.logIn
|
||||
this.logOut = function () {
|
||||
xoApi.logOut()
|
||||
$state.go('login')
|
||||
}
|
||||
|
||||
// When a searched is entered, we must switch to the list view if
|
||||
// necessary.
|
||||
this.ensureListView = function () {
|
||||
$state.go('list')
|
||||
}
|
||||
|
||||
this.tasks = xoApi.getView('runningTasks')
|
||||
})
|
||||
.directive('navbar', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
controller: 'NavbarCtrl as navbar',
|
||||
template: view,
|
||||
scope: {}
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
@@ -1,132 +0,0 @@
|
||||
nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
//- Brand and toggle get grouped for better mobile display
|
||||
.navbar-header
|
||||
//- Button used to (un)collapse on mobile display.
|
||||
button.navbar-toggle(type="button", ng-init="collapsed = true", ng-click="collapsed = !collapsed")
|
||||
span.sr-only Toggle navigation
|
||||
span.icon-bar
|
||||
span.icon-bar
|
||||
span.icon-bar
|
||||
//- Brand name.
|
||||
a.navbar-brand(ui-sref = 'index')
|
||||
img.navbar-logo(src="images/logo.png")
|
||||
| Xen Orchestra
|
||||
//- All navbar items are collapsed on mobile display.
|
||||
.collapse.navbar-collapse(ng-class="!collapsed && 'in'")
|
||||
//- Search form of the navbar.
|
||||
form.navbar-form.navbar-left(role="search", style="width: 250px")
|
||||
//- Forced width due to issue with `input`s (https://github.com/twbs/bootstrap/issues/9950.
|
||||
.input-group
|
||||
input.form-control.inverse(
|
||||
type = 'text'
|
||||
placeholder = ''
|
||||
ng-model = '$root.listFilter'
|
||||
ng-change = 'navbar.ensureListView()'
|
||||
)
|
||||
span.input-group-btn
|
||||
button.btn.btn-search(
|
||||
type = 'button'
|
||||
ng-click = 'navbar.ensureListView()'
|
||||
)
|
||||
i.fa.fa-search
|
||||
//- /Search form.
|
||||
ul.nav.navbar-nav
|
||||
li
|
||||
a(href="https://xen-orchestra.com/#/pricing?pk_campaign=xoa_source", target="_blank", tooltip="Source version without Pro support. Use in production at your own risk.")
|
||||
i.xo-icon-info.text-danger
|
||||
span.hidden-sm No Pro Support!
|
||||
//- Right items of the navbar.
|
||||
ul.nav.navbar-nav.navbar-right
|
||||
li.navbar-text(ng-if="'disconnected' === navbar.status")
|
||||
i.xo-icon-error
|
||||
| Disconnected from XO-Server
|
||||
li.navbar-text(ng-if="'connecting' === navbar.status")
|
||||
i.fa.fa-refresh.fa-spin
|
||||
| Connecting to XO-Server
|
||||
//- Running tasks
|
||||
li.disabled(ng-if="!navbar.tasks.size", tooltip="No running tasks")
|
||||
a.dropdown-toggle.inverse
|
||||
i.xo-icon-task
|
||||
li.dropdown(dropdown, ng-if="navbar.tasks.size")
|
||||
a.dropdown-toggle.inverse(dropdown-toggle)
|
||||
i.xo-icon-task
|
||||
ul.dropdown-menu.inverse
|
||||
li.task-menu(
|
||||
ng-repeat="task in navbar.tasks.all | orderBy:natural('name_label') track by task.id"
|
||||
)
|
||||
a(
|
||||
ui-sref="hosts_view({id: task.$host})"
|
||||
tooltip = "{{task.name_label}}"
|
||||
)
|
||||
//- i.fa.fa-spinner.fa-fw
|
||||
//- | {{task.name_label}}
|
||||
.progress-condensed
|
||||
.progress-bar.progress-bar-success.progress-bar-striped.active.progress-bar-black(
|
||||
role = "progressbar"
|
||||
aria-valuemin = "0"
|
||||
aria-valuenow = "{{task.progress*100}}"
|
||||
aria-valuemax = "100"
|
||||
style = "width: {{task.progress*100}}%"
|
||||
)
|
||||
| {{task.progress*100 | number:1}}%
|
||||
//- Main menu.
|
||||
li.dropdown(dropdown)
|
||||
a.dropdown-toggle.inverse(dropdown-toggle)
|
||||
i.fa.fa-th
|
||||
ul.dropdown-menu.inverse
|
||||
li(
|
||||
ui-sref-active = 'active'
|
||||
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
|
||||
)
|
||||
a(ui-sref = 'tree')
|
||||
i.fa.fa-indent
|
||||
| Tree view
|
||||
li(ui-sref-active="active")
|
||||
a(ui-sref="list")
|
||||
i.fa.fa-align-justify
|
||||
| Flat view
|
||||
//- li.disabled(ui-sref-active="active")
|
||||
//- a(ui-sref="graph")
|
||||
//- i.fa.fa-sitemap
|
||||
//- | Graphs view
|
||||
li.divider
|
||||
//- li.disabled
|
||||
//- a
|
||||
//- i.fa.fa-clock-o
|
||||
//- | Scheduler
|
||||
li(
|
||||
ui-sref-active = 'active'
|
||||
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
|
||||
)
|
||||
a(ui-sref="settings.index")
|
||||
i.fa.fa-cog
|
||||
| Settings
|
||||
li.divider
|
||||
li(ui-sref-active="active")
|
||||
a(ui-sref="about")
|
||||
i.fa.fa-info-circle(style="color:#5bc0de")
|
||||
| About
|
||||
//- /Main menu.
|
||||
|
||||
li
|
||||
a(ui-sref="settings.update")
|
||||
i.fa.fa-question-circle.text-warning(ng-if = '!navbar.updater.state', tooltip = 'No update information available')
|
||||
i.fa.fa-question-circle.text-info(ng-if = 'navbar.updater.state == "connected"', tooltip = 'Update information may be available')
|
||||
i.fa.fa-check.text-success(ng-if = 'navbar.updater.state == "upToDate"', tooltip = 'Your XOA is up-to-date')
|
||||
i.fa.fa-bell.text-primary(ng-if = 'navbar.updater.state == "upgradeNeeded"', tooltip = 'You need to update your XOA (new version is available)')
|
||||
i.fa.fa-bell-slash.text-warning(ng-if = 'navbar.updater.state == "registerNeeded"', tooltip = 'Your XOA is not registered for updates')
|
||||
i.fa.fa-exclamation-triangle.text-danger(ng-if = 'navbar.updater.state == "error"', tooltip = 'Can\'t fetch update information')
|
||||
|
||||
li
|
||||
a(ui-sref="settings.users", tooltip="{{navbar.user.email}}")
|
||||
i.fa.fa-user
|
||||
span.hidden-sm {{navbar.user.email}}
|
||||
li
|
||||
a(ng-click = 'navbar.logOut()')
|
||||
i.fa.fa-sign-out
|
||||
|
|
||||
|
|
||||
|
||||
//- /Right items.
|
||||
//- /Navbar items.
|
||||
//- /Navbar.
|
||||
@@ -1,435 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import Bluebird from 'bluebird'
|
||||
import forEach from 'lodash.foreach'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import view from './view'
|
||||
import _indexOf from 'lodash.indexof'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.newSr', [
|
||||
uiRouter
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('SRs_new', {
|
||||
url: '/srs/new/:container',
|
||||
controller: 'NewSrCtrl as newSr',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('NewSrCtrl', function ($scope, $state, $stateParams, xo, xoApi, notify, modal, bytesToSizeFilter) {
|
||||
|
||||
this.reset = function (data = {}) {
|
||||
|
||||
this.data = {}
|
||||
delete this.lockCreation
|
||||
this.lock = !(
|
||||
(data.srType === 'Local') &&
|
||||
(data.srPath && data.srPath.path)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
this.resetLists = function () {
|
||||
delete this.data.nfsList
|
||||
delete this.data.scsiList
|
||||
delete this.lockCreation
|
||||
this.lock = true
|
||||
|
||||
this.resetErrors()
|
||||
}
|
||||
|
||||
this.resetErrors = function () {
|
||||
delete this.data.error
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads NFS paths and iScsi iqn`s
|
||||
*/
|
||||
this.populateSettings = function (type, server, auth, user, password) {
|
||||
|
||||
this.reset()
|
||||
this.loading = true
|
||||
|
||||
server = this._parseAddress(server)
|
||||
|
||||
if (type === 'NFS' || type === 'NFS_ISO') {
|
||||
xoApi.call('sr.probeNfs', {
|
||||
host: this.container.id,
|
||||
server: server.host
|
||||
})
|
||||
.then(response => this.data.paths = response)
|
||||
.catch(error => notify.warning({
|
||||
title: 'NFS Detection',
|
||||
message: error.message
|
||||
}))
|
||||
.finally(() => this.loading = false)
|
||||
} else if (type === 'iSCSI') {
|
||||
let params = {
|
||||
host: this.container.id
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
params.chapUser = user
|
||||
params.chapPassword = password
|
||||
}
|
||||
|
||||
params.target = server.host
|
||||
if (server.port) {
|
||||
params.port = server.port
|
||||
}
|
||||
|
||||
xoApi.call('sr.probeIscsiIqns', params)
|
||||
.then(response => {
|
||||
|
||||
if (response.length > 0) {
|
||||
this.data.iqns = response
|
||||
} else {
|
||||
notify.warning({
|
||||
title: 'iSCSI Detection',
|
||||
message: 'No IQNs found'
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
.catch(error => notify.warning({
|
||||
title: 'iSCSI Detection',
|
||||
message: error.message
|
||||
}))
|
||||
.finally(() => this.loading = false)
|
||||
} else {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads iScsi LUNs
|
||||
*/
|
||||
this.populateIScsiIds = function (iqn, auth, user, password) {
|
||||
delete this.data.iScsiIds
|
||||
this.loading = true
|
||||
|
||||
let params = {
|
||||
host: this.container.id,
|
||||
target: iqn.ip,
|
||||
targetIqn: iqn.iqn
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
params.chapUser = user
|
||||
params.chapPassword = password
|
||||
}
|
||||
|
||||
xoApi.call('sr.probeIscsiLuns', params)
|
||||
.then(response => {
|
||||
|
||||
forEach(response, item => {
|
||||
item.display = 'LUN ' + item.id + ': ' +
|
||||
item.serial + ' ' + bytesToSizeFilter(item.size) +
|
||||
' (' + item.vendor + ')'
|
||||
})
|
||||
|
||||
this.data.iScsiIds = response
|
||||
})
|
||||
.catch(error => notify.warning({
|
||||
title: 'LUNs Detection',
|
||||
message: error.message
|
||||
}))
|
||||
.finally(() => this.loading = false)
|
||||
}
|
||||
|
||||
this._parseAddress = function (address) {
|
||||
let index = address.indexOf(':')
|
||||
let port = false
|
||||
let host = address
|
||||
if (index > -1) {
|
||||
port = address.substring(index + 1)
|
||||
host = address.substring(0, index)
|
||||
}
|
||||
return {
|
||||
host,
|
||||
port
|
||||
}
|
||||
}
|
||||
|
||||
this._prepareNfsParams = function (data) {
|
||||
let server = this._parseAddress(data.srServer)
|
||||
|
||||
let params = {
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
server: server.host,
|
||||
serverPath: data.srPath.path
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
this._prepareScsiParams = function (data) {
|
||||
let params = {
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
target: data.srIqn.ip,
|
||||
targetIqn: data.srIqn.iqn,
|
||||
scsiId: data.srIScsiId.scsiId
|
||||
}
|
||||
|
||||
let server = this._parseAddress(data.srServer)
|
||||
if (server.port) {
|
||||
params.port = server.port
|
||||
}
|
||||
if (data.srAuth) {
|
||||
params.chapUser = data.srChapUser
|
||||
params.chapPassword = data.srChapPassword
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
this.createSR = function (data) {
|
||||
this.lock = true
|
||||
this.creating = true
|
||||
|
||||
let operationToPromise
|
||||
|
||||
switch (data.srType) {
|
||||
case 'NFS':
|
||||
let nfsParams = this._prepareNfsParams(data)
|
||||
operationToPromise = this._checkNfsExistence(nfsParams)
|
||||
.then(() => xoApi.call('sr.createNfs', nfsParams))
|
||||
break
|
||||
|
||||
case 'iSCSI':
|
||||
let scsiParams = this._prepareScsiParams(data)
|
||||
operationToPromise = this._checkScsiExistence(scsiParams)
|
||||
.then(() => xoApi.call('sr.createIscsi', scsiParams))
|
||||
break
|
||||
|
||||
case 'lvm':
|
||||
let device = data.srDevice.device
|
||||
|
||||
operationToPromise = xoApi.call('sr.createLvm', {
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
device
|
||||
})
|
||||
break
|
||||
|
||||
case 'NFS_ISO':
|
||||
case 'Local':
|
||||
let server = this._parseAddress(data.srServer || '')
|
||||
|
||||
let path = (
|
||||
data.srType === 'NFS_ISO' ?
|
||||
server.host + ':' :
|
||||
''
|
||||
) + data.srPath.path
|
||||
|
||||
operationToPromise = xoApi.call('sr.createIso', {
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
path
|
||||
})
|
||||
break
|
||||
|
||||
default:
|
||||
operationToPromise = Bluebird.reject({message: 'Unhanled SR Type'})
|
||||
break
|
||||
}
|
||||
|
||||
operationToPromise
|
||||
.then(id => {
|
||||
$state.go('SRs_view', {id})
|
||||
})
|
||||
.catch(error => {
|
||||
notify.error({
|
||||
title: 'Storage Creation Error',
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.lock = false
|
||||
this.creating = false
|
||||
})
|
||||
}
|
||||
|
||||
this._checkScsiExistence = function (params) {
|
||||
this.resetLists()
|
||||
|
||||
return xoApi.call('sr.probeIscsiExists', params)
|
||||
.then(response => {
|
||||
if (response.length > 0) {
|
||||
this.data.scsiList = response
|
||||
return modal.confirm({
|
||||
title: 'Previous LUN Usage',
|
||||
message: 'This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation. Are you sure?'
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
this._checkNfsExistence = function (params) {
|
||||
this.resetLists()
|
||||
|
||||
return xoApi.call('sr.probeNfsExists', params)
|
||||
.then(response => {
|
||||
if (response.length > 0) {
|
||||
this.data.nfsList = response
|
||||
return modal.confirm({
|
||||
title: 'Previous Path Usage',
|
||||
message: 'This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation. Are you sure?'
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
const hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
const srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
this._gatherConnectedUuids = function () {
|
||||
const srIds = []
|
||||
|
||||
// Shared SRs.
|
||||
forEach(srsByContainer[this.container.$poolId], sr => {
|
||||
srIds.push(sr.id)
|
||||
})
|
||||
|
||||
// Local SRs.
|
||||
forEach(hostsByPool[this.container.$poolId], host => {
|
||||
forEach(srsByContainer[host.id], sr => {
|
||||
srIds.push(sr.id)
|
||||
})
|
||||
})
|
||||
|
||||
return srIds
|
||||
}
|
||||
|
||||
this._processSRList = function (list) {
|
||||
let inUse = false
|
||||
let SRs = this._gatherConnectedUuids()
|
||||
|
||||
forEach(list, item => {
|
||||
inUse = (item.used = _indexOf(SRs, item.uuid) > -1) || inUse
|
||||
})
|
||||
|
||||
this.lockCreation = inUse
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
this.loadScsiList = function (data) {
|
||||
this.resetLists()
|
||||
this.loading = true
|
||||
|
||||
let params = this._prepareScsiParams(data)
|
||||
|
||||
xoApi.call('sr.probeIscsiExists', params)
|
||||
.then(response => {
|
||||
|
||||
if (response.length > 0) {
|
||||
this.data.scsiList = this._processSRList(response)
|
||||
}
|
||||
|
||||
this.lock = !Boolean(data.srIScsiId)
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
notify.error({
|
||||
title: 'iSCSI Error',
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
.finally(() => this.loading = false)
|
||||
}
|
||||
|
||||
this.loadNfsList = function (data) {
|
||||
this.resetLists()
|
||||
|
||||
let server = this._parseAddress(data.srServer)
|
||||
|
||||
xoApi.call('sr.probeNfsExists', {
|
||||
host: this.container.id,
|
||||
server: server.host,
|
||||
serverPath: data.srPath.path
|
||||
})
|
||||
.then(response => {
|
||||
|
||||
if (response.length > 0) {
|
||||
this.data.scsiList = this._processSRList(response)
|
||||
}
|
||||
|
||||
this.lock = !Boolean(data.srPath.path)
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
notify.error({
|
||||
title: 'NFS error',
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.reattachNfs = function (uuid, {name, nameError}, {desc, descError}, iso) {
|
||||
this._reattach(uuid, 'nfs', {name, nameError}, {desc, descError}, iso)
|
||||
}
|
||||
|
||||
this.reattachIScsi = function (uuid, {name, nameError}, {desc, descError}) {
|
||||
this._reattach(uuid, 'iscsi', {name, nameError}, {desc, descError})
|
||||
}
|
||||
|
||||
this._reattach = function (uuid, type, {name, nameError}, {desc, descError}, iso = false) {
|
||||
this.resetErrors()
|
||||
let method = 'sr.reattach' + (iso ? 'Iso' : '')
|
||||
|
||||
if (nameError || descError) {
|
||||
this.data.error = {
|
||||
name: nameError,
|
||||
desc: descError
|
||||
}
|
||||
notify.warning({
|
||||
title: 'Missing parameters',
|
||||
message: 'Complete the General section information, please'
|
||||
})
|
||||
} else {
|
||||
this.lock = true
|
||||
this.attaching = true
|
||||
xoApi.call(method, {
|
||||
host: this.container.id,
|
||||
uuid,
|
||||
nameLabel: name,
|
||||
nameDescription: desc,
|
||||
type
|
||||
})
|
||||
.then(id => {
|
||||
$state.go('SRs_view', {id})
|
||||
})
|
||||
.catch(error => notify.error({
|
||||
title: 'reattach',
|
||||
message: error.message
|
||||
})
|
||||
)
|
||||
.finally(() => {
|
||||
this.lock = false
|
||||
this.attaching = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.reset()
|
||||
|
||||
$scope.$watch(() => xoApi.get($stateParams.container), container => {
|
||||
this.container = container
|
||||
})
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
@@ -1,183 +0,0 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-sr
|
||||
| Add SR on
|
||||
a(ng-if="'pool' === newSr.container.type", ui-sref="pools_view({id: newSr.container.id})")
|
||||
| {{newSr.container.name_label}}
|
||||
a(ng-if="'host' === newSr.container.type", ui-sref="hosts_view({id: newSr.container.id})")
|
||||
| {{newSr.container.name_label}}
|
||||
form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
|
||||
.grid
|
||||
//- Choose SR type panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-info-circle(style="color: #e25440;")
|
||||
| General
|
||||
.panel-body
|
||||
.form-group
|
||||
label.col-sm-3.control-label Type
|
||||
.col-sm-9
|
||||
select.form-control(ng-change = 'newSr.reset(formData)', ng-model = 'formData.srType', name = 'srType', ng-required = 'true')
|
||||
option(value="") -- Choose a type of SR --
|
||||
optgroup(label="VDI SR")
|
||||
option(value="NFS") NFS
|
||||
option(value="iSCSI") iSCSI
|
||||
option(value="lvm") Local LVM
|
||||
optgroup(label="ISO SR")
|
||||
option(value="Local") Local
|
||||
option(value="NFS_ISO") NFS ISO
|
||||
.form-group(ng-class = '{"has-error": newSr.data.error.name}')
|
||||
label.col-sm-3.control-label Name
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="", name = 'srName', ng-model = 'formData.srName', ng-required = 'true')
|
||||
.form-group(ng-class = '{"has-error": newSr.data.error.desc}')
|
||||
label.col-sm-3.control-label Description
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="SR Created by Xen Orchestra", name = 'srDesc', ng-model = 'formData.srDesc', ng-required = 'true')
|
||||
//- Choose SR details
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs(style="color: #e25440;")
|
||||
| Settings
|
||||
.panel-body
|
||||
.form-group(ng-if = 'formData.srType === "NFS" || formData.srType === "iSCSI" || formData.srType === "NFS_ISO"')
|
||||
label.col-sm-3.control-label
|
||||
| Server
|
||||
span(ng-if = 'formData.srType === "iSCSI"')
|
||||
| (auth
|
||||
input(type = 'checkbox', ng-model = 'formData.srAuth')
|
||||
| )
|
||||
.col-sm-9
|
||||
.input-group
|
||||
input.form-control(type="text", placeholder='address{{ formData.srType === "iSCSI" ? "[:port]" : "" }}', name = 'srServer', ng-model = 'formData.srServer', required)
|
||||
span.input-group-btn
|
||||
button.btn.btn-default(type = 'button', ng-click = 'newSr.populateSettings(formData.srType, formData.srServer, formData.srAuth, formData.srChapUser, formData.srChapPassword)')
|
||||
i.fa.fa-search
|
||||
|
||||
//- For Local LVM
|
||||
.form-group(ng-if = 'formData.srType === "lvm"')
|
||||
label.col-sm-3.control-label Device
|
||||
.col-sm-9
|
||||
input.form-control(
|
||||
ng-if = 'formData.srType === "lvm"'
|
||||
type = 'text'
|
||||
name = 'srDevice'
|
||||
ng-model = 'formData.srDevice.device'
|
||||
placeholder = 'Device, e.g /dev/sda...'
|
||||
ng-change = 'newSr.lock = !formData.srDevice.device'
|
||||
required
|
||||
)
|
||||
|
||||
.form-group(ng-if = 'newSr.data.paths || formData.srType === "Local"')
|
||||
label.col-sm-3.control-label Path
|
||||
.col-sm-9
|
||||
//- For NFS
|
||||
select.form-control(
|
||||
ng-if = 'newSr.data.paths'
|
||||
name = 'srPath'
|
||||
ng-change = 'newSr.loadNfsList(formData)'
|
||||
ng-model = 'formData.srPath'
|
||||
ng-options = 'item.path for item in newSr.data.paths', required)
|
||||
option(value = '', disabled) -- Choose path --
|
||||
//- For Local
|
||||
input.form-control(
|
||||
ng-if = 'formData.srType === "Local"'
|
||||
type = 'text'
|
||||
name = 'srPath'
|
||||
ng-model = 'formData.srPath.path'
|
||||
ng-change = 'newSr.lock = !formData.srPath.path'
|
||||
required
|
||||
)
|
||||
|
||||
//- For iScsi
|
||||
.form-group(ng-if = 'formData.srType === "iSCSI"')
|
||||
.col-sm-9.col-sm-offset-3.form-inline(ng-if = 'formData.srAuth')
|
||||
label.sr-only(for = 'chapUser') User
|
||||
input#chapUser.form-control(type = 'text', ng-model = 'formData.srChapUser', placeholder = 'user', ng-required = 'formData.srAuth')
|
||||
|  
|
||||
label.sr-only(for = 'chapUser') Password
|
||||
input#chapPassword.form-control(type = 'password', ng-model = 'formData.srChapPassword', placeholder = 'password', ng-required = 'formData.srAuth')
|
||||
.form-group(ng-if = 'newSr.data.iqns')
|
||||
label.col-sm-3.control-label IQN
|
||||
.col-sm-9
|
||||
select.form-control(ng-change = 'newSr.populateIScsiIds(formData.srIqn, formData.srAuth, formData.srChapUser, formData.srChapPassword)', name = 'srIqn', ng-model = 'formData.srIqn', ng-options = '(item.iqn + " (" + item.ip + ")") for item in newSr.data.iqns', required)
|
||||
option(value = '', disabled) -- Choose IQN --
|
||||
.form-group(ng-if = 'newSr.data.iScsiIds')
|
||||
label.col-sm-3.control-label LUN
|
||||
.col-sm-9
|
||||
select.form-control(name = 'srIScsiId', ng-change = 'newSr.loadScsiList(formData)', ng-model = 'formData.srIScsiId', ng-options = 'item.display for item in newSr.data.iScsiIds', required)
|
||||
option(value = '', disabled) -- Choose LUN --
|
||||
.form-group.text-center(ng-if = 'newSr.loading')
|
||||
i.fa.fa-circle-o-notch.fa-spin.fa-2x
|
||||
|
||||
.grid(ng-if = 'newSr.data.nfsList && newSr.data.nfsList.length > 0')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-eye(style="color: #e25440;")
|
||||
| NFS storage use
|
||||
.panel-body
|
||||
table.table.table-condensed
|
||||
tr
|
||||
th.text-center Storage UUID
|
||||
th
|
||||
tr(ng-repeat = 'nfsSr in newSr.data.nfsList')
|
||||
td.text-center {{ nfsSr.uuid }}
|
||||
td.text-center(ng_if = '!nfsSr.used')
|
||||
button.btn.btn-sm.btn-primary(type = 'button', ng-class = '{disabled: newSr.lock}', ng-click = 'newSr.reattachNfs(nfsSr.uuid, {name: formData.srName, nameError: srForm.srName.$error.required}, {desc: formData.srDesc, descError: srForm.srDesc.$error.required}, "NFS_ISO" === formData.srType)') Reattach
|
||||
td.text-center(ng_if = 'nfsSr.used', ng-class = '{disabled: newSr.lock}')
|
||||
button.btn.btn-sm.btn-danger(ui-sref = 'SRs_view({id: nfsSr.uuid})', ng-class = '{disabled: newSr.lock}')
|
||||
i.fa.fa-eye
|
||||
| In use
|
||||
p.text-center(ng-if = 'newSr.attaching')
|
||||
i.fa.fa-circle-o-notch.fa-spin.fa-2x
|
||||
|
||||
.grid(ng-if = 'newSr.data.scsiList && newSr.data.scsiList.length > 0')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-eye(style="color: #e25440;")
|
||||
| iSCSI storage use
|
||||
.panel-body
|
||||
table.table.table-condensed
|
||||
tr
|
||||
th.text-center Storage UUID
|
||||
th
|
||||
tr(ng-repeat = 'scsiSr in newSr.data.scsiList')
|
||||
td.text-center {{ scsiSr.uuid }}
|
||||
td.text-center(ng_if = '!scsiSr.used')
|
||||
button.btn.btn-sm.btn-primary(type = 'button', ng-class = '{disabled: newSr.lock}', ng-click = 'newSr.reattachIScsi(scsiSr.uuid, {name: formData.srName, nameError: srForm.srName.$error.required}, {desc: formData.srDesc, descError: srForm.srDesc.$error.required})') Reattach
|
||||
td.text-center(ng_if = 'scsiSr.used')
|
||||
button.btn.btn-sm.btn-danger(ui-sref = 'SRs_view({id: scsiSr.uuid})', ng-class = '{disabled: newSr.lock}')
|
||||
i.fa.fa-eye
|
||||
| In use
|
||||
p.text-center(ng-if = 'newSr.attaching')
|
||||
i.fa.fa-circle-o-notch.fa-spin.fa-2x
|
||||
|
||||
//- Summary
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flag-checkered(style="color: #e25440;")
|
||||
| Summary
|
||||
.panel-body
|
||||
.grid
|
||||
.grid-cell
|
||||
p.stat-name
|
||||
| Name:
|
||||
p.center.big {{formData.srName}}
|
||||
.grid-cell
|
||||
p.stat-name
|
||||
| Type:
|
||||
p.center.big {{formData.srType}}
|
||||
.grid-cell
|
||||
div(ng-if = 'formData.srType === "iSCSI"')
|
||||
p.stat-name Size
|
||||
p.center.big {{formData.srIScsiId.size | bytesToSize}}
|
||||
div(ng-if = 'formData.srType === "NFS"')
|
||||
p.stat-name Path
|
||||
p.center.big {{formData.srPath.path}}
|
||||
p.center
|
||||
button.btn.btn-lg.btn-primary(type="submit", ng-disabled = 'newSr.lock || newSr.lockCreation')
|
||||
i.fa.fa-play
|
||||
| Create SR
|
||||
i.fa.fa-circle-o-notch.fa-spin(ng-if = 'newSr.creating')
|
||||
@@ -1,264 +0,0 @@
|
||||
angular = require 'angular'
|
||||
cloneDeep = require 'lodash.clonedeep'
|
||||
forEach = require 'lodash.foreach'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.newVm', [
|
||||
require 'angular-ui-router'
|
||||
]
|
||||
.config ($stateProvider) ->
|
||||
$stateProvider.state 'VMs_new',
|
||||
url: '/vms/new/:container'
|
||||
controller: 'NewVmsCtrl'
|
||||
template: require './view'
|
||||
.controller 'NewVmsCtrl', (
|
||||
$scope, $stateParams, $state
|
||||
xoApi, xo
|
||||
bytesToSizeFilter, sizeToBytesFilter
|
||||
notify
|
||||
) ->
|
||||
{get} = xoApi
|
||||
|
||||
removeItems = do ->
|
||||
splice = Array::splice.call.bind Array::splice
|
||||
(array, index, n) -> splice array, index, n ? 1
|
||||
|
||||
merge = do ->
|
||||
push = Array::push.apply.bind Array::push
|
||||
(args...) ->
|
||||
result = []
|
||||
for arg in args
|
||||
push result, arg if arg?
|
||||
result
|
||||
|
||||
pool = default_SR = null
|
||||
do (
|
||||
networks = xoApi.getIndex('networksByPool')
|
||||
) ->
|
||||
Object.defineProperties($scope, {
|
||||
networks: {
|
||||
get: () => pool && networks[pool.id]
|
||||
}
|
||||
})
|
||||
|
||||
srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
vmTemplatesByContainer = xoApi.getIndex('vmTemplatesByContainer')
|
||||
$scope.$watch(
|
||||
-> get $stateParams.container
|
||||
(container) ->
|
||||
$scope.container = container
|
||||
|
||||
# If the container was not found, no need to continue.
|
||||
return unless container?
|
||||
|
||||
if container.type is 'host'
|
||||
host = container
|
||||
pool = (get container.$poolId) ? {}
|
||||
else
|
||||
host = {}
|
||||
pool = container
|
||||
|
||||
default_SR = get pool.default_SR
|
||||
default_SR = if default_SR
|
||||
default_SR.id
|
||||
else
|
||||
''
|
||||
|
||||
# Computes the list of templates.
|
||||
templates = $scope.templates = []
|
||||
forEach(vmTemplatesByContainer[host.id], (template) =>
|
||||
templates.push(template)
|
||||
return
|
||||
)
|
||||
forEach(vmTemplatesByContainer[pool.id], (template) =>
|
||||
templates.push(template)
|
||||
return
|
||||
)
|
||||
|
||||
# Computes the list of srs.
|
||||
SRs = []
|
||||
forEach(srsByContainer[host.id], (template) =>
|
||||
SRs.push(template)
|
||||
return
|
||||
)
|
||||
forEach(srsByContainer[pool.id], (template) =>
|
||||
SRs.push(template)
|
||||
return
|
||||
)
|
||||
|
||||
# Computes the list of ISO SRs.
|
||||
$scope.ISO_SRs = (SR for SR in SRs when SR.content_type is 'iso')
|
||||
|
||||
# Computes the list of writable SRs.
|
||||
$scope.writable_SRs = (SR for SR in SRs when SR.content_type isnt 'iso')
|
||||
)
|
||||
|
||||
$scope.availableMethods = {}
|
||||
$scope.CPUs = ''
|
||||
$scope.installation_cdrom = ''
|
||||
$scope.installation_method = ''
|
||||
$scope.installation_network = ''
|
||||
$scope.memory = ''
|
||||
$scope.name_description = ''
|
||||
$scope.name_label = ''
|
||||
$scope.template = ''
|
||||
$scope.VDIs = []
|
||||
$scope.VIFs = []
|
||||
$scope.isDiskTemplate = false
|
||||
|
||||
$scope.addVIF = do ->
|
||||
id = 0
|
||||
->
|
||||
$scope.VIFs.push {
|
||||
id: id++
|
||||
network: ''
|
||||
}
|
||||
$scope.addVIF()
|
||||
|
||||
$scope.removeVIF = (index) -> removeItems $scope.VIFs, index
|
||||
|
||||
$scope.moveVDI = (index, direction) ->
|
||||
{VDIs} = $scope
|
||||
|
||||
newIndex = index + direction
|
||||
[VDIs[index], VDIs[newIndex]] = [VDIs[newIndex], VDIs[index]]
|
||||
|
||||
$scope.removeVDI = (index) -> removeItems $scope.VDIs, index
|
||||
|
||||
VDI_id = 0
|
||||
$scope.addVDI = ->
|
||||
$scope.VDIs.push {
|
||||
id: VDI_id++
|
||||
bootable: false
|
||||
size: ''
|
||||
SR: default_SR
|
||||
type: 'system'
|
||||
}
|
||||
|
||||
# When the selected template changes, updates other variables.
|
||||
$scope.$watch 'template', (template) ->
|
||||
return unless template
|
||||
|
||||
{install_methods} = template.template_info
|
||||
availableMethods = $scope.availableMethods = Object.create null
|
||||
for method in install_methods
|
||||
availableMethods[method] = true
|
||||
if install_methods.length is 1 # FIXME: does not work with network.
|
||||
$scope.installation_method = install_methods[0]
|
||||
else
|
||||
delete $scope.installation_method
|
||||
|
||||
|
||||
VDIs = $scope.VDIs = cloneDeep template.template_info.disks
|
||||
# if the template has no config disk
|
||||
# nor it's Other install media (specific case)
|
||||
# then do NOT display disk and network panel
|
||||
if VDIs.length is 0 and template.name_label isnt 'Other install media'
|
||||
$scope.isDiskTemplate = true
|
||||
$scope.VIFs.length = 0
|
||||
else $scope.isDiskTemplate = false
|
||||
for VDI in VDIs
|
||||
VDI.id = VDI_id++
|
||||
VDI.size = bytesToSizeFilter VDI.size
|
||||
VDI.SR or= default_SR
|
||||
|
||||
$scope.createVM = ->
|
||||
{
|
||||
CPUs
|
||||
installation_cdrom
|
||||
installation_method
|
||||
installation_network
|
||||
memory
|
||||
name_description
|
||||
name_label
|
||||
template
|
||||
VDIs
|
||||
VIFs
|
||||
} = $scope
|
||||
# Does not edit the displayed data directly.
|
||||
VDIs = cloneDeep VDIs
|
||||
for VDI, index in VDIs
|
||||
# Removes the dummy identifier used for AngularJS.
|
||||
delete VDI.id
|
||||
|
||||
# Adds the device number based on the index.
|
||||
VDI.device = "#{index}"
|
||||
|
||||
# Transforms the size from human readable format to bytes.
|
||||
VDI.size = sizeToBytesFilter VDI.size
|
||||
# TODO: handles invalid values.
|
||||
|
||||
# Does not edit the displayed data directly.
|
||||
VIFs = cloneDeep VIFs
|
||||
for VIF in VIFs
|
||||
# Removes the dummy identifier used for AngularJS.
|
||||
delete VIF.id
|
||||
|
||||
# Removes the MAC address if empty.
|
||||
if 'MAC' of VIF
|
||||
VIF.MAC = VIF.MAC.trim()
|
||||
delete VIF.MAC unless VIF.MAC
|
||||
|
||||
|
||||
if installation_method is 'cdrom'
|
||||
installation = {
|
||||
method: 'cdrom'
|
||||
repository: installation_cdrom
|
||||
}
|
||||
else if installation_network
|
||||
matches = /^(http|ftp|nfs)/i.exec installation_network
|
||||
throw new Error 'invalid network URL' unless matches
|
||||
installation = {
|
||||
method: matches[1].toLowerCase()
|
||||
repository: installation_network
|
||||
}
|
||||
else
|
||||
installation = undefined
|
||||
|
||||
data = {
|
||||
installation
|
||||
name_label
|
||||
template: template.id
|
||||
VDIs
|
||||
VIFs
|
||||
}
|
||||
|
||||
# TODO:
|
||||
# - disable the form during creation
|
||||
# - indicate the progress of the operation
|
||||
notify.info {
|
||||
title: 'VM creation'
|
||||
message: 'VM creation started'
|
||||
}
|
||||
|
||||
xoApi.call('vm.create', data).then (id) ->
|
||||
# If nothing to sets, just stops.
|
||||
return id unless CPUs or name_description or memory
|
||||
|
||||
data = {
|
||||
id
|
||||
}
|
||||
data.CPUs = +CPUs if CPUs
|
||||
|
||||
if name_description
|
||||
data.name_description = name_description
|
||||
|
||||
if memory
|
||||
memory = sizeToBytesFilter memory
|
||||
# FIXME: handles invalid entries.
|
||||
data.memory = memory
|
||||
|
||||
xoApi.call('vm.set', data).then -> id
|
||||
.then (id) ->
|
||||
$state.go 'VMs_view', { id }
|
||||
.catch (error) ->
|
||||
notify.error {
|
||||
title: 'VM creation'
|
||||
message: 'The creation failed'
|
||||
}
|
||||
|
||||
console.log error
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
@@ -1,220 +0,0 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-vm
|
||||
| Create VM on
|
||||
a(ng-if="'pool' === container.type", ui-sref="pools_view({id: container.id})")
|
||||
| {{container.name_label}}
|
||||
a(ng-if="'host' === container.type", ui-sref="hosts_view({id: container.id})")
|
||||
| {{container.name_label}}
|
||||
//- Add server panel
|
||||
form.form-horizontal(ng-submit="createVM()")
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-info-circle(style="color: #e25440;")
|
||||
| VM info
|
||||
.panel-body
|
||||
.form-group
|
||||
label.col-sm-3.control-label Template
|
||||
.col-sm-9
|
||||
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | orderBy:natural('name_label') track by template.id", required="")
|
||||
.form-group
|
||||
label.col-sm-3.control-label Name
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="Name of your new VM", required="", ng-model="name_label")
|
||||
.form-group
|
||||
label.col-sm-3.control-label Description
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="Optional description of you new VM", ng-model="name_description")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-dashboard(style="color: #e25440;")
|
||||
| Performances
|
||||
.panel-body
|
||||
.form-group
|
||||
label.col-sm-3.control-label vCPUs
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="{{template.CPUs.number}}", ng-model="CPUs")
|
||||
.form-group
|
||||
label.col-sm-3.control-label RAM
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="{{template.memory.size | bytesToSize}}", ng-model="memory")
|
||||
.grid(ng-if="isDiskTemplate")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-info-circle(style="color: #e25440;")
|
||||
| Template info
|
||||
.panel-body
|
||||
p.center This template will create automatically a VM with:
|
||||
.col-md-6
|
||||
ul(ng-repeat="VIF in template.VIFs | resolve | orderBy:natural('device') track by VIF.id")
|
||||
li Interface \#{{VIF.device}} (MTU {{VIF.MTU}}) on {{(VIF.$network | resolve).name_label}}
|
||||
.col-md-6
|
||||
ul(ng-repeat = 'VBD in (template.$VBDs | resolve) track by VBD.id')
|
||||
li Disk {{(VBD.VDI | resolve).name_label}} ({{(VBD.VDI | resolve).size | bytesToSize}}) on {{((VBD.VDI | resolve).$SR | resolve).name_label}}
|
||||
.grid(ng-if="!isDiskTemplate")
|
||||
//- Install panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-download(style="color: #e25440;")
|
||||
| Install settings
|
||||
.panel-body
|
||||
.form-group(ng-show="availableMethods.cdrom")
|
||||
label.col-sm-3.control-label ISO/DVD
|
||||
.col-sm-9
|
||||
.input-group
|
||||
span.input-group-addon
|
||||
input(
|
||||
type = 'radio'
|
||||
name = 'installation_method'
|
||||
ng-model = '$parent.installation_method'
|
||||
value = 'cdrom'
|
||||
required
|
||||
)
|
||||
select.form-control.disabled(
|
||||
ng-disabled="'cdrom' !== installation_method"
|
||||
ng-model="$parent.installation_cdrom"
|
||||
required
|
||||
)
|
||||
option(value = '') Please select
|
||||
optgroup(ng-repeat="SR in ISO_SRs | orderBy:natural('name_label') track by SR.id", ng-if="SR.VDIs.length", label="{{SR.name_label}}")
|
||||
option(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.id", ng-value="VDI.id")
|
||||
| {{VDI.name_label}}
|
||||
.form-group(ng-show="availableMethods.http || availableMethods.ftp || availableMethods.nfs")
|
||||
label.col-sm-3.control-label Network
|
||||
.col-sm-9
|
||||
.input-group
|
||||
span.input-group-addon
|
||||
input(
|
||||
type = 'radio'
|
||||
name = 'installation_method'
|
||||
ng-model = '$parent.installation_method'
|
||||
value = 'network'
|
||||
required
|
||||
)
|
||||
input.form-control(type="text", ng-disabled="'network' !== installation_method", placeholder="e.g: http://ftp.debian.org/debian", ng-model="$parent.installation_network")
|
||||
|
||||
//- <div class="form-group"> FIXME
|
||||
//- <label class="col-sm-3 control-label">Home server</label>
|
||||
//- <div class="col-sm-9">
|
||||
//- <select class="form-control">
|
||||
//- <option>Default (auto)</option>
|
||||
//- </select>
|
||||
//- </div>
|
||||
//- </div>
|
||||
|
||||
//- Interface panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-network(style="color: #e25440;")
|
||||
| Interfaces
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th MAC
|
||||
th Network
|
||||
th.col-md-1  
|
||||
//- Buttons
|
||||
tr(ng-repeat="VIF in VIFs track by VIF.id")
|
||||
td
|
||||
input.form-control(type="text", ng-model="VIF.MAC", ng-pattern="/^\s*[0-9a-f]{2}(:[0-9a-f]{2}){5}\s*$/i", placeholder="00:00:00:00:00")
|
||||
td
|
||||
select.form-control(
|
||||
ng-options = 'network.id as network.name_label for network in networks | orderBy:natural("name_label") track by network.id'
|
||||
ng-model = 'VIF.network'
|
||||
required
|
||||
)
|
||||
option(value = '') Please select
|
||||
td
|
||||
.pull-right
|
||||
button.btn.btn-default(type="button", ng-click="removeVIF($index)", title="Remove this interface")
|
||||
i.fa.fa-times
|
||||
.btn-form
|
||||
p.center
|
||||
.btn-form
|
||||
p.center
|
||||
button.btn.btn-success(type="button", ng-click="addVIF()")
|
||||
i.fa.fa-plus
|
||||
| Add interface
|
||||
//- end of misc and interface panel
|
||||
//- Disk panel
|
||||
.grid(ng-if="!isDiskTemplate")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-sr(style="color: #e25440;")
|
||||
| Disks
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th.col-md-2 SR
|
||||
th.col-md-1 Bootable?
|
||||
th.col-md-2 Size
|
||||
th.col-md-2 Name
|
||||
th.col-md-4 Description
|
||||
th.col-md-1  
|
||||
//- Buttons
|
||||
tr(ng-repeat="VDI in VDIs track by VDI.id")
|
||||
td
|
||||
select.form-control(ng-model="VDI.SR", ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))")
|
||||
td.text-center
|
||||
input(type="checkbox", ng-model="VDI.bootable")
|
||||
td
|
||||
input.form-control(type="text", ng-model="VDI.size", required="")
|
||||
td
|
||||
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="VDI.name_label")
|
||||
td
|
||||
input.form-control(type="text", placeholder="Description of this virtual disk", ng-model="VDI.name_description")
|
||||
td
|
||||
.btn-group
|
||||
button.btn.btn-default(type="button", ng-click="moveVDI($index, -1)", ng-disabled="$first", title="Move this disk up")
|
||||
i.fa.fa-chevron-up
|
||||
button.btn.btn-default(type="button", ng-click="moveVDI($index, 1)", ng-disabled="$last", title="Move this disk down")
|
||||
i.fa.fa-chevron-down
|
||||
.pull-right
|
||||
button.btn.btn-default(type="button", ng-click="removeVDI($index)", title="Remove this disk")
|
||||
i.fa.fa-times
|
||||
.btn-form
|
||||
p.center
|
||||
.btn-form
|
||||
p.center
|
||||
button.btn.btn-success(type="button", ng-click="addVDI()")
|
||||
i.fa.fa-plus
|
||||
| Add disk
|
||||
//- Confirmation panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flag-checkered(style="color: #e25440;")
|
||||
| Summary
|
||||
.panel-body
|
||||
.grid
|
||||
.grid-cell
|
||||
p.center.big {{name_label}}
|
||||
|
|
||||
span.small(ng-if="template.name_label") ({{template.name_label}})
|
||||
.grid
|
||||
.grid-cell
|
||||
//- p.stat-name vCPUs
|
||||
p.center.big(tooltip="vCPUs")
|
||||
| {{CPUs || template.CPUs.number || 0}}x
|
||||
i.xo-icon-cpu
|
||||
.grid-cell
|
||||
//- p.stat-name RAM
|
||||
p.center.big(tooltip="RAM")
|
||||
| {{(memory) || (template.memory.size | bytesToSize)}}
|
||||
i.xo-icon-memory
|
||||
.grid-cell
|
||||
//- p.stat-name Disks
|
||||
p.center.big(tooltip="Disks")
|
||||
| {{(VDIs.length) || (template.$VBDs.length) || 0}}x
|
||||
i.fa.fa-hdd-o
|
||||
.grid-cell
|
||||
//- p.stat-name Interfaces
|
||||
p.center.big(tooltip="Network interfaces")
|
||||
| {{(VIFs.length) || (template.VIFs.length) || 0}}x
|
||||
i.xo-icon-network
|
||||
p.center
|
||||
button.btn.btn-lg.btn-primary(type="submit")
|
||||
i.fa.fa-play
|
||||
| Create VM
|
||||
@@ -1,107 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.pool', [
|
||||
uiRouter
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('pools_view', {
|
||||
url: '/pools/:id',
|
||||
controller: 'PoolCtrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('PoolCtrl', function ($scope, $stateParams, xoApi, xo, modal) {
|
||||
{
|
||||
const {id} = $stateParams
|
||||
const hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
const runningHostsByPool = xoApi.getIndex('runningHostsByPool')
|
||||
const srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
|
||||
Object.defineProperties($scope, {
|
||||
hosts: {
|
||||
get: () => hostsByPool[id]
|
||||
},
|
||||
runningHosts: {
|
||||
get: () => runningHostsByPool[id]
|
||||
},
|
||||
srs: {
|
||||
get: () => srsByContainer[id]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$scope.$watch(() => xoApi.get($stateParams.id), function (pool) {
|
||||
$scope.pool = pool
|
||||
})
|
||||
|
||||
$scope.currentLogPage = 1
|
||||
|
||||
$scope.savePool = function ($data) {
|
||||
let {pool} = $scope
|
||||
let {name_label, name_description} = $data
|
||||
|
||||
$data = {
|
||||
id: pool.id
|
||||
}
|
||||
if (name_label !== pool.name_label) {
|
||||
$data.name_label = name_label
|
||||
}
|
||||
if (name_description !== pool.name_description) {
|
||||
$data.name_description = name_description
|
||||
}
|
||||
|
||||
xoApi.call('pool.set', $data)
|
||||
}
|
||||
|
||||
$scope.deleteAllLog = function () {
|
||||
return modal.confirm({
|
||||
title: 'Log deletion',
|
||||
message: 'Are you sure you want to delete all the logs?'
|
||||
}).then(function () {
|
||||
// TODO: return all promises.
|
||||
forEach($scope.pool.messages, function (message) {
|
||||
xo.log.delete(message.id)
|
||||
console.log('Remove log', message.id)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.deleteLog = function (id) {
|
||||
console.log('Remove log', id)
|
||||
return xo.log.delete(id)
|
||||
}
|
||||
|
||||
// $scope.patchPool = ($files, id) ->
|
||||
// file = $files[0]
|
||||
// xo.pool.patch id
|
||||
// .then ({ $sendTo: url }) ->
|
||||
// return $upload.http {
|
||||
// method: 'POST'
|
||||
// url
|
||||
// data: file
|
||||
// }
|
||||
// .progress throttle(
|
||||
// (event) ->
|
||||
// percentage = (100 * event.loaded / event.total)|0
|
||||
|
||||
// notify.info
|
||||
// title: 'Upload patch'
|
||||
// message: "#{percentage}%"
|
||||
// 6e3
|
||||
// )
|
||||
// .then (result) ->
|
||||
// throw result.status if result.status isnt 200
|
||||
// notify.info
|
||||
// title: 'Upload patch'
|
||||
// message: 'Success'
|
||||
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
@@ -1,152 +0,0 @@
|
||||
//- TODO: lots of stuff.
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-pool
|
||||
| {{pool.name_label}}
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs(style="color: #e25440;")
|
||||
| General
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="poolSettings.$show()")
|
||||
i.fa.fa-edit.fa-fw
|
||||
.panel-body
|
||||
form(editable-form="", name="poolSettings", onbeforesave="savePool($data)")
|
||||
dl.dl-horizontal
|
||||
dt Name
|
||||
dd
|
||||
span(editable-text="pool.name_label", e-name="name_label", e-form="poolSettings")
|
||||
| {{pool.name_label}}
|
||||
dt Description
|
||||
dd
|
||||
span(editable-text="pool.name_description", e-name="name_description", e-form="poolSettings")
|
||||
| {{pool.name_description}}
|
||||
dt Master
|
||||
dd(ng-repeat="master in [pool.master] | resolve")
|
||||
a(ui-sref="hosts_view({id: master.id})")
|
||||
| {{master.name_label}}
|
||||
dt Tags
|
||||
dd
|
||||
span(ng-repeat="tag in pool.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
dt(ng-if="pool.default_SR") Default SR
|
||||
dd(ng-if="pool.default_SR", ng-init="default_SR = (pool.default_SR | resolve)")
|
||||
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
|
||||
dt HA
|
||||
dd
|
||||
| {{pool.HA_enabled}}
|
||||
dt UUID
|
||||
dd {{pool.UUID}}
|
||||
.btn-form(ng-show="poolSettings.$visible")
|
||||
p.center
|
||||
button.btn.btn-default(type="button", ng-disabled="poolSettings.$waiting", ng-click="poolSettings.$cancel()")
|
||||
i.fa.fa-times
|
||||
| Cancel
|
||||
|
|
||||
button.btn.btn-primary(type="submit", ng-disabled="poolSettings.$waiting")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-stats(style="color: #e25440;")
|
||||
| Stats
|
||||
.row
|
||||
.col-sm-6.col-lg-4
|
||||
p.stat-name Hosts:
|
||||
p.center.big-stat {{hosts | count}}
|
||||
.col-sm-6.col-lg-4
|
||||
p.stat-name Running:
|
||||
p.center.big-stat {{runningHosts | count}}
|
||||
|
||||
//- Action panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flash(style="color: #e25440;")
|
||||
| Actions
|
||||
.panel-body
|
||||
.grid-cell.text-center
|
||||
.grid
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add SR", type="button", style="width: 90%", disabled)
|
||||
i.xo-icon-sr.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add VM", type="button", style="width: 90%", xo-sref="VMs_new({container: pool.id})")
|
||||
i.xo-icon-vm.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Patch the pool", type="button", style="width: 90%", ng-file-select = "patchPool($files, pool.id)")
|
||||
i.fa.fa-file-code-o.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add Host", type="button", style="width: 90%")
|
||||
i.xo-icon-host.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Disconnect", type="button", style="width: 90%; margin-bottom: 0.5em")
|
||||
i.fa.fa-unlink.fa-2x.fa-fw
|
||||
|
||||
//- Hosts panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-host(style="color: #e25440;")
|
||||
| Hosts
|
||||
.panel-body
|
||||
table.table.table-hover.table-condensed
|
||||
th Name
|
||||
th.col-md-4 Description
|
||||
th.col-md-6 Memory
|
||||
tr(xo-sref="hosts_view({id: host.id})", ng-repeat="host in hosts | map | orderBy:natural('name_label') track by host.id")
|
||||
td.oneliner {{host.name_label}}
|
||||
td.oneliner {{host.name_description}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{host.memory.usage}}", aria-valuemax="{{host.memory.size}}", style="width: {{[host.memory.usage, host.memory.size] | percentage}}")
|
||||
|
||||
//- Shared SR panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-sr(style="color: #e25440;")
|
||||
| Shared SR
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
th Name
|
||||
th Description
|
||||
th Type
|
||||
th Size
|
||||
th.col-md-4 Physical/Allocated usage
|
||||
tr(
|
||||
ng-repeat="SR in srs | map | orderBy:natural('name_label') track by SR.id"
|
||||
xo-sref="SRs_view({id: SR.id})"
|
||||
)
|
||||
td.oneliner {{SR.name_label}}
|
||||
td.oneliner {{SR.name_description}}
|
||||
td {{SR.SR_type}}
|
||||
td {{SR.size | bytesToSize}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
|
||||
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
|
||||
|
||||
//- Logs panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments(style="color: #e25440;")
|
||||
| Logs
|
||||
span.quick-edit(ng-if="pool.messages | isNotEmpty", tooltip="Remove all logs", xo-click="deleteAllLog()")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="pool.messages | isEmpty") No recent logs
|
||||
table.table.table-hover(ng-if="pool.messages | isNotEmpty")
|
||||
th Date
|
||||
th Name
|
||||
tr(ng-repeat="message in pool.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="deleteLog(message.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
|
||||
.center(ng-if = '(pool.messages | count) > 5')
|
||||
pagination(boundary-links="true", total-items="pool.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
@@ -1,98 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
|
||||
import filter from 'lodash.filter'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('settings.acls', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
|
||||
xoApi,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings.acls', {
|
||||
controller: 'SettingsAcls as ctrl',
|
||||
url: '/acls',
|
||||
resolve: {
|
||||
acls (xo) {
|
||||
return xo.acl.get()
|
||||
},
|
||||
users (xo) {
|
||||
return xo.user.getAll()
|
||||
},
|
||||
groups (xo) {
|
||||
return xo.group.getAll()
|
||||
},
|
||||
roles (xo) {
|
||||
return xo.role.getAll()
|
||||
}
|
||||
},
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsAcls', function ($scope, acls, users, groups, roles, xoApi, xo) {
|
||||
this.acls = acls
|
||||
|
||||
this.users = users
|
||||
{
|
||||
let usersById = this.usersById = Object.create(null)
|
||||
for (let user of users) {
|
||||
usersById[user.id] = user
|
||||
}
|
||||
let groupsById = this.groupsById = Object.create(null)
|
||||
for (let group of groups) {
|
||||
groupsById[group.id] = group
|
||||
}
|
||||
let rolesById = this.rolesById = Object.create(null)
|
||||
for (let role of roles) {
|
||||
rolesById[role.id] = role
|
||||
}
|
||||
}
|
||||
|
||||
this.roles = roles
|
||||
this.groups = groups
|
||||
this.entities = this.users.concat(this.groups)
|
||||
|
||||
this.objects = xoApi.all
|
||||
|
||||
let refreshAcls = () => {
|
||||
xo.acl.get().then(acls => {
|
||||
this.acls = acls
|
||||
})
|
||||
}
|
||||
|
||||
this.getUser = (id) => {
|
||||
for (let user of this.users) {
|
||||
if (user.id === id) {
|
||||
return user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.addAcl = () => {
|
||||
xo.acl.add(this.subject.id, this.object.id, this.role.id).then(refreshAcls)
|
||||
this.subject = this.object = this.role = null
|
||||
}
|
||||
this.removeAcl = (subject, object, role) => {
|
||||
xo.acl.remove(subject, object, role).then(refreshAcls)
|
||||
}
|
||||
})
|
||||
.filter('selectHighLevel', () => {
|
||||
const HIGH_LEVEL_OBJECTS = {
|
||||
pool: true,
|
||||
host: true,
|
||||
VM: true,
|
||||
SR: true
|
||||
}
|
||||
let isHighLevel = (object) => HIGH_LEVEL_OBJECTS[object.type]
|
||||
|
||||
return (objects) => filter(objects, isHighLevel)
|
||||
})
|
||||
.name
|
||||
@@ -1,95 +0,0 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-key(style="color: #e25440;")
|
||||
| ACLs
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-plus-circle(style="color: #e25440;")
|
||||
| Create
|
||||
.panel-body.text-center
|
||||
form(
|
||||
ng-submit = 'ctrl.addAcl()'
|
||||
)
|
||||
.form-group
|
||||
ui-select(
|
||||
ng-model = 'ctrl.subject'
|
||||
)
|
||||
ui-select-match(
|
||||
placeholder = 'Choose a user or group'
|
||||
)
|
||||
div
|
||||
span(ng-if = '$select.selected.email')
|
||||
i.fa.fa-fw.fa-user
|
||||
| {{$select.selected.email}}
|
||||
span(ng-if = '$select.selected.name')
|
||||
i.fa.fa-fw.fa-users
|
||||
| {{$select.selected.name}}
|
||||
ui-select-choices(
|
||||
repeat = 'entity in ctrl.entities | filter:{ permission: "!admin" } | filter:$select.search'
|
||||
)
|
||||
div
|
||||
span(ng-if = 'entity.email')
|
||||
i.fa.fa-fw.fa-user
|
||||
| {{entity.email}}
|
||||
span(ng-if = 'entity.name')
|
||||
i.fa.fa-fw.fa-users
|
||||
| {{entity.name}}
|
||||
.form-group
|
||||
ui-select(
|
||||
ng-model = 'ctrl.object'
|
||||
)
|
||||
ui-select-match(
|
||||
placeholder = 'Choose an object'
|
||||
)
|
||||
div
|
||||
i(class = 'xo-icon-{{$select.selected.type | lowercase}}')
|
||||
| {{$select.selected.name_label}}
|
||||
ui-select-choices(
|
||||
repeat = 'object in ctrl.objects | selectHighLevel | filter:$select.search | orderBy:["type", "name_label"]'
|
||||
)
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{object.name_label}}
|
||||
span(ng-if="(object.type === 'SR' || object.type === 'VM') && object.$container")
|
||||
| ({{(object.$container | resolve).name_label}})
|
||||
.form-group
|
||||
ui-select(
|
||||
ng-model = 'ctrl.role'
|
||||
)
|
||||
ui-select-match(
|
||||
placeholder = 'Choose a role'
|
||||
)
|
||||
div
|
||||
i(class = 'xo-icon-{{$select.selected.type | lowercase}}')
|
||||
| {{$select.selected.name}}
|
||||
ui-select-choices(
|
||||
repeat = 'role in ctrl.roles | filter:$select.search | orderBy:"name"'
|
||||
)
|
||||
div
|
||||
i(class = 'xo-icon-{{role.type | lowercase}}')
|
||||
| {{role.name}}
|
||||
button.btn.btn-success
|
||||
i.fa.fa-plus
|
||||
| Create
|
||||
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-street-view(style="color: #e25440;")
|
||||
| Manage
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th User
|
||||
th Object
|
||||
th Role
|
||||
th
|
||||
tr(ng-repeat = 'acl in ctrl.acls')
|
||||
td {{ ctrl.usersById[acl.subject].email || ctrl.groupsById[acl.subject].name }}
|
||||
td {{(acl.object | resolve).name_label}}
|
||||
td {{ ctrl.rolesById[acl.action].name }}
|
||||
td
|
||||
button.btn.btn-sm.btn-danger(ng-click = 'ctrl.removeAcl(acl.subject, acl.object, acl.action)')
|
||||
i.fa.fa-trash
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
import uiEvent from 'angular-ui-event'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('settings.groups', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
uiEvent,
|
||||
|
||||
xoApi,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings.groups', {
|
||||
controller: 'SettingsGroups as ctrl',
|
||||
url: '/groups',
|
||||
resolve: {
|
||||
users (xo) {
|
||||
return xo.user.getAll()
|
||||
},
|
||||
groups (xo) {
|
||||
return xo.group.getAll()
|
||||
}
|
||||
},
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsGroups', function ($scope, $interval, users, groups, xoApi, xo) {
|
||||
this.uiCollapse = Object.create(null)
|
||||
this.addedUsers = []
|
||||
|
||||
this.users = users
|
||||
this.userEmails = Object.create(null)
|
||||
this.users.forEach(user => {
|
||||
this.userEmails[user.id] = user.email
|
||||
})
|
||||
this.groups = groups
|
||||
|
||||
const selectedGroups = this.selectedGroups = {}
|
||||
this.newGroups = []
|
||||
|
||||
const refreshUsers = () => {
|
||||
xo.user.getAll().then(users => {
|
||||
this.users = users
|
||||
this.userEmails = Object.create(null)
|
||||
this.users.forEach(user => {
|
||||
this.userEmails[user.id] = user.email
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const refreshGroups = () => {
|
||||
let editing = this._editingGroup
|
||||
for (let groupId in this.uiCollapse) {
|
||||
editing = editing || this.uiCollapse[groupId]
|
||||
}
|
||||
if (!editing) {
|
||||
xo.group.getAll().then(groups => this.groups = groups)
|
||||
}
|
||||
}
|
||||
|
||||
const interval = $interval(() => {
|
||||
refreshUsers()
|
||||
refreshGroups()
|
||||
}, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
this.addGroup = () => {
|
||||
this.newGroups.push({
|
||||
// Fake (unique) id needed by Angular.JS
|
||||
id: Math.random()
|
||||
})
|
||||
}
|
||||
|
||||
this.addGroup()
|
||||
|
||||
this.saveGroups = () => {
|
||||
const newGroups = this.newGroups
|
||||
const groups = this.groups
|
||||
const updateGroups = []
|
||||
|
||||
for (let i = 0, len = groups.length; i < len; i++) {
|
||||
const group = groups[i]
|
||||
const {id} = group
|
||||
if (selectedGroups[id]) {
|
||||
delete selectedGroups[id]
|
||||
xo.group.delete(id)
|
||||
} else {
|
||||
xo.group.set(group)
|
||||
updateGroups.push(group)
|
||||
}
|
||||
}
|
||||
for (let i = 0, len = newGroups.length; i < len; i++) {
|
||||
const group = newGroups[i]
|
||||
const {name} = group
|
||||
if (!name) {
|
||||
continue
|
||||
}
|
||||
xo.group.create({name})
|
||||
.then(function (id) {
|
||||
group.id = id
|
||||
group.users = []
|
||||
})
|
||||
updateGroups.push(group)
|
||||
}
|
||||
|
||||
this.groups = updateGroups
|
||||
this.newGroups.length = 0
|
||||
this.addGroup()
|
||||
}
|
||||
|
||||
this.addUserToGroup = (group, index) => {
|
||||
group.users.push(this.addedUsers[index].id)
|
||||
delete this.addedUsers[index]
|
||||
}
|
||||
|
||||
this.flagUserRemoval = (group, index, remove) => {
|
||||
group.removals || (group.removals = {})
|
||||
group.removals[group.users[index]] = remove
|
||||
}
|
||||
|
||||
this.saveGroup = (group) => {
|
||||
const users = []
|
||||
group.users.forEach(user => {
|
||||
let remove = group.removals && group.removals[user]
|
||||
if (!remove) {
|
||||
users.push(user)
|
||||
}
|
||||
})
|
||||
group.removals && delete group.removals
|
||||
xo.group.setUsers(group.id, users)
|
||||
.then(() => {
|
||||
group.users = users
|
||||
this.uiCollapse[group.id] = false
|
||||
})
|
||||
}
|
||||
|
||||
this.editingGroup = editing => {
|
||||
this._editingGroup = editing
|
||||
}
|
||||
})
|
||||
.filter('notInGroup', function () {
|
||||
return function (users, group) {
|
||||
const filtered = []
|
||||
users.forEach(user => {
|
||||
if (!group.users || group.users.indexOf(user.id) === -1) {
|
||||
filtered.push(user)
|
||||
}
|
||||
})
|
||||
return filtered
|
||||
}
|
||||
})
|
||||
.name
|
||||
@@ -1,63 +0,0 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-users(style="color: #e25440;")
|
||||
| Groups
|
||||
.grid
|
||||
.panel.panel-default
|
||||
form(ng-submit="ctrl.saveGroups()", autocomplete="off").panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th.col-md-5 Name
|
||||
th.col-md-5 Manage
|
||||
th.col-md-2.text-center
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove group")
|
||||
tr(ng-repeat="group in ctrl.groups | orderBy:natural('id') track by group.id")
|
||||
td
|
||||
input.form-control(type="text", ng-model="group.name", ui-event = '{focus: "ctrl.editingGroup(true)", blur: "ctrl.editingGroup(false)"}')
|
||||
td
|
||||
button.btn.btn-info(type = 'button', ng-click = 'ctrl.uiCollapse[group.id] = !ctrl.uiCollapse[group.id]')
|
||||
| Users
|
||||
span.badge {{ group.users.length || "" }}
|
||||
|
|
||||
i.fa(ng-class = '{"fa-chevron-down": !ctrl.uiCollapse[$index], "fa-chevron-up": ctrl.uiCollapse[$index]}')
|
||||
br
|
||||
br
|
||||
div(collapse = '!ctrl.uiCollapse[group.id]')
|
||||
ul.list-group(ng-if = '!group.users.length')
|
||||
li.list-group-item.disabled: em (empty)
|
||||
ul.list-group(ng-if = 'group.users.length')
|
||||
li.list-group-item(ng-repeat = 'user in group.users')
|
||||
span(ng-if = '!removeUser') {{ ctrl.userEmails[user] }}
|
||||
del(ng-if = 'removeUser') {{ ctrl.userEmails[user] }}
|
||||
span.pull-right
|
||||
label
|
||||
input.hidden(type = 'checkbox', ng-model = 'removeUser', ng-change = 'ctrl.flagUserRemoval($parent.group, $index, removeUser)')
|
||||
|
|
||||
i.fa.fa-trash-o(tooltip="Remove user from group")
|
||||
p.form-inline
|
||||
select.form-control(ng-model = 'ctrl.addedUsers[$index]', ng-options = 'addedUser.email for addedUser in ctrl.users | notInGroup:group | orderBy:natural("email") track by addedUser.id')
|
||||
|
|
||||
button.btn.btn-success.btn-sm(type="button", ng-click="ctrl.addUserToGroup(group, $index)")
|
||||
i.fa.fa-plus
|
||||
button.btn.btn-info(type="button", ng-click = 'ctrl.saveGroup(group)')
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
td.text-center
|
||||
input(type="checkbox", ng-model="ctrl.selectedGroups[group.id]")
|
||||
tr(ng-repeat="group in ctrl.newGroups")
|
||||
td
|
||||
input.form-control(
|
||||
type = "text"
|
||||
ng-model = "group.name"
|
||||
placeholder = "name"
|
||||
)
|
||||
td  
|
||||
td  
|
||||
p.text-center
|
||||
button.btn.btn-primary(type="submit")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-success(type="button", ng-click="ctrl.addGroup()")
|
||||
i.fa.fa-plus
|
||||
@@ -1,37 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import acls from './acls'
|
||||
import groups from './groups'
|
||||
import servers from './servers'
|
||||
import update from './update'
|
||||
import users from './users'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('settings', [
|
||||
uiRouter,
|
||||
|
||||
acls,
|
||||
groups,
|
||||
servers,
|
||||
update,
|
||||
users
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings', {
|
||||
abstract: true,
|
||||
template: view,
|
||||
url: '/settings'
|
||||
})
|
||||
|
||||
// Redirect to default sub-state.
|
||||
$stateProvider.state('settings.index', {
|
||||
url: '',
|
||||
controller: function ($state) {
|
||||
$state.go('settings.servers')
|
||||
}
|
||||
})
|
||||
})
|
||||
.name
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('settings.servers', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
|
||||
xoApi,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings.servers', {
|
||||
controller: 'SettingsServers as ctrl',
|
||||
url: '/servers',
|
||||
resolve: {
|
||||
servers (xo) {
|
||||
return xo.server.getAll()
|
||||
}
|
||||
},
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsServers', function ($scope, $interval, servers, xoApi, xo, notify) {
|
||||
this.servers = servers
|
||||
const selected = this.selectedServers = {}
|
||||
const newServers = this.newServers = []
|
||||
|
||||
const refreshServers = () => {
|
||||
xo.server.getAll().then(servers => {
|
||||
this.servers = servers
|
||||
})
|
||||
}
|
||||
|
||||
const interval = $interval(refreshServers, 10e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
this.connectServer = (id) => {
|
||||
notify.info({
|
||||
title: 'Server connect',
|
||||
message: 'Connecting the server...'
|
||||
})
|
||||
xo.server.connect(id).catch(error => {
|
||||
notify.error({
|
||||
title: 'Server connection error',
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.disconnectServer = (id) => {
|
||||
notify.info({
|
||||
title: 'Server disconnect',
|
||||
message: 'Disconnecting the server...'
|
||||
})
|
||||
xo.server.disconnect(id)
|
||||
}
|
||||
|
||||
this.addServer = () => {
|
||||
newServers.push({
|
||||
// Fake (unique) id needed by Angular.JS
|
||||
id: Math.random(),
|
||||
status: 'connecting'
|
||||
})
|
||||
}
|
||||
|
||||
this.addServer()
|
||||
this.saveServers = () => {
|
||||
const newServers = this.newServers
|
||||
const servers = this.servers
|
||||
const updateServers = []
|
||||
|
||||
for (let i = 0, len = servers.length; i < len; i++) {
|
||||
const server = servers[i]
|
||||
const {id} = server
|
||||
if (selected[id]) {
|
||||
delete selected[id]
|
||||
xo.server.remove(id)
|
||||
} else {
|
||||
if (!server.password) {
|
||||
delete server.password
|
||||
}
|
||||
xo.server.set(server)
|
||||
delete server.password
|
||||
updateServers.push(server)
|
||||
}
|
||||
}
|
||||
for (let i = 0, len = newServers.length; i < len; i++) {
|
||||
const server = newServers[i]
|
||||
const {host, username, password} = server
|
||||
if (!host) {
|
||||
continue
|
||||
}
|
||||
xo.server.add({
|
||||
host,
|
||||
username,
|
||||
password,
|
||||
autoConnect: false
|
||||
}).then(function (id) {
|
||||
server.id = id
|
||||
xo.server.connect(id).catch(error => {
|
||||
notify.error({
|
||||
title: 'Server connection error',
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
})
|
||||
delete server.password
|
||||
updateServers.push(server)
|
||||
}
|
||||
this.servers = updateServers
|
||||
this.newServers.length = 0
|
||||
this.addServer()
|
||||
}
|
||||
})
|
||||
.name
|
||||
@@ -1,80 +0,0 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-cloud(style="color: #e25440;")
|
||||
| Servers
|
||||
.grid
|
||||
.panel.panel-default
|
||||
//- .panel-heading.panel-title
|
||||
//- i.fa.fa-cloud(style="color: #e25440;")
|
||||
//- | Connections
|
||||
form(ng-submit="ctrl.saveServers()", autocomplete="off").panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th.col-md-5 Host
|
||||
th.col-md-2 User
|
||||
th.col-md-3 Password
|
||||
th.col-md-1.text.center Actions
|
||||
th.col-md-1.text-center
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Forget server")
|
||||
tr(ng-repeat="server in ctrl.servers | orderBy:natural('host') track by server.id")
|
||||
td
|
||||
.input-group
|
||||
span.input-group-addon(ng-if="server.status === 'connected'")
|
||||
i.fa.fa-check-circle.fa-lg.text-success(tooltip="Connected")
|
||||
span.input-group-addon(ng-if="server.status === 'disconnected'")
|
||||
i.fa.fa-times-circle.fa-lg.text-danger(tooltip="Disconnected")
|
||||
span.input-group-addon(ng-if="server.status === 'connecting'")
|
||||
i.fa.fa-cog.fa-lg.fa-spin(tooltip="Connecting...")
|
||||
input.form-control(type="text", ng-model="server.host")
|
||||
td
|
||||
input.form-control(type="text", ng-model="server.username")
|
||||
td
|
||||
input.form-control(type="password", ng-model="server.password", placeholder="Fill to change the password")
|
||||
td.text-center
|
||||
button.btn.btn-default(
|
||||
ng-if="server.status === 'disconnected'",
|
||||
type="button",
|
||||
ng-click="ctrl.connectServer(server.id)",
|
||||
tooltip="Reconnect this server"
|
||||
)
|
||||
i.fa.fa-link
|
||||
button.btn.btn-danger(
|
||||
ng-if="server.status === 'connected'",
|
||||
type="button",
|
||||
ng-click="ctrl.disconnectServer(server.id)"
|
||||
tooltip="Disconnect this server"
|
||||
)
|
||||
i.fa.fa-unlink
|
||||
td.text-center
|
||||
input(type="checkbox", ng-model="ctrl.selectedServers[server.id]")
|
||||
tr(ng-repeat="server in ctrl.newServers")
|
||||
td
|
||||
input.form-control(
|
||||
type = "text"
|
||||
ng-model = "server.host"
|
||||
placeholder = "address[:port]"
|
||||
)
|
||||
td
|
||||
input.form-control(
|
||||
type = "text"
|
||||
ng-model = "server.username"
|
||||
ng-required = "server.host"
|
||||
placeholder = "user"
|
||||
)
|
||||
td
|
||||
input.form-control(
|
||||
type="password"
|
||||
ng-model="server.password"
|
||||
ng-required = "server.host"
|
||||
placeholder="password"
|
||||
)
|
||||
td  
|
||||
td  
|
||||
p.text-center
|
||||
button.btn.btn-primary(type="submit")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-success(type="button", ng-click="ctrl.addServer()")
|
||||
i.fa.fa-plus
|
||||
@@ -1,93 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import _assign from 'lodash.assign'
|
||||
import ansiUp from 'ansi_up'
|
||||
import updater from '../../updater'
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
import {AuthenticationFailed} from '../../updater'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('settings.update', [
|
||||
uiRouter,
|
||||
|
||||
updater,
|
||||
xoApi,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings.update', {
|
||||
controller: 'SettingsUpdate as ctrl',
|
||||
url: '/update',
|
||||
onExit: updater => {
|
||||
updater.removeAllListeners('end')
|
||||
},
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.filter('ansitohtml', function ($sce) {
|
||||
return function (input) {
|
||||
return $sce.trustAsHtml(ansiUp.ansi_to_html(input))
|
||||
}
|
||||
})
|
||||
.controller('SettingsUpdate', function (xoApi, xo, updater, notify) {
|
||||
this.updater = updater
|
||||
|
||||
this.updater.isRegistered()
|
||||
.then(() => this.updater.on('end', () => {
|
||||
if (this.updater.state === 'registerNeeded' && this.updater.registerState !== 'unregistered' && this.updater.registerState !== 'error') {
|
||||
this.updater.isRegistered()
|
||||
}
|
||||
}))
|
||||
.catch(err => console.error(err))
|
||||
|
||||
this.updater.getConfiguration()
|
||||
.then(configuration => this.configuration = _assign({}, configuration))
|
||||
.catch(error => notify.error({
|
||||
title: 'XOA Updater',
|
||||
message: error.message
|
||||
}))
|
||||
|
||||
this.registerXoa = (email, password) => {
|
||||
this.regPwd = ''
|
||||
this.updater.register(email, password)
|
||||
.then(() => this.updater.update())
|
||||
.catch(AuthenticationFailed, () => {})
|
||||
}
|
||||
|
||||
this.update = () => {
|
||||
this.updater.update()
|
||||
.catch(error => notify.error({
|
||||
title: 'XOA Updater',
|
||||
message: error.message
|
||||
}))
|
||||
}
|
||||
|
||||
this.upgrade = () => {
|
||||
this.updater.upgrade()
|
||||
.catch(error => notify.error({
|
||||
title: 'XOA Updater',
|
||||
message: error.message
|
||||
}))
|
||||
}
|
||||
|
||||
this.configure = (host, port) => {
|
||||
const config = {}
|
||||
config.proxyHost = host && host.trim() || null
|
||||
config.proxyPort = port && port.trim() || null
|
||||
return this.updater.configure(config)
|
||||
.then(configuration => this.configuration = _assign({}, configuration))
|
||||
.catch(error => notify.error({
|
||||
title: 'XOA Updater',
|
||||
message: error.message
|
||||
}))
|
||||
.finally(() => this.update())
|
||||
}
|
||||
|
||||
this.valid = trial => {
|
||||
return trial && trial.end && Date.now() < trial.end
|
||||
}
|
||||
})
|
||||
.name
|
||||
@@ -1,112 +0,0 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-refresh(style="color: #e25440;")
|
||||
| Update
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-globe(style="color: #e25440;")
|
||||
| Status
|
||||
.panel-body
|
||||
p(ng-if = '!ctrl.updater.state')
|
||||
a.btn.btn-warning: i.fa.fa-question-circle(ng-if = '!ctrl.updater.state', tooltip = 'No update information available')
|
||||
| No update information available
|
||||
a.btn.btn-default(ng-class = '{disabled: ctrl.updater.isConnected}', ng-click = 'ctrl.update()')
|
||||
i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.isConnected}')
|
||||
.form-group(ng-if = 'ctrl.updater.state && ctrl.updater.state === "registerNeeded"')
|
||||
a.btn.btn-warning(ng-if = 'ctrl.updater.state === "registerNeeded"'): i.fa.fa-bell-slash(tooltip = 'Your XOA is not registered for updates')
|
||||
| Registration needed
|
||||
button.btn.btn-default(ng-if = 'ctrl.updater.registerState === "registered"', ng-click = 'ctrl.updater.update()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}'): i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.updating || ctrl.updater.upgrading}')
|
||||
.form-group(ng-if = 'ctrl.updater.state && ctrl.updater.state !== "registerNeeded"')
|
||||
a.btn.btn-info(ng-if = 'ctrl.updater.state === "connected"'): i.fa.fa-question-circle(tooltip = 'Update information may be available')
|
||||
a.btn.btn-success(ng-if = 'ctrl.updater.state === "upToDate"'): i.fa.fa-check(tooltip = 'Your XOA is up-to-date')
|
||||
a.btn.btn-primary(ng-if = 'ctrl.updater.state === "upgradeNeeded"'): i.fa.fa-bell(tooltip = 'You need to update your XOA (new version is available)')
|
||||
a.btn.btn-danger(ng-if = 'ctrl.updater.state === "error"'): i.fa.fa-exclamation-triangle(tooltip = 'Can\'t fetch update information')
|
||||
|
|
||||
button#update.btn.btn-info(type = 'button', ng-click = 'ctrl.update()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}')
|
||||
| Check for updates
|
||||
i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.updating}')
|
||||
|
|
||||
button#upgrade.btn.btn-primary(ng-if = 'ctrl.updater.state === "upgradeNeeded"', type = 'button', ng-click = 'ctrl.upgrade()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}')
|
||||
| Upgrade
|
||||
i.fa.fa-cog(ng-class = '{"fa-spin": ctrl.updater.upgrading}')
|
||||
div
|
||||
p(ng-repeat = 'entry in ctrl.updater._log')
|
||||
span(ng-class = '{"text-danger": entry.level === "error", "text-muted": entry.level === "info", "text-warning": entry.level === "warning", "text-success": entry.level === "success"}') {{ entry.date }}
|
||||
| :
|
||||
span(ng-bind-html = 'entry.message | ansitohtml')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-pencil(style="color: #e25440;")
|
||||
| Registration
|
||||
.panel-body.text-center
|
||||
.text-warning(ng-if = '!ctrl.updater.state || ctrl.updater.registerState === "unknown"')
|
||||
| No registration information available.
|
||||
br
|
||||
span.big-stat
|
||||
i.fa.fa-exclamation-triangle.text-warning
|
||||
div(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "error"')
|
||||
.text-danger Can't fetch registration information.
|
||||
br
|
||||
span.big-stat
|
||||
i.fa.fa-exclamation-triangle.text-danger
|
||||
br
|
||||
.text-danger {{ ctrl.updater.registerError }}
|
||||
br
|
||||
button.btn.btn-default(type = 'button', ng-click = 'ctrl.updater.isRegistered()')
|
||||
i.fa.fa-refresh
|
||||
| Refresh
|
||||
form(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "unregistered"', ng-submit = 'ctrl.registerXoa(ctrl.regEmail, ctrl.regPwd)')
|
||||
p.form-static-control XOA is not registered.
|
||||
p.small Your xen-orchestra.com email and password
|
||||
.form-group
|
||||
.input-group
|
||||
span.input-group-addon: i.fa.fa-envelope-o.fa-fw
|
||||
label.sr-only(for = 'regEmail') Email
|
||||
input#regEmail.form-control(type = 'email', placeholder = 'Email', ng-model = 'ctrl.regEmail', required)
|
||||
.form-group
|
||||
.input-group
|
||||
span.input-group-addon: i.fa.fa-key.fa-fw
|
||||
label.sr-only(for = 'regPwd') Email
|
||||
input#regPwd.form-control(type = 'password', placeholder = 'Password', ng-model = 'ctrl.regPwd', required)
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
i.fa.fa-check
|
||||
| Register
|
||||
p.form-static-control.text-danger {{ ctrl.updater.registerError }}
|
||||
p(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "registered"')
|
||||
| Your Xen Orchestra appliance is registered to
|
||||
span.text-success {{ ctrl.updater.token.registrationEmail }}
|
||||
| .
|
||||
br
|
||||
span.big-stat
|
||||
i.fa.fa-check-circle.text-success
|
||||
.grid
|
||||
.grid-cell(ng-if = 'ctrl.updater.state && ctrl.configuration')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs(style="color: #e25440;")
|
||||
| Settings
|
||||
.panel-body
|
||||
form.form-inline(ng-submit = 'ctrl.configure(ctrl.configuration.proxyHost, ctrl.configuration.proxyPort)')
|
||||
fieldset
|
||||
h4
|
||||
i.fa.fa-globe
|
||||
| Proxy settings
|
||||
p If you need a proxy to access the Internet
|
||||
.form-group
|
||||
label.control-label Host:
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyHost', placeholder = 'myproxy.example.org')
|
||||
|
|
||||
|
|
||||
.form-group
|
||||
label.control-label Port:
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyPort', placeholder = '3128')
|
||||
|
|
||||
|
|
||||
|
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
i.fa.fa-floppy-o
|
||||
| Save
|
||||
@@ -1,122 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
import uiEvent from 'angular-ui-event'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('settings.users', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
uiEvent,
|
||||
|
||||
xoApi,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings.users', {
|
||||
controller: 'SettingsUsers as ctrl',
|
||||
url: '/users',
|
||||
resolve: {
|
||||
users (xo) {
|
||||
return xo.user.getAll()
|
||||
}
|
||||
},
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsUsers', function ($scope, $interval, users, xoApi, xo) {
|
||||
this.users = users
|
||||
this.permissions = [
|
||||
{
|
||||
label: 'User',
|
||||
value: 'none'
|
||||
},
|
||||
{
|
||||
label: 'Admin',
|
||||
value: 'admin'
|
||||
}
|
||||
]
|
||||
|
||||
const selected = this.selectedUsers = {}
|
||||
this.newUsers = []
|
||||
|
||||
const refreshUsers = () => {
|
||||
if (!this._editingUser) {
|
||||
xo.user.getAll().then(users => {
|
||||
this.users = users
|
||||
this.userEmails = Object.create(null)
|
||||
this.users.forEach(user => {
|
||||
this.userEmails[user.id] = user.email
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const interval = $interval(() => {
|
||||
refreshUsers()
|
||||
}, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
this.addUser = () => {
|
||||
this.newUsers.push({
|
||||
// Fake (unique) id needed by Angular.JS
|
||||
id: Math.random(),
|
||||
permission: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
this.addUser()
|
||||
|
||||
this.saveUsers = () => {
|
||||
const newUsers = this.newUsers
|
||||
const users = this.users
|
||||
const updateUsers = []
|
||||
|
||||
for (let i = 0, len = users.length; i < len; i++) {
|
||||
const user = users[i]
|
||||
const {id} = user
|
||||
if (selected[id]) {
|
||||
delete selected[id]
|
||||
xo.user.delete(id)
|
||||
} else {
|
||||
if (!user.password) {
|
||||
delete user.password
|
||||
}
|
||||
xo.user.set(user)
|
||||
delete user.password
|
||||
updateUsers.push(user)
|
||||
}
|
||||
}
|
||||
for (let i = 0, len = newUsers.length; i < len; i++) {
|
||||
const user = newUsers[i]
|
||||
const {email, permission, password} = user
|
||||
if (!email) {
|
||||
continue
|
||||
}
|
||||
xo.user.create({
|
||||
email,
|
||||
permission,
|
||||
password
|
||||
}).then(function (id) {
|
||||
user.id = id
|
||||
})
|
||||
delete user.password
|
||||
updateUsers.push(user)
|
||||
}
|
||||
this.users = updateUsers
|
||||
this.newUsers.length = 0
|
||||
this.userEmails = Object.create(null)
|
||||
this.users.forEach(user => {
|
||||
this.userEmails[user.id] = user.email
|
||||
})
|
||||
this.addUser()
|
||||
}
|
||||
})
|
||||
|
||||
.name
|
||||
@@ -1,55 +0,0 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-user(style="color: #e25440;")
|
||||
| Users
|
||||
.grid
|
||||
.panel.panel-default
|
||||
//- .panel-heading.panel-title
|
||||
//- i.fa.fa-users(style="color: #e25440;")
|
||||
//- | Users
|
||||
form(ng-submit="ctrl.saveUsers()", autocomplete="off").panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th.col-md-4 Email
|
||||
th.col-md-4 Permissions
|
||||
th.col-md-3 Password
|
||||
th.col-md-1.text-center
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove user")
|
||||
tr(ng-repeat="user in ctrl.users | orderBy:natural('id') track by user.id")
|
||||
td
|
||||
input.form-control(type="text", ng-model="user.email", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
|
||||
td
|
||||
select.form-control(ng-options="p.value as p.label for p in ctrl.permissions", ng-model="user.permission", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
|
||||
td
|
||||
input.form-control(type="password", ng-model="user.password", placeholder="Fill to change the password", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
|
||||
td.text-center
|
||||
input(type="checkbox", ng-model="ctrl.selectedUsers[user.id]")
|
||||
tr(ng-repeat="user in ctrl.newUsers")
|
||||
td
|
||||
input.form-control(
|
||||
type = "text"
|
||||
ng-model = "user.email"
|
||||
placeholder = "email"
|
||||
)
|
||||
td
|
||||
select.form-control(
|
||||
ng-options = "p.value as p.label for p in ctrl.permissions"
|
||||
ng-model = "user.permission"
|
||||
ng-required = "user.email"
|
||||
)
|
||||
td
|
||||
input.form-control(
|
||||
type = "password"
|
||||
ng-model = "user.password"
|
||||
ng-required = "user.email"
|
||||
placeholder = "password"
|
||||
)
|
||||
td  
|
||||
p.text-center
|
||||
button.btn.btn-primary(type="submit")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-success(type="button", ng-click="ctrl.addUser()")
|
||||
i.fa.fa-plus
|
||||
@@ -1,23 +0,0 @@
|
||||
.grid(style = 'height: 100%')
|
||||
//- Side menu
|
||||
.settings-menu
|
||||
ul.nav
|
||||
li
|
||||
a(ui-sref = '.servers', ui-sref-active = 'active')
|
||||
i.fa.fa-fw.fa-cloud.fa-menu
|
||||
span.menu-entry Servers
|
||||
a(ui-sref = '.users')
|
||||
i.fa.fa-fw.fa-user.fa-menu
|
||||
span.menu-entry Users
|
||||
a(ui-sref = '.groups')
|
||||
i.fa.fa-fw.fa-users.fa-menu
|
||||
span.menu-entry Groups
|
||||
a(ui-sref = '.acls')
|
||||
i.fa.fa-fw.fa-key.fa-menu
|
||||
span.menu-entry ACLs
|
||||
a(ui-sref = '.update')
|
||||
i.fa.fa-fw.fa-refresh.fa-menu
|
||||
span.menu-entry Update
|
||||
|
||||
//- Content
|
||||
div.settings-content(ui-view = '')
|
||||
@@ -1,192 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import isEmpty from 'lodash.isempty'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import Bluebird from 'bluebird'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.sr', [
|
||||
uiRouter
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('SRs_view', {
|
||||
url: '/srs/:id',
|
||||
controller: 'SrCtrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SrCtrl', function ($scope, $stateParams, $state, $q, notify, xoApi, xo, modal, $window, bytesToSizeFilter) {
|
||||
|
||||
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
|
||||
$scope.currentLogPage = 1
|
||||
$scope.currentVDIPage = 1
|
||||
|
||||
let {get} = xoApi
|
||||
$scope.$watch(() => xoApi.get($stateParams.id), function (SR) {
|
||||
$scope.SR = SR
|
||||
})
|
||||
|
||||
$scope.saveSR = function ($data) {
|
||||
let {SR} = $scope
|
||||
let {name_label, name_description} = $data
|
||||
|
||||
$data = {
|
||||
id: SR.id
|
||||
}
|
||||
if (name_label !== SR.name_label) {
|
||||
$data.name_label = name_label
|
||||
}
|
||||
if (name_description !== SR.name_description) {
|
||||
$data.name_description = name_description
|
||||
}
|
||||
|
||||
return xoApi.call('sr.set', $data)
|
||||
}
|
||||
|
||||
$scope.deleteVDI = function (id) {
|
||||
console.log('Delete VDI', id)
|
||||
|
||||
return modal.confirm({
|
||||
title: 'VDI deletion',
|
||||
message: 'Are you sure you want to delete this VDI? This operation is irreversible.'
|
||||
}).then(function () {
|
||||
return xo.vdi.delete(id)
|
||||
})
|
||||
}
|
||||
|
||||
$scope.disconnectVBD = function (id) {
|
||||
console.log('Disconnect VBD', id)
|
||||
|
||||
return xoApi.call('vbd.disconnect', {id: id})
|
||||
}
|
||||
|
||||
$scope.connectPBD = function (id) {
|
||||
console.log('Connect PBD', id)
|
||||
|
||||
return xoApi.call('pbd.connect', {id: id})
|
||||
}
|
||||
|
||||
$scope.disconnectPBD = function (id) {
|
||||
console.log('Disconnect PBD', id)
|
||||
|
||||
return xoApi.call('pbd.disconnect', {id: id})
|
||||
}
|
||||
|
||||
$scope.reconnectAllHosts = function () {
|
||||
// TODO: return a Bluebird.all(promises).
|
||||
for (let id of $scope.SR.$PBDs) {
|
||||
let pbd = xoApi.get(id)
|
||||
|
||||
xoApi.call('pbd.connect', {id: pbd.id})
|
||||
}
|
||||
}
|
||||
|
||||
$scope.disconnectAllHosts = function () {
|
||||
return modal.confirm({
|
||||
title: 'Disconnect hosts',
|
||||
message: 'Are you sure you want to disconnect all hosts to this SR?'
|
||||
}).then(function () {
|
||||
for (let id of $scope.SR.$PBDs) {
|
||||
let pbd = xoApi.get(id)
|
||||
|
||||
xoApi.call('pbd.disconnect', {id: pbd.id})
|
||||
console.log(pbd.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$scope.rescanSr = function (id) {
|
||||
console.log('Rescan SR', id)
|
||||
|
||||
return xoApi.call('sr.scan', {id: id})
|
||||
}
|
||||
|
||||
$scope.removeSR = function (id) {
|
||||
console.log('Remove SR', id)
|
||||
|
||||
return modal.confirm({
|
||||
title: 'SR deletion',
|
||||
message: 'Are you sure you want to delete this SR? This operation is irreversible.'
|
||||
}).then(function () {
|
||||
return Bluebird.map($scope.SR.$PBDs, pbdId => {
|
||||
let pbd = xoApi.get(pbdId)
|
||||
|
||||
return xoApi.call('pbd.disconnect', { id: pbd.id })
|
||||
})
|
||||
}).then(function () {
|
||||
return xoApi.call('sr.destroy', {id: id})
|
||||
}).then(function () {
|
||||
$state.go('index')
|
||||
notify.info({
|
||||
title: 'SR remove',
|
||||
message: 'SR is removed'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.forgetSR = function (id) {
|
||||
console.log('Forget SR', id)
|
||||
|
||||
return modal.confirm({
|
||||
title: 'SR forget',
|
||||
message: 'Are you sure you want to forget this SR? No VDI on this SR will be removed.'
|
||||
}).then(function () {
|
||||
return Bluebird.map($scope.SR.$PBDs, pbdId => {
|
||||
let pbd = xoApi.get(pbdId)
|
||||
|
||||
return xoApi.call('pbd.disconnect', { id: pbd.id })
|
||||
})
|
||||
}).then(function () {
|
||||
return xoApi.call('sr.forget', {id: id})
|
||||
}).then(function () {
|
||||
$state.go('index')
|
||||
notify.info({
|
||||
title: 'SR forget',
|
||||
message: 'SR is forgotten'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.saveDisks = function (data) {
|
||||
// Group data by disk.
|
||||
let disks = {}
|
||||
forEach(data, function (value, key) {
|
||||
let i = key.indexOf('/')
|
||||
|
||||
let id = key.slice(0, i)
|
||||
let prop = key.slice(i + 1)
|
||||
|
||||
;(disks[id] || (disks[id] = {}))[prop] = value
|
||||
})
|
||||
|
||||
let promises = []
|
||||
forEach(disks, function (attributes, id) {
|
||||
// Keep only changed attributes.
|
||||
let disk = get(id)
|
||||
|
||||
forEach(attributes, function (value, name) {
|
||||
if (value === disk[name]) {
|
||||
delete attributes[name]
|
||||
}
|
||||
})
|
||||
|
||||
if (!isEmpty(attributes)) {
|
||||
// Inject id.
|
||||
attributes.id = id
|
||||
|
||||
// Ask the server to update the object.
|
||||
promises.push(xoApi.call('vdi.set', attributes))
|
||||
}
|
||||
})
|
||||
|
||||
return $q.all(promises)
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
@@ -1,211 +0,0 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-sr
|
||||
| {{SR.name_label}}
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs(style="color: #e25440;")
|
||||
| General
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="srSettings.$show()")
|
||||
i.fa.fa-edit.fa-fw
|
||||
.panel-body
|
||||
form(editable-form="", name="srSettings", onbeforesave="saveSR($data)")
|
||||
dl.dl-horizontal
|
||||
dt Name
|
||||
dd
|
||||
span(editable-text="SR.name_label", e-name="name_label", e-form="srSettings")
|
||||
| {{SR.name_label}}
|
||||
dt Description
|
||||
dd
|
||||
span(editable-text="SR.name_description", e-name="name_description", e-form="srSettings")
|
||||
| {{SR.name_description}}
|
||||
dt Content type:
|
||||
dd {{SR.SR_type}}
|
||||
dt Tags
|
||||
dd(ng-if="SR.tags.length")
|
||||
span(ng-repeat="tag in SR.tags")
|
||||
span.label.label-primary {{tag}}
|
||||
dd(ng-if="!SR.tags.length")
|
||||
em No tags.
|
||||
dt Shared
|
||||
div(ng-repeat="container in [SR.$container] | resolve")
|
||||
dd(ng-if="'pool' === container.type")
|
||||
| Yes (
|
||||
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
|
||||
| )
|
||||
dd(ng-if="'host' === container.type") No
|
||||
dt Size
|
||||
dd {{SR.size | bytesToSize}}
|
||||
dt UUID
|
||||
dd {{SR.UUID}}
|
||||
.btn-form(ng-show="srSettings.$visible")
|
||||
p.center
|
||||
button.btn.btn-default(type="button", ng-disabled="srSettings.$waiting", ng-click="srSettings.$cancel()")
|
||||
i.fa.fa-times
|
||||
| Cancel
|
||||
|
|
||||
button.btn.btn-primary(type="submit", ng-disabled="srSettings.$waiting")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-stats(style="color: #e25440;")
|
||||
| Stats
|
||||
.panel-body
|
||||
.row
|
||||
.col-sm-6.col-lg-4
|
||||
p.stat-name Physical Alloc:
|
||||
canvas.stat-simple(id="doughnut", class="chart chart-doughnut", data="[(SR.physical_usage), (SR.size - SR.physical_usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
|
||||
.col-sm-6.col-lg-4
|
||||
p.stat-name Virtual Alloc:
|
||||
canvas.stat-simple(id="doughnut", class="chart chart-doughnut", data="[(SR.usage), (SR.size - SR.usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
|
||||
.col-sm-4.visible-lg
|
||||
p.stat-name VDIs:
|
||||
p.center.big-stat {{SR.VDIs.length}}
|
||||
.row.hidden-lg
|
||||
.col-sm-12
|
||||
br
|
||||
p.stat-name {{SR.VDIs.length}} VDIs
|
||||
//- Action panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flash(style="color: #e25440;")
|
||||
| Actions
|
||||
.panel-body.text-center
|
||||
.grid
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Rescan all the VDI", type="button", style="width: 90%", ng-click="rescanSr(SR.id)")
|
||||
i.fa.fa-refresh.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Reconnect all hosts", type="button", style="width: 90%", ng-click="reconnectAllHosts()")
|
||||
i.fa.fa-retweet.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Disconnect all hosts", type="button", style="width: 90%", xo-click="disconnectAllHosts()")
|
||||
i.fa.fa-power-off.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Forget SR", type="button", style="width: 90%", xo-click="forgetSR(SR.id)")
|
||||
i.fa.fa-2x.fa-fw.fa-ban
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Remove SR", type="button", style="width: 90%", xo-click="removeSR(SR.id)")
|
||||
i.fa.fa-2x.fa-trash-o
|
||||
//- TODO: Space panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-memory(style="color: #e25440;")
|
||||
| VDI Map
|
||||
.panel-body
|
||||
.progress
|
||||
.progress-bar.progress-bar-vm(ng-if="((VDI.size/SR.size)*100) > 0.5", ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.id", role="progressbar", aria-valuemin="0", aria-valuenow="{{VDI.size}}", aria-valuemax="{{SR.size}}", style="width: {{[VDI.size, SR.size] | percentage}}", tooltip="{{VDI.name_label}} ({{[VDI.size, SR.size] | percentage}})")
|
||||
//- display the name only if it fits in its progress bar
|
||||
span(ng-if="VDI.name_label.length < ((VDI.size/SR.size)*100)") {{VDI.name_label}}
|
||||
ul.list-inline.text-center
|
||||
li Total: {{SR.size | bytesToSize}}
|
||||
li Currently used: {{SR.usage | bytesToSize}}
|
||||
li Available: {{SR.size-SR.usage | bytesToSize}}
|
||||
//- TODO: VDIs.
|
||||
.grid
|
||||
form(name = "disksForm" editable-form = '' onbeforesave = 'saveDisks($data)').panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-hdd-o(style="color: #e25440;")
|
||||
| Virtual disks
|
||||
span.quick-edit(tooltip="Edit disks", ng-click="disksForm.$show()")
|
||||
i.fa.fa-edit.fa-fw
|
||||
span.quick-edit(tooltip="Rescan", ng-click="rescanSr(SR.id)")
|
||||
i.fa.fa-refresh.fa-fw
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th Name
|
||||
th Description
|
||||
th Size
|
||||
th Virtual Machine:
|
||||
tr(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') | slice:(10*(currentVDIPage-1)):(10*currentVDIPage)")
|
||||
td.oneliner
|
||||
span(
|
||||
editable-text="VDI.name_label"
|
||||
e-name = '{{VDI.id}}/name_label'
|
||||
)
|
||||
| {{VDI.name_label}}
|
||||
span.label.label-info(ng-if="VDI.$snapshot_of") snapshot
|
||||
td.oneliner
|
||||
span(
|
||||
editable-text="VDI.name_description"
|
||||
e-name = '{{VDI.id}}/name_description'
|
||||
)
|
||||
| {{VDI.name_description}}
|
||||
td
|
||||
//- FIXME: should be editable, but the server needs first
|
||||
//- to accept a human readable string.
|
||||
| {{VDI.size | bytesToSize}}
|
||||
td.oneliner {{((VDI.$VBD | resolve).VM | resolve).name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(ng-if="(VDI.$VBD | resolve).attached", xo-click="disconnectVBD(VBD.id)")
|
||||
i.fa.fa-unlink.fa-lg(tooltip="Disconnect this disk")
|
||||
a(ng-if="!(VDI.$VBD | resolve).attached", xo-click="deleteVDI(VDI.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this disk")
|
||||
//- TODO: Ability to create new VDIs.
|
||||
.center(ng-if = '(SR.VDIs | resolve).length > 10')
|
||||
pagination(boundary-links="true", total-items="(SR.VDIs | resolve).length", ng-model="$parent.currentVDIPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.btn-form(ng-show="disksForm.$visible")
|
||||
p.center
|
||||
button.btn.btn-default(
|
||||
type="reset"
|
||||
ng-disabled="disksForm.$waiting"
|
||||
ng-click="disksForm.$cancel()"
|
||||
)
|
||||
i.fa.fa-times
|
||||
| Cancel
|
||||
|
|
||||
button.btn.btn-primary(
|
||||
type="submit"
|
||||
ng-disabled="disksForm.$waiting"
|
||||
)
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
//- /VDIs.
|
||||
//- Hosts.
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-link(style="color: #e25440;")
|
||||
| Connected hosts
|
||||
span.quick-edit(tooltip="Reconnect all hosts", ng-click="reconnectAllHosts()")
|
||||
i.fa.fa-plus-square.fa-fw
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
th Name
|
||||
th Status
|
||||
tr(ng-repeat="PBD in SR.$PBDs | resolve", xo-sref="hosts_view({id: (PBD.host | resolve).id})")
|
||||
td {{(PBD.host | resolve).name_label}}
|
||||
td(ng-if="PBD.attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="disconnectPBD(PBD.id)")
|
||||
i.fa.fa-unlink.fa-lg(tooltip="Disconnect to this host")
|
||||
td(ng-if="!PBD.attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="connectPBD(PBD.id)")
|
||||
i.fa.fa-link.fa-lg(tooltip="Reconnect to this host")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments(style="color: #e25440;")
|
||||
| Logs
|
||||
.panel-body
|
||||
p.center(ng-if="SR.messages | isEmpty") No recent logs
|
||||
table.table.table-hover(ng-if="SR.messages | isNotEmpty")
|
||||
th.col-md-1 Date
|
||||
th.col-md-1 Name
|
||||
tr(ng-repeat="message in SR.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
a.quick-remove(tooltip="Remove log")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.center(ng-if = '(SR.messages | count) > 5')
|
||||
pagination(boundary-links="true", total-items="SR.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
//- /Hosts.
|
||||
@@ -1,256 +0,0 @@
|
||||
angular = require 'angular'
|
||||
forEach = require 'lodash.foreach'
|
||||
throttle = require 'lodash.throttle'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.tree', [
|
||||
require 'angular-file-upload'
|
||||
require 'angular-ui-router'
|
||||
|
||||
require 'xo-api'
|
||||
require 'xo-services'
|
||||
|
||||
require '../delete-vms'
|
||||
]
|
||||
.config ($stateProvider) ->
|
||||
$stateProvider.state 'tree',
|
||||
url: '/tree'
|
||||
controller: 'TreeCtrl'
|
||||
template: require './view'
|
||||
.controller 'TreeCtrl', (
|
||||
$scope
|
||||
$upload
|
||||
dateFilter
|
||||
deleteVmsModal
|
||||
modal
|
||||
notify
|
||||
xo
|
||||
xoApi
|
||||
) ->
|
||||
$scope.stats = xoApi.stats
|
||||
|
||||
$scope.hosts = xoApi.getView('hosts')
|
||||
$scope.hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
|
||||
$scope.pools = xoApi.getView('pools')
|
||||
|
||||
VMs = $scope.VMs = xoApi.getView('VM')
|
||||
$scope.runningVms = xoApi.getView('runningVms')
|
||||
$scope.runningVmsByPool = xoApi.getIndex('runningVmsByPool')
|
||||
$scope.vmsByPool = xoApi.getIndex('vmsByPool')
|
||||
$scope.vmsByContainer = xoApi.getIndex('vmsByContainer')
|
||||
$scope.vmControllersByContainer = xoApi.getIndex('vmControllersByContainer')
|
||||
|
||||
$scope.srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
|
||||
$scope.pool_disconnect = xo.pool.disconnect
|
||||
$scope.new_sr = xo.pool.new_sr
|
||||
|
||||
$scope.pool_addHost = (id) ->
|
||||
xo.host.attach id
|
||||
|
||||
$scope.enableHost = (id) ->
|
||||
xo.host.enable id
|
||||
notify.info {
|
||||
title: 'Host action'
|
||||
message: 'Host is enabled'
|
||||
}
|
||||
|
||||
$scope.disableHost = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Disable host'
|
||||
message: 'Are you sure you want to disable this host? In disabled state, no new VMs can be started and currently active VMs on the host continue to execute.'
|
||||
}).then ->
|
||||
xo.host.disable id
|
||||
.then ->
|
||||
notify.info {
|
||||
title: 'Host action'
|
||||
message: 'Host is disabled'
|
||||
}
|
||||
|
||||
$scope.pool_removeHost = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Remove host from pool'
|
||||
message: 'Are you sure you want to detach this host from its pool? It will be automatically rebooted'
|
||||
}).then ->
|
||||
xo.host.detach id
|
||||
|
||||
$scope.rebootHost = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Reboot host'
|
||||
message: 'Are you sure you want to reboot this host? It will be disabled then rebooted'
|
||||
}).then ->
|
||||
xo.host.restart id
|
||||
|
||||
$scope.restartToolStack = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Restart XAPI'
|
||||
message: 'Are you sure you want to restart the XAPI toolstack?'
|
||||
}).then ->
|
||||
xo.host.restartToolStack id
|
||||
|
||||
$scope.shutdownHost = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Shutdown host'
|
||||
message: 'Are you sure you want to shutdown this host?'
|
||||
}).then ->
|
||||
xo.host.stop id
|
||||
|
||||
$scope.startHost = (id) ->
|
||||
xo.host.start id
|
||||
|
||||
$scope.startVM = xo.vm.start
|
||||
$scope.stopVM = xo.vm.stop
|
||||
$scope.force_stopVM = (id) -> xo.vm.stop id, true
|
||||
$scope.rebootVM = xo.vm.restart
|
||||
$scope.force_rebootVM = (id) -> xo.vm.restart id, true
|
||||
$scope.suspendVM = (id) -> xo.vm.suspend id, true
|
||||
$scope.resumeVM = (id) -> xo.vm.resume id, true
|
||||
|
||||
$scope.migrateVM = (id, hostId) ->
|
||||
(xo.vm.migrate id, hostId).catch (error) ->
|
||||
modal.confirm
|
||||
title: 'VM migrate'
|
||||
message: 'This VM can\'t be migrated with Xen Motion to this host because they don\'t share any storage. Do you want to try a Xen Storage Motion?'
|
||||
|
||||
.then ->
|
||||
notify.info {
|
||||
title: 'VM migration'
|
||||
message: 'The migration process started'
|
||||
}
|
||||
|
||||
xo.vm.migratePool {
|
||||
id
|
||||
target_host_id: hostId
|
||||
}
|
||||
$scope.snapshotVM = (id) ->
|
||||
vm = xoApi.get(id)
|
||||
date = dateFilter Date.now(), 'yyyy-MM-ddTHH:mmZ'
|
||||
snapshot_name = "#{vm.name_label}_#{date}"
|
||||
xo.vm.createSnapshot id, snapshot_name
|
||||
# check if there is any operation pending on a VM
|
||||
$scope.isVMWorking = (VM) ->
|
||||
return true for _ of VM.current_operations
|
||||
false
|
||||
|
||||
$scope.deleteVMs = ->
|
||||
{selected_VMs} = $scope
|
||||
|
||||
deleteVmsModal (id for id, selected of selected_VMs when selected)
|
||||
|
||||
# VMs checkboxes.
|
||||
do ->
|
||||
# This map marks which VMs are selected.
|
||||
selected_VMs = $scope.selected_VMs = Object.create null
|
||||
|
||||
# Number of selected VMs.
|
||||
$scope.n_selected_VMs = 0
|
||||
|
||||
# This is the master checkbox.
|
||||
# Three states: true/false/null
|
||||
$scope.master_selection = false
|
||||
|
||||
# Wheter all VMs are selected.
|
||||
$scope.all = false
|
||||
|
||||
# Whether no VMs are selected.
|
||||
$scope.none = true
|
||||
|
||||
# Updates `all`, `none` and `master_selection` when necessary.
|
||||
$scope.$watch 'n_selected_VMs', (n) ->
|
||||
$scope.all = (VMs.size is n)
|
||||
$scope.none = (n is 0)
|
||||
|
||||
# When the master checkbox is clicked from indeterminate
|
||||
# state, it should go to unchecked like Gmail.
|
||||
$scope.master_selection = (n isnt 0)
|
||||
|
||||
make_matcher = (sieve) ->
|
||||
(item) ->
|
||||
for key, val of sieve
|
||||
return false unless item[key] is val
|
||||
true
|
||||
|
||||
$scope.selectVMs = (sieve) ->
|
||||
if (sieve is true) or (sieve is false)
|
||||
forEach(VMs.all, (VM) ->
|
||||
selected_VMs[VM.id] = sieve
|
||||
return
|
||||
)
|
||||
$scope.n_selected_VMs = if sieve then VMs.size else 0
|
||||
return
|
||||
|
||||
matcher = make_matcher sieve
|
||||
n = 0
|
||||
forEach(VMs.all, (VM) ->
|
||||
if (selected_VMs[VM.id] = matcher(VM))
|
||||
++n
|
||||
return
|
||||
)
|
||||
|
||||
$scope.n_selected_VMs = n
|
||||
|
||||
$scope.updateVMSelection = (id) ->
|
||||
if selected_VMs[id]
|
||||
++$scope.n_selected_VMs
|
||||
else
|
||||
--$scope.n_selected_VMs
|
||||
|
||||
$scope.bulkAction = (action, args...) ->
|
||||
fn = $scope[action]
|
||||
unless angular.isFunction fn
|
||||
throw new Error "invalid action #{action}"
|
||||
|
||||
for id, selected of selected_VMs
|
||||
fn id, args... if selected
|
||||
|
||||
# Unselects all VMs.
|
||||
$scope.selectVMs false
|
||||
|
||||
$scope.importVm = ($files, id) ->
|
||||
file = $files[0]
|
||||
notify.info {
|
||||
title: 'VM import started'
|
||||
message: "Starting the VM import"
|
||||
}
|
||||
|
||||
xo.vm.import id
|
||||
.then ({ $sendTo: url }) ->
|
||||
return $upload.http {
|
||||
method: 'POST'
|
||||
url
|
||||
data: file
|
||||
}
|
||||
.then (result) ->
|
||||
throw result.status if result.status isnt 200
|
||||
notify.info
|
||||
title: 'VM import'
|
||||
message: 'Success'
|
||||
|
||||
$scope.patchPool = ($files, id) ->
|
||||
file = $files[0]
|
||||
xo.pool.patch id
|
||||
.then ({ $sendTo: url }) ->
|
||||
return $upload.http {
|
||||
method: 'POST'
|
||||
url
|
||||
data: file
|
||||
}
|
||||
.progress throttle(
|
||||
(event) ->
|
||||
percentage = (100 * event.loaded / event.total)|0
|
||||
|
||||
notify.info
|
||||
title: 'Upload patch'
|
||||
message: "#{percentage}%"
|
||||
6e3
|
||||
)
|
||||
.then (result) ->
|
||||
throw result.status if result.status isnt 200
|
||||
notify.info
|
||||
title: 'Upload patch'
|
||||
message: 'Success'
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
@@ -1,412 +0,0 @@
|
||||
//- @todo Remove code duplication for the VMs listing by using a macro.
|
||||
.sub-bar
|
||||
.grid
|
||||
.grid-cell.overview
|
||||
//- Stats
|
||||
i(tooltip="{{pools.size}} pools").hidden-xs
|
||||
i.small {{pools.size}}x
|
||||
|
|
||||
i.xo-icon-pool
|
||||
|
|
||||
|
|
||||
i(tooltip="{{hosts.size}} hosts").hidden-xs
|
||||
i.small {{hosts.size}}x
|
||||
|
|
||||
i.xo-icon-host
|
||||
|
|
||||
|
|
||||
i(tooltip="{{runningVms.size}} of {{VMs.size}} VMs running")
|
||||
i.small {{runningVms.size}}x
|
||||
|
|
||||
i.xo-icon-vm
|
||||
|
|
||||
|
|
||||
i(tooltip="{{stats.$vCPUs}} vCPUs used of {{stats.$CPUs}} CPUs")
|
||||
i.small {{stats.$vCPUs}}x
|
||||
|
|
||||
i.xo-icon-cpu
|
||||
|
|
||||
|
|
||||
i(tooltip="{{stats.$memory.usage | bytesToSize}} RAM allocated of {{stats.$memory.size | bytesToSize}}")
|
||||
i.small {{stats.$memory.usage | bytesToSize}}
|
||||
|
|
||||
i.xo-icon-memory
|
||||
.grid-cell
|
||||
.btn-group.before-action-bar.dropdown(dropdown)
|
||||
a.btn.navbar-btn.btn-default.dropdown-toggle.inversed(dropdown-toggle)
|
||||
input.inverse(
|
||||
type="checkbox",
|
||||
ng-model="master_selection",
|
||||
ng-change="selectVMs(master_selection)",
|
||||
ui-indeterminate="!(all || none)", stop-event="click"
|
||||
)
|
||||
|
|
||||
i.fa.fa-caret-down
|
||||
ul.dropdown-menu.inverse(role="menu")
|
||||
li(ng-repeat="power_state in ['Halted', 'Running']")
|
||||
a(ng-click="selectVMs({power_state: power_state})")
|
||||
i.fa-fw(class="xo-icon-{{power_state | lowercase}}")
|
||||
| {{power_state}}
|
||||
li.divider
|
||||
li(
|
||||
ng-if="hosts.size"
|
||||
ng-repeat="host in hosts.all | map | orderBy:natural('name_label') track by host.id"
|
||||
)
|
||||
a(ng-click="selectVMs({$container: host.id})")
|
||||
i.xo-icon-host.fa-fw
|
||||
| On {{host.name_label}}
|
||||
.action-bar(ng-if="!none")
|
||||
|
|
||||
.btn-group
|
||||
button.btn.navbar-btn.btn-default.inversed(tooltip="Stop VM", type="button", ng-click="bulkAction('stopVM')")
|
||||
i.fa.fa-stop
|
||||
button.btn.navbar-btn.btn-default.inversed(tooltip="Start VM", type="button", ng-click="bulkAction('startVM')")
|
||||
i.fa.fa-play
|
||||
button.btn.navbar-btn.btn-default.inversed(tooltip="Reboot VM", type="button", ng-click="bulkAction('rebootVM')")
|
||||
i.fa.fa-refresh
|
||||
|
|
||||
.btn-group.dropdown(dropdown)
|
||||
button.btn.navbar-btn.btn-default.dropdown-toggle.inversed(
|
||||
dropdown-toggle
|
||||
tooltip="Migrate VM"
|
||||
type="button"
|
||||
)
|
||||
i.fa.fa-share
|
||||
|
|
||||
i.fa.fa-caret-down
|
||||
ul.dropdown-menu.inverse(role="menu")
|
||||
li(ng-repeat="host in hosts.all | map | orderBy:natural('name_label') track by host.id")
|
||||
a(ng-click="bulkAction('migrateVM', host.id)")
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{host.name_label}}
|
||||
|
|
||||
.btn-group.dropdown(dropdown)
|
||||
button.btn.navbar-btn.btn-default.dropdown-toggle.inversed(
|
||||
dropdown-toggle
|
||||
type="button"
|
||||
)
|
||||
| More
|
||||
|
|
||||
i.fa.fa-caret-down
|
||||
ul.dropdown-menu.inverse(role="menu")
|
||||
li
|
||||
a(ng-click="bulkAction('suspendVM')")
|
||||
i.fa.fa-pause.fa-fw
|
||||
| Suspend
|
||||
li
|
||||
a(ng-click="bulkAction('resumeVM')")
|
||||
i.fa.fa-play.fa-fw
|
||||
| Resume
|
||||
li
|
||||
a(ng-click="bulkAction('force_rebootVM')")
|
||||
i.fa.fa-bolt.fa-fw
|
||||
| Force reboot
|
||||
li
|
||||
a(ng-click="bulkAction('force_stopVM')")
|
||||
i.fa.fa-power-off.fa-fw
|
||||
| Force shutdown
|
||||
li.divider
|
||||
li
|
||||
a(ng-click="bulkAction('snapshotVM')")
|
||||
i.xo-icon-snapshot.fa-fw
|
||||
| Take a snapshot
|
||||
li
|
||||
a(ng-click="deleteVMs()")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
| Delete
|
||||
|
||||
//- FIXME: Ugly trick to force the pools to be under the sub bar.
|
||||
//- Add +7px to the 50px for having some space before the first pool.
|
||||
div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
|
||||
//- If we haven't any data
|
||||
div(ng-if="!pools.size")
|
||||
.grid
|
||||
.panel.panel-default.text-center
|
||||
h1 Welcome on Xen Orchestra!
|
||||
h3 It seems you aren't connected to any Xen server:
|
||||
br
|
||||
a.btn.btn-success.big(ui-sref="settings.index")
|
||||
i.fa.fa-plus-circle
|
||||
| Add server
|
||||
br
|
||||
br
|
||||
br
|
||||
p You can add a new host anytime by clicking on the menu icon "
|
||||
i.fa.fa-th
|
||||
| " and choose "
|
||||
i.fa.fa-cog
|
||||
| Settings"
|
||||
p Enjoy Xen Orchestra!
|
||||
//- If we have data
|
||||
div(ng-if="pools.size")
|
||||
//- Contains a pool and all its children (hosts).
|
||||
.grid.pool-block(
|
||||
ng-repeat="pool in pools.all | map | orderBy:[natural('name_label'), 'id'] track by pool.id"
|
||||
)
|
||||
//- Pseudo pool if it is not a named pool.
|
||||
//- .grid-cell.grid--gutters.pool-cell(ng-if="!pool.name_label")
|
||||
//- p.center(style="margin-top: 2em;") No pool connected
|
||||
//- Contains information about the pool if it is a named pool.
|
||||
.grid-cell.grid--gutters.pool-cell.hidden-xs
|
||||
//- Header (name + dropdown menu).
|
||||
.dropdown.dropdown-pool(dropdown)
|
||||
a.pool-name(ui-sref="pools_view({id: pool.id})")
|
||||
span(ng-if="pool.name_label")
|
||||
| {{pool.name_label}}
|
||||
span.text-muted(ng-if="!pool.name_label")
|
||||
| {{(pool.master | resolve).name_label}}
|
||||
a.dropdown-toggle(ng-if="pool.name_label", dropdown-toggle)
|
||||
|
|
||||
i.fa.fa-caret-down.big-caret
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
//- TODO: remove until handled this properly
|
||||
//- li
|
||||
//- a(xo-sref="SRs_new({container: pool.id})")
|
||||
//- i.xo-icon-sr.fa-fw
|
||||
//- | Add SR
|
||||
li
|
||||
a(xo-sref="VMs_new({container: pool.id})")
|
||||
i.xo-icon-vm.fa-fw
|
||||
| Create VM
|
||||
//- TODO: solve the "a" problem for ng-file-select
|
||||
li(ng-file-select="patchPool($files, pool.id)")
|
||||
a
|
||||
i.fa.fa-file-code-o.fa-fw
|
||||
| Patch
|
||||
li.divider
|
||||
li
|
||||
a.disabled(xo-click="pool_disconnect(pool.id)")
|
||||
i.fa.fa-unlink.fa-fw
|
||||
| Disconnect
|
||||
//- /Header.
|
||||
//- Stats & SRs list.
|
||||
div
|
||||
//- Stats.
|
||||
ul.list-unstyled.stats
|
||||
li
|
||||
i(tooltip="{{hostsByPool[pool.id] | count}} hosts connected")
|
||||
i.small {{hostsByPool[pool.id] | count}}x
|
||||
|
|
||||
i.xo-icon-host
|
||||
|
|
||||
|
|
||||
i(tooltip="{{runningVmsByPool[pool.id] | count}} of {{vmsByPool[pool.id] | count}} VMs running")
|
||||
i.small {{runningVmsByPool[pool.id] | count}}x
|
||||
|
|
||||
i.xo-icon-vm
|
||||
li(ng-if="pool.master")
|
||||
| Master:
|
||||
|
|
||||
a(ui-sref="hosts_view({id: (pool.master | resolve).id})") {{(pool.master | resolve).name_label}}
|
||||
//- /Stats.
|
||||
//- SRs.
|
||||
div(ng-if="!(srsByContainer[pool.id] | isEmpty)")
|
||||
p.center.small-caps SRs:
|
||||
table.table.table-hover.table-condensed
|
||||
tr(ng-repeat="SR in srsByContainer[pool.id] | map | orderBy:natural('name_label') track by SR.id", xo-sref="SRs_view({id: SR.id})")
|
||||
td.col-md-6.sr-name.no-border(ng-class="{'default-sr': SR.id === pool.default_SR}", title="{{SR.name_label}}")
|
||||
i.xo-icon-sr
|
||||
| {{SR.name_label}}
|
||||
td.col-md-6.right.no-border
|
||||
.progress.progress-small(tooltip="Disk: {{[SR.usage, SR.size] | percentage}} allocated")
|
||||
.progress-bar(role="progressbar", aria-valuenow="{{100*SR.usage/SR.size}}", aria-valuemin="0", aria-valuemax="100", style="width: {{[SR.usage, SR.size] | percentage}}")
|
||||
//- Contains all the hosts of this pool.
|
||||
.grid-cell.grid--gutters.hosts-vms-cells
|
||||
//- Contains a host and all its children (VMs).
|
||||
.grid(ng-repeat="host in hostsByPool[pool.id] | map | orderBy:natural('name_label') track by host.id")
|
||||
//- Contains information about the host.
|
||||
.grid-cell.grid--gutters.host-cell
|
||||
//- Header (name + dropdown menu).
|
||||
.dropdown.dropdown-pool(dropdown)
|
||||
a.host-name(ui-sref="hosts_view({id: host.id})")
|
||||
| {{host.name_label}}
|
||||
a.dropdown-toggle(dropdown-toggle)
|
||||
|
|
||||
i.fa.fa-caret-down
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li
|
||||
a(xo-sref="SRs_new({container: host.id})")
|
||||
i.xo-icon-sr.fa-fw
|
||||
| Add SR
|
||||
li
|
||||
a(xo-sref="VMs_new({container: host.id})")
|
||||
i.xo-icon-vm.fa-fw
|
||||
| Create VM
|
||||
//- TODO: solve the "a" problem for ng-file-select
|
||||
li(ng-file-select="importVm($files, host.id)")
|
||||
a
|
||||
i.fa.fa-upload.fa-fw
|
||||
| Import VM
|
||||
li.divider
|
||||
li
|
||||
a(ng-repeat="controller in [vmControllersByContainer[host.id]] track by controller.id", xo-sref="consoles_view({id: controller.id})")
|
||||
i.xo-icon-console.fa-fw
|
||||
| Console
|
||||
li(ng-if="!host.enabled")
|
||||
a(xo-click="enableHost(host.id)")
|
||||
i.fa.fa-check-circle.fa-fw
|
||||
| Enable
|
||||
li(ng-if="host.enabled")
|
||||
a(xo-click="disableHost(host.id)")
|
||||
i.fa.fa-times-circle.fa-fw
|
||||
| Disable
|
||||
li
|
||||
a(xo-click="rebootHost(host.id)")
|
||||
i.fa.fa-refresh.fa-fw
|
||||
| Reboot
|
||||
li(ng-if="host.power_state === 'Halted'")
|
||||
a(xo-click="startHost(host.id)")
|
||||
i.fa.fa-power-off.fa-fw
|
||||
| Start
|
||||
li(ng-if="host.power_state === 'Running'")
|
||||
a(xo-click="shutdownHost(host.id)")
|
||||
i.fa.fa-power-off.fa-fw
|
||||
| Shutdown
|
||||
li
|
||||
a(xo-click="restartToolStack(host.id)")
|
||||
i.fa.fa-retweet.fa-fw
|
||||
| Restart toolstack
|
||||
li(ng-if="pool.name_label")
|
||||
a(xo-click="pool_removeHost(host.id)")
|
||||
i.fa.fa-cloud-upload.fa-fw
|
||||
| Remove from pool
|
||||
li(ng-if="!pool.name_label")
|
||||
a(xo-click="pool_addHost(host.id)")
|
||||
i.fa.fa-cloud-download.fa-fw
|
||||
| Add to pool
|
||||
//- /Header.
|
||||
//- Stats.
|
||||
ul.list-unstyled.stats
|
||||
//- Warning icon if host is halted or disabled
|
||||
li.text-danger(ng-if="host.power_state === 'Halted'")
|
||||
i.fa.fa-warning
|
||||
| Halted
|
||||
li.text-warning(ng-if="!host.enabled && host.power_state === 'Running'")
|
||||
i.fa.fa-warning
|
||||
| Disabled
|
||||
//- Memory
|
||||
li(ng-if="host.power_state === 'Running' && host.enabled")
|
||||
i.xo-icon-memory.i-progress
|
||||
.progress.progress-small(tooltip="RAM: {{[host.memory.usage, host.memory.size] | percentage}} allocated")
|
||||
.progress-bar(role="progressbar", aria-valuenow="{{100*host.memory.usage/host.memory.size}}", aria-valuemin="0", aria-valuemax="100", style="width: {{[host.memory.usage, host.memory.size] | percentage}}")
|
||||
//- Host address
|
||||
li.text-muted.substats
|
||||
i.xo-icon-network
|
||||
| {{host.address}}
|
||||
//- Contains all the VMs of this host.
|
||||
.grid-cell.grid--gutters.vm-cell
|
||||
//- If no VMs, fill the space with a message.
|
||||
.vms-notice(ng-if="vmsByContainer[host.id] | isEmpty")
|
||||
//- | Host halted.
|
||||
p(ng-if="host.power_state === 'Halted'")
|
||||
| Host halted.
|
||||
div(ng-if="host.power_state === 'Running'")
|
||||
p(ng-if="!host.enabled")
|
||||
| Host disabled.
|
||||
p(ng-if="host.enabled")
|
||||
| No VMs on this host.
|
||||
//- /Message if no VMs.
|
||||
//- TODO: comment
|
||||
.table-responsive(ng-if="!(vmsByContainer[host.id] | isEmpty)")
|
||||
table.table.table-hover.table-condensed
|
||||
//- Contains a VM.
|
||||
tr(ng-repeat="VM in vmsByContainer[host.id] | map | orderBy:natural('name_label') track by VM.id", xo-sref="VMs_view({id: VM.id})")
|
||||
//- Handle used for drag & drop.
|
||||
td.grab
|
||||
//- Checkbox used for selection.
|
||||
td.select-vm
|
||||
input(type="checkbox", ng-model="selected_VMs[VM.id]", ng-change="updateVMSelection(VM.id)")
|
||||
//- Power state
|
||||
td.vm-power-state
|
||||
i.xo-icon-working(ng-if="isVMWorking(VM)", tooltip="{{VM.power_state}} and {{(VM.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{VM.power_state | lowercase}}",ng-if="!isVMWorking(VM)", tooltip="{{VM.power_state}}")
|
||||
//- VM name.
|
||||
td.vm-name.col-xs-8.col-sm-2.col-md-2
|
||||
p.vm {{VM.name_label}}
|
||||
//- Quick actions.
|
||||
td.vm-quick-buttons.col-md-2.hidden-xs
|
||||
.quick-buttons
|
||||
a(tooltip="Shutdown VM", xo-click="stopVM(VM.id)")
|
||||
i.fa.fa-stop
|
||||
a(tooltip="Start VM", xo-click="startVM(VM.id)")
|
||||
i.fa.fa-play
|
||||
a(tooltip="Reboot VM", xo-click="rebootVM(VM.id)")
|
||||
i.fa.fa-refresh
|
||||
a(tooltip="VM Console", xo-sref="consoles_view({id: VM.id})")
|
||||
i.xo-icon-console
|
||||
//- Description.
|
||||
td.vm-description.col-md-4.hidden-xs
|
||||
i(class="xo-icon-{{VM.os_version.distro | osFamily}}",ng-if="VM.os_version.distro", tooltip="{{VM.os_version.name}}")
|
||||
|
|
||||
i.fa.fa-fw(ng-if="!VM.os_version.distro")
|
||||
| {{VM.name_description}}
|
||||
//- Metrics.
|
||||
//- Memory
|
||||
td.vm-memory-stat.col-md-2.hidden-xs
|
||||
.cpu
|
||||
| {{VM.memory.size | bytesToSize}}
|
||||
i.xo-icon-docker.fa-fw(ng-if="VM.docker", tooltip="Docker enabled")
|
||||
i.fa.fa-fw(ng-if="VM.PV_drivers && !VM.docker")
|
||||
i.xo-icon-info.fa-fw(ng-if="!VM.PV_drivers", tooltip="Xen tools not installed")
|
||||
//- /Metrics.
|
||||
//- Address.
|
||||
td.text-muted.text-right.col-md-2.hidden-xs
|
||||
| {{VM.addresses["0/ip"]}}
|
||||
//- Contains a pseudo-host which contains all VMs not in any hosts.
|
||||
.grid(ng-if="!(vmsByPool[pool.id] | isEmpty)")
|
||||
//- This is where the information about a host would be displayed.
|
||||
.grid-cell.host-cell
|
||||
//- Contains all the VMs of this pool.
|
||||
.grid.grid-cell.vm-cell
|
||||
//- TODO: comment
|
||||
.table-responsive
|
||||
table.table.table-hover.table-condensed
|
||||
//- Contains a VM.
|
||||
tr(ng-repeat="VM in vmsByContainer[pool.id] | map | orderBy:natural('name_label') track by VM.id", xo-sref="VMs_view({id: VM.id})")
|
||||
//- Handle used for drag & drop.
|
||||
td.grab
|
||||
//- Checkbox used for selection.
|
||||
td.select-vm
|
||||
input(type="checkbox", ng-model="selected_VMs[VM.id]", ng-change="updateVMSelection(VM.id)")
|
||||
//- Power state
|
||||
td.vm-power-state
|
||||
i.xo-icon-working(ng-if="isVMWorking(VM)", tooltip="{{VM.power_state}} and {{(VM.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{VM.power_state | lowercase}}",ng-if="!isVMWorking(VM)", tooltip="{{VM.power_state}}")
|
||||
//- VM name.
|
||||
td.vm-name.col-xs-8.col-sm-2.col-md-2
|
||||
p.vm {{VM.name_label}}
|
||||
//- Quick actions.
|
||||
td.vm-quick-buttons.col-md-2.hidden-xs
|
||||
.quick-buttons
|
||||
a(tooltip="Shutdown VM", xo-click="stopVM(VM.id)")
|
||||
i.fa.fa-stop
|
||||
a(ng-if="VM.power_state == 'Suspended'", tooltip="Resume VM", xo-click="resumeVM(VM.id)")
|
||||
i.fa.fa-play
|
||||
a(ng-if="VM.power_state != 'Suspended'", tooltip="Start VM", xo-click="startVM(VM.id)")
|
||||
i.fa.fa-play
|
||||
a(tooltip="Reboot VM", xo-click="rebootVM(VM.id)")
|
||||
i.fa.fa-refresh
|
||||
a(tooltip="VM Console")
|
||||
i.xo-icon-console
|
||||
//- Description.
|
||||
td.vm-description.col-md-4.hidden-xs
|
||||
i(class="xo-icon-{{VM.os_version.distro | osFamily}}",ng-if="VM.os_version.distro", tooltip="{{VM.os_version.name}}")
|
||||
|
|
||||
i.fa.fa-fw(ng-if="!VM.os_version.distro")
|
||||
| {{VM.name_description}}
|
||||
//- Metrics.
|
||||
//- Memory
|
||||
td.vm-memory-stat.col-md-2.hidden-xs
|
||||
.cpu
|
||||
| {{VM.memory.size | bytesToSize}}
|
||||
i.xo-icon-docker.fa-fw(ng-if="VM.docker", tooltip="Docker enabled")
|
||||
i.fa.fa-fw(ng-if="VM.PV_drivers && !VM.docker")
|
||||
i.xo-icon-info.fa-fw(ng-if="!VM.PV_drivers", tooltip="Xen tools not installed")
|
||||
//- /Metrics.
|
||||
//- Address.
|
||||
td.text-muted.text-right.col-md-2.hidden-xs
|
||||
| {{VM.addresses["0/ip"]}}
|
||||
//- /Pseudo host containing VMs not on any hosts.
|
||||
//- /Hosts of this pool.
|
||||
//- /Pool with its children.
|
||||
@@ -1,343 +0,0 @@
|
||||
import * as format from '@julien-f/json-rpc/format'
|
||||
import angular from 'angular'
|
||||
import Bluebird from 'bluebird'
|
||||
import makeError from 'make-error'
|
||||
import parse from '@julien-f/json-rpc/parse'
|
||||
import WebSocket from 'ws'
|
||||
import {EventEmitter} from 'events'
|
||||
|
||||
const calls = {}
|
||||
|
||||
function jsonRpcCall (socket, method, params = {}) {
|
||||
const req = format.request(method, params)
|
||||
const reqId = req.id
|
||||
socket.send(JSON.stringify(req))
|
||||
let waiter = {}
|
||||
const promise = new Bluebird((resolve, reject) => {
|
||||
waiter.resolve = resolve
|
||||
waiter.reject = reject
|
||||
})
|
||||
calls[reqId] = waiter
|
||||
return promise
|
||||
}
|
||||
|
||||
function jsonRpcNotify (socket, method, params = {}) {
|
||||
return Bluebird.resolve(socket.send(JSON.stringify(format.notification(method, params))))
|
||||
}
|
||||
|
||||
function getCurrentUrl () {
|
||||
if (typeof window === 'undefined') {
|
||||
throw new Error('cannot get current URL')
|
||||
}
|
||||
return String(window.location)
|
||||
}
|
||||
|
||||
function adaptUrl (url, port = null) {
|
||||
const matches = /^http(s?):\/\/([^\/:]*(?::[^\/]*)?)(?:[^:]*)?$/.exec(url)
|
||||
if (!matches || !matches[2]) {
|
||||
throw new Error('current URL not recognized')
|
||||
}
|
||||
return 'ws' + matches[1] + '://' + matches[2] + '/api/updater'
|
||||
}
|
||||
|
||||
function blockXoaAccess (xoaState) {
|
||||
return xoaState.state === 'untrustedTrial'
|
||||
}
|
||||
|
||||
export const NotRegistered = makeError('NotRegistered')
|
||||
export const AuthenticationFailed = makeError('AuthenticationFailed')
|
||||
export default angular.module('updater', [
|
||||
// notify
|
||||
])
|
||||
.factory('updater', function ($interval, $timeout) {
|
||||
class Updater extends EventEmitter {
|
||||
constructor () {
|
||||
super()
|
||||
this._log = []
|
||||
this._lastRun = 0
|
||||
this._lowState = null
|
||||
this.state = null
|
||||
this.registerState = 'uknown'
|
||||
this.registerError = ''
|
||||
this._connection = null
|
||||
this.isConnected = false
|
||||
this.updating = false
|
||||
this.upgrading = false
|
||||
this.token = null
|
||||
}
|
||||
|
||||
update () {
|
||||
this.emit('updating')
|
||||
this.updating = true
|
||||
return this._update(false)
|
||||
}
|
||||
|
||||
upgrade () {
|
||||
this.emit('upgrading')
|
||||
this.upgrading = true
|
||||
return this._update(true)
|
||||
.return(true)
|
||||
}
|
||||
|
||||
_open () {
|
||||
if (this._connection) {
|
||||
return this._connection
|
||||
} else {
|
||||
this._connection = new Bluebird((resolve, reject) => {
|
||||
const socket = new WebSocket(adaptUrl(getCurrentUrl()))
|
||||
const middle = new EventEmitter()
|
||||
this.isConnected = true
|
||||
const timeout = $timeout(() => {
|
||||
middle.emit('reconnect_failed')
|
||||
}, 4000)
|
||||
socket.onmessage = ({data}) => {
|
||||
const message = parse(data)
|
||||
if (message.type === 'response' && message.id !== undefined) {
|
||||
if (calls[message.id]) {
|
||||
if (message.result) {
|
||||
calls[message.id].resolve(message.result)
|
||||
} else {
|
||||
calls[message.id].reject(message.error)
|
||||
}
|
||||
delete calls[message.id]
|
||||
}
|
||||
} else {
|
||||
middle.emit(message.method, message.params)
|
||||
}
|
||||
}
|
||||
socket.onclose = () => {
|
||||
middle.emit('disconnect')
|
||||
}
|
||||
middle.on('connected', ({message}) => {
|
||||
$timeout.cancel(timeout)
|
||||
this.log('success', message)
|
||||
this.state = 'connected'
|
||||
resolve(socket)
|
||||
if (!this.updating) {
|
||||
this.update()
|
||||
}
|
||||
this.emit('connected', message)
|
||||
})
|
||||
middle.on('print', ({content}) => {
|
||||
Array.isArray(content) || (content = [content])
|
||||
content.forEach(elem => this.log('info', elem))
|
||||
this.emit('print', content)
|
||||
})
|
||||
middle.on('end', end => {
|
||||
this._lowState = end
|
||||
switch (this._lowState.state) {
|
||||
case 'xoa-up-to-date':
|
||||
case 'xoa-upgraded':
|
||||
case 'updater-upgraded':
|
||||
this.state = 'upToDate'
|
||||
break
|
||||
case 'xoa-upgrade-needed':
|
||||
case 'updater-upgrade-needed':
|
||||
this.state = 'upgradeNeeded'
|
||||
break
|
||||
case 'register-needed':
|
||||
this.state = 'registerNeeded'
|
||||
break
|
||||
case 'error':
|
||||
this.state = 'error'
|
||||
break
|
||||
default:
|
||||
this.state = null
|
||||
}
|
||||
this.log(end.level, end.message)
|
||||
this._lastRun = Date.now()
|
||||
this.upgrading = this.updating = false
|
||||
this.emit('end', end)
|
||||
if (this._lowState.state === 'updater-upgraded') {
|
||||
this.update()
|
||||
}
|
||||
this.xoaState()
|
||||
})
|
||||
middle.on('warning', warning => {
|
||||
this.log('warning', warning.message)
|
||||
this.emit('warning', warning)
|
||||
})
|
||||
middle.on('server-error', error => {
|
||||
this.log('error', error.message)
|
||||
this._lowState = error
|
||||
this.state = 'error'
|
||||
this.upgrading = this.updating = false
|
||||
this.emit('error', error)
|
||||
})
|
||||
middle.on('disconnect', () => {
|
||||
this._lowState = null
|
||||
this.state = null
|
||||
this.upgrading = this.updating = false
|
||||
this.log('warning', 'Lost connection with xoa-updater')
|
||||
this.emit('disconnect')
|
||||
middle.emit('reconnect_failed') // No reconnecting attempts implemented so far
|
||||
})
|
||||
middle.on('reconnect_failed', () => {
|
||||
this.isConnected = false
|
||||
middle.removeAllListeners()
|
||||
socket.close()
|
||||
this._connection = null
|
||||
const message = 'xoa-updater could not be reached'
|
||||
this._xoaStateError({message})
|
||||
reject(new Error(message))
|
||||
this.log('error', message)
|
||||
this.emit('reconnect_failed')
|
||||
})
|
||||
})
|
||||
return this._connection
|
||||
}
|
||||
}
|
||||
|
||||
isRegistered () {
|
||||
return this._open()
|
||||
.then(socket => {
|
||||
return jsonRpcCall(socket, 'isRegistered')
|
||||
.then(token => {
|
||||
if (token.registrationToken === undefined) {
|
||||
throw new NotRegistered('Your Xen Orchestra Appliance is not registered')
|
||||
} else {
|
||||
this.registerState = 'registered'
|
||||
this.token = token
|
||||
return token
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(NotRegistered, () => this.registerState = 'unregistered')
|
||||
.catch(error => {
|
||||
this.registerError = error.message
|
||||
this.registerState = 'error'
|
||||
})
|
||||
}
|
||||
|
||||
register (email, password) {
|
||||
return this._open()
|
||||
.then(socket => {
|
||||
return jsonRpcCall(socket, 'register', {email, password})
|
||||
.then(token => {
|
||||
this.registerState = 'registered'
|
||||
this.token = token
|
||||
return token
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.code && error.code === 1) {
|
||||
this.registerError = 'Authentication failed'
|
||||
throw new AuthenticationFailed('Authentication failed')
|
||||
} else {
|
||||
this.registerError = error.message
|
||||
this.registerState = 'error'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
xoaState () {
|
||||
return this._open()
|
||||
.then(socket => {
|
||||
return jsonRpcCall(socket, 'xoaState')
|
||||
.then(state => {
|
||||
this._xoaState = state
|
||||
this._xoaStateTS = Date.now()
|
||||
return state
|
||||
})
|
||||
})
|
||||
.catch(error => this._xoaStateError(error))
|
||||
}
|
||||
|
||||
_xoaStateError (error) {
|
||||
this._xoaState = {
|
||||
state: 'ERROR',
|
||||
message: error.message
|
||||
}
|
||||
this._xoaStateTS = Date.now()
|
||||
return this._xoaState
|
||||
}
|
||||
|
||||
_update (upgrade = false) {
|
||||
return this._open()
|
||||
.tap(() => this.log('info', 'Start ' + (upgrade ? 'upgrading' : 'updating' + '...')))
|
||||
.then(socket => jsonRpcNotify(socket, 'update', {upgrade}))
|
||||
}
|
||||
|
||||
start () {
|
||||
if (!this._xoaState) {
|
||||
this.xoaState()
|
||||
}
|
||||
if (!this._interval) {
|
||||
this._interval = $interval(() => this.run(), 60 * 60 * 1000)
|
||||
return this.run()
|
||||
} else {
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
stop () {
|
||||
if (this._interval) {
|
||||
$interval.cancel(this._interval)
|
||||
delete this._interval
|
||||
}
|
||||
}
|
||||
|
||||
run () {
|
||||
if (Date.now() - this._lastRun < 24 * 60 * 60 * 1000) {
|
||||
return Bluebird.resolve()
|
||||
} else {
|
||||
return this.update()
|
||||
}
|
||||
}
|
||||
|
||||
isStarted () {
|
||||
return this._interval !== null
|
||||
}
|
||||
|
||||
log (level, message) {
|
||||
const date = new Date()
|
||||
this._log.unshift({
|
||||
date: date.toLocaleString(),
|
||||
level,
|
||||
message
|
||||
})
|
||||
while (this._log.length > 10) {
|
||||
this._log.pop()
|
||||
}
|
||||
}
|
||||
|
||||
getConfiguration () {
|
||||
return this._open()
|
||||
.then(socket => {
|
||||
return jsonRpcCall(socket, 'getConfiguration')
|
||||
.then(configuration => this._configuration = configuration)
|
||||
})
|
||||
}
|
||||
|
||||
configure (config) {
|
||||
return this._open()
|
||||
.then(socket => {
|
||||
return jsonRpcCall(socket, 'configure', config)
|
||||
.then(configuration => this._configuration = configuration)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return new Updater()
|
||||
})
|
||||
.run(function (updater, $rootScope, $state, xoApi) {
|
||||
updater.start()
|
||||
.catch(() => {})
|
||||
|
||||
$rootScope.$on('$stateChangeStart', function (event, state) {
|
||||
if (Date.now() - updater._xoaStateTS > (60 * 60 * 1000)) {
|
||||
updater.xoaState()
|
||||
}
|
||||
let {user} = xoApi
|
||||
let loggedIn = !!user
|
||||
if (!loggedIn || !updater._xoaState || state.name === 'settings.update') {
|
||||
return
|
||||
} else if (blockXoaAccess(updater._xoaState)) {
|
||||
event.preventDefault()
|
||||
$state.go('settings.update')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
.name
|
||||
@@ -1,766 +0,0 @@
|
||||
angular = require 'angular'
|
||||
forEach = require 'lodash.foreach'
|
||||
isEmpty = require 'lodash.isempty'
|
||||
_difference = require 'lodash.difference'
|
||||
_sortBy = require 'lodash.sortby'
|
||||
|
||||
isoDevice = require('../iso-device')
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.vm', [
|
||||
require 'angular-ui-router',
|
||||
require 'angular-ui-bootstrap'
|
||||
|
||||
isoDevice
|
||||
]
|
||||
.config ($stateProvider) ->
|
||||
$stateProvider.state 'VMs_view',
|
||||
url: '/vms/:id'
|
||||
controller: 'VmCtrl'
|
||||
template: require './view'
|
||||
.controller 'VmCtrl', (
|
||||
$scope, $state, $stateParams, $location, $q
|
||||
xoApi, xo
|
||||
sizeToBytesFilter, bytesToSizeFilter
|
||||
modal
|
||||
$window
|
||||
$timeout
|
||||
dateFilter
|
||||
notify
|
||||
) ->
|
||||
$window.bytesToSize = bytesToSizeFilter # FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
{get} = xoApi
|
||||
|
||||
merge = do ->
|
||||
push = Array::push.apply.bind Array::push
|
||||
(args...) ->
|
||||
result = []
|
||||
for arg in args
|
||||
push result, arg if arg?
|
||||
result
|
||||
|
||||
$scope.currentLogPage = 1
|
||||
$scope.currentSnapPage = 1
|
||||
$scope.currentPCIPage = 1
|
||||
$scope.currentGPUPage = 1
|
||||
|
||||
$scope.refreshStatControl = refreshStatControl = {
|
||||
baseStatInterval: 5000
|
||||
baseTimeOut: 10000
|
||||
period: null
|
||||
running: false
|
||||
attempt: 0
|
||||
|
||||
start: () ->
|
||||
return if this.running
|
||||
this.stop()
|
||||
this.running = true
|
||||
this._reset()
|
||||
$scope.$on('$destroy', () => this.stop())
|
||||
return this._trig(Date.now())
|
||||
_trig: (t1) ->
|
||||
if this.running
|
||||
timeoutSecurity = $timeout(
|
||||
() => this.stop(),
|
||||
this.baseTimeOut
|
||||
)
|
||||
return $scope.refreshStats($scope.VM.id)
|
||||
.then () => this._reset()
|
||||
.catch (err) =>
|
||||
if !this.running || this.attempt >= 2 || $scope.VM.power_state isnt 'Running' || $scope.isVMWorking($scope.VM)
|
||||
return this.stop()
|
||||
else
|
||||
this.attempt++
|
||||
.finally () =>
|
||||
$timeout.cancel(timeoutSecurity)
|
||||
if this.running
|
||||
t2 = Date.now()
|
||||
return this.period = $timeout(
|
||||
() => this._trig(t2),
|
||||
Math.max(this.baseStatInterval - (t2 - t1), 0)
|
||||
)
|
||||
_reset: () ->
|
||||
this.attempt = 0
|
||||
stop: () ->
|
||||
if this.period
|
||||
$timeout.cancel(this.period)
|
||||
this.running = false
|
||||
return
|
||||
}
|
||||
|
||||
$scope.hosts = xoApi.getView('hosts')
|
||||
|
||||
networksByPool = xoApi.getIndex('networksByPool')
|
||||
srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
|
||||
$scope.$watch(
|
||||
-> get $stateParams.id, 'VM'
|
||||
(VM) ->
|
||||
$scope.VM = VM
|
||||
|
||||
return unless VM?
|
||||
|
||||
# For the edition of this VM.
|
||||
$scope.memorySize = bytesToSizeFilter VM.memory.size
|
||||
|
||||
$scope.bootParams = parseBootParams($scope.VM.boot.order)
|
||||
|
||||
# build VDI list of this VM
|
||||
mountedIso = ''
|
||||
VDIs = []
|
||||
for VBD in VM.$VBDs
|
||||
oVbd = get VBD
|
||||
continue unless oVbd
|
||||
|
||||
oVdi = get oVbd.VDI
|
||||
continue unless oVdi
|
||||
|
||||
VDIs.push oVdi if oVdi and not oVbd.is_cd_drive
|
||||
if oVbd.is_cd_drive and oVdi # "Load" the cd drive
|
||||
mountedIso = oVdi.id
|
||||
|
||||
$scope.VDIs = _sortBy(VDIs, (value) -> (get resolveVBD(value))?.position);
|
||||
|
||||
container = get VM.$container
|
||||
|
||||
if container.type is 'host'
|
||||
host = container
|
||||
pool = (get container.$poolId) ? {}
|
||||
else
|
||||
host = {}
|
||||
pool = container
|
||||
|
||||
$scope.networks = networksByPool[pool.id]
|
||||
|
||||
# Computes the list of srs.
|
||||
SRs = $scope.SRs = []
|
||||
forEach(srsByContainer[host.id], (template) =>
|
||||
SRs.push(template)
|
||||
return
|
||||
)
|
||||
forEach(srsByContainer[pool.id], (template) =>
|
||||
SRs.push(template)
|
||||
return
|
||||
)
|
||||
# compute writable accessible SR from this VM
|
||||
$scope.writable_SRs = (SR for SR in SRs when SR.content_type isnt 'iso')
|
||||
|
||||
prepareDiskData mountedIso
|
||||
|
||||
if VM.power_state is 'Running' && !($scope.isVMWorking($scope.VM))
|
||||
refreshStatControl.start()
|
||||
else
|
||||
refreshStatControl.stop()
|
||||
)
|
||||
|
||||
descriptor = (obj) ->
|
||||
return obj.name_label + (if obj.name_description.length then ' - ' + obj.name_description else '')
|
||||
|
||||
prepareDiskData = (mounted) ->
|
||||
# For populating adding position choice
|
||||
unfreePositions = [];
|
||||
maxPos = 0;
|
||||
# build VDI list of this VM
|
||||
for VBD in $scope.VM.$VBDs
|
||||
oVbd = get VBD
|
||||
oVdi = get oVbd?.VDI
|
||||
if oVdi?
|
||||
unfreePositions.push parseInt oVbd.position
|
||||
maxPos = if (oVbd.position > maxPos) then parseInt oVbd.position else maxPos
|
||||
|
||||
# $scope.vdiFreePos = _difference([0..++maxPos], unfreePositions)
|
||||
$scope.maxPos = maxPos
|
||||
|
||||
$scope.VDIOpts = []
|
||||
ISOOpts = []
|
||||
for SR in $scope.SRs
|
||||
if 'iso' isnt SR.SR_type
|
||||
for rVdi in SR.VDIs
|
||||
oVdi = get rVdi
|
||||
|
||||
$scope.VDIOpts.push({
|
||||
sr: descriptor(SR),
|
||||
label: descriptor(oVdi),
|
||||
vdi: oVdi
|
||||
})
|
||||
else
|
||||
for rIso in SR.VDIs
|
||||
oIso = get rIso
|
||||
ISOOpts.push({
|
||||
sr: SR.name_label,
|
||||
label: descriptor(oIso),
|
||||
iso: oIso
|
||||
})
|
||||
|
||||
$scope.isoDeviceData = {
|
||||
opts: ISOOpts
|
||||
mounted
|
||||
}
|
||||
|
||||
parseBootParams = (params) ->
|
||||
texts = {
|
||||
c: 'Hard-Drive',
|
||||
d: 'DVD-Drive',
|
||||
n: 'Network'
|
||||
}
|
||||
bootParams = []
|
||||
i = 0
|
||||
if params
|
||||
while (i < params.length)
|
||||
char = params.charAt(i++)
|
||||
bootParams.push({
|
||||
e: char,
|
||||
t: texts[char],
|
||||
v: true
|
||||
})
|
||||
delete texts[char]
|
||||
for key, text of texts
|
||||
bootParams.push({
|
||||
e: key,
|
||||
t: text,
|
||||
v: false
|
||||
})
|
||||
return bootParams
|
||||
|
||||
$scope.bootMove = (index, move) ->
|
||||
tmp = $scope.bootParams[index + move]
|
||||
$scope.bootParams[index + move] = $scope.bootParams[index]
|
||||
$scope.bootParams[index] = tmp
|
||||
|
||||
$scope.saveBootParams = (id, bootParams) ->
|
||||
if $scope.savingBootOrder
|
||||
return
|
||||
$scope.savingBootOrder = true
|
||||
paramString = ''
|
||||
forEach(bootParams, (boot) -> boot.v && paramString += boot.e)
|
||||
return xoApi.call 'vm.bootOrder', {vm: id, order: paramString}
|
||||
.finally () ->
|
||||
$scope.savingBootOrder = false
|
||||
$scope.bootReordering = false
|
||||
|
||||
$scope.refreshStats = (id) ->
|
||||
return xo.vm.refreshStats id
|
||||
|
||||
.then (result) ->
|
||||
result.cpuSeries = []
|
||||
forEach result.cpus, (v,k) ->
|
||||
result.cpuSeries.push 'CPU ' + k
|
||||
return
|
||||
result.vifSeries = []
|
||||
forEach result.vifs, (v,k) ->
|
||||
result.vifSeries.push '#' + Math.floor(k/2) + ' ' + if k % 2 then 'out' else 'in'
|
||||
return
|
||||
result.xvdSeries = []
|
||||
forEach result.xvds, (v,k) ->
|
||||
# 97 is ascii code of 'a'
|
||||
result.xvdSeries.push 'xvd' + String.fromCharCode(Math.floor(k/2) + 97, ) + ' ' + if k % 2 then 'write' else 'read'
|
||||
return
|
||||
forEach result.date, (v,k) ->
|
||||
result.date[k] = new Date(v*1000).toLocaleTimeString()
|
||||
$scope.stats = result
|
||||
|
||||
$scope.startVM = (id) ->
|
||||
xo.vm.start id
|
||||
notify.info {
|
||||
title: 'VM starting...'
|
||||
message: 'Start VM'
|
||||
}
|
||||
|
||||
$scope.stopVM = (id) ->
|
||||
xo.vm.stop id
|
||||
notify.info {
|
||||
title: 'VM shutdown...'
|
||||
message: 'Gracefully shutdown the VM'
|
||||
}
|
||||
|
||||
$scope.force_stopVM = (id) ->
|
||||
xo.vm.stop id, true
|
||||
notify.info {
|
||||
title: 'VM force shutdown...'
|
||||
message: 'Force shutdown the VM'
|
||||
}
|
||||
|
||||
$scope.rebootVM = (id) ->
|
||||
xo.vm.restart id
|
||||
notify.info {
|
||||
title: 'VM reboot...'
|
||||
message: 'Gracefully reboot the VM'
|
||||
}
|
||||
|
||||
$scope.force_rebootVM = (id) ->
|
||||
xo.vm.restart id, true
|
||||
notify.info {
|
||||
title: 'VM reboot...'
|
||||
message: 'Force reboot the VM'
|
||||
}
|
||||
|
||||
$scope.suspendVM = (id) ->
|
||||
xo.vm.suspend id, true
|
||||
notify.info {
|
||||
title: 'VM suspend...'
|
||||
message: 'Suspend the VM'
|
||||
}
|
||||
|
||||
$scope.resumeVM = (id) ->
|
||||
xo.vm.resume id, true
|
||||
notify.info {
|
||||
title: 'VM resume...'
|
||||
message: 'Resume the VM'
|
||||
}
|
||||
|
||||
$scope.migrateVM = (id, hostId) ->
|
||||
(xo.vm.migrate id, hostId).catch (error) ->
|
||||
modal.confirm
|
||||
title: 'VM migrate'
|
||||
message: 'This VM can\'t be migrated with Xen Motion to this host because they don\'t share any storage. Do you want to try a Xen Storage Motion?'
|
||||
|
||||
.then ->
|
||||
notify.info {
|
||||
title: 'VM migration'
|
||||
message: 'The migration process started'
|
||||
}
|
||||
|
||||
xo.vm.migratePool {
|
||||
id
|
||||
target_host_id: hostId
|
||||
}
|
||||
|
||||
$scope.destroyVM = (id) ->
|
||||
modal.confirm
|
||||
title: 'VM deletion'
|
||||
message: 'Are you sure you want to delete this VM? (including its disks)'
|
||||
.then ->
|
||||
# FIXME: provides a way to not delete its disks.
|
||||
xo.vm.delete id, true
|
||||
.then ->
|
||||
$state.go 'index'
|
||||
notify.info {
|
||||
title: 'VM deletion'
|
||||
message: 'VM is removed'
|
||||
}
|
||||
|
||||
$scope.saveSnapshot = (id, $data) ->
|
||||
snapshot = get (id)
|
||||
|
||||
result = {
|
||||
id: snapshot.id
|
||||
name_label: $data
|
||||
}
|
||||
|
||||
if $data isnt snapshot.name_label
|
||||
result.name_label = $data
|
||||
|
||||
xoApi.call 'vm.set', result
|
||||
|
||||
$scope.saveVM = ($data) ->
|
||||
{VM} = $scope
|
||||
{CPUs, memory, name_label, name_description, high_availability, auto_poweron} = $data
|
||||
|
||||
$data = {
|
||||
id: VM.id
|
||||
}
|
||||
if memory isnt $scope.memorySize and (memory = sizeToBytesFilter memory)
|
||||
$data.memory = memory
|
||||
$scope.memorySize = bytesToSizeFilter memory
|
||||
if CPUs isnt VM.CPUs.number
|
||||
$data.CPUs = +CPUs
|
||||
if name_label isnt VM.name_label
|
||||
$data.name_label = name_label
|
||||
if name_description isnt VM.name_description
|
||||
$data.name_description = name_description
|
||||
if high_availability isnt VM.high_availability
|
||||
$data.high_availability = high_availability
|
||||
if auto_poweron isnt VM.auto_poweron
|
||||
$data.auto_poweron = auto_poweron
|
||||
|
||||
xoApi.call 'vm.set', $data
|
||||
|
||||
#-----------------------------------------------------------------
|
||||
# Disks
|
||||
#-----------------------------------------------------------------
|
||||
|
||||
# TODO: implement in XO-Server.
|
||||
$scope.moveDisk = (index, direction) ->
|
||||
{VDIs} = $scope
|
||||
|
||||
newIndex = index + direction
|
||||
[VDIs[index], VDIs[newIndex]] = [VDIs[newIndex], VDIs[index]]
|
||||
|
||||
return
|
||||
|
||||
migrateDisk = (id, sr_id) ->
|
||||
return modal.confirm({
|
||||
title: 'Disk migration'
|
||||
message: 'Are you sure you want to migrate (move) this disk to another SR?'
|
||||
}).then ->
|
||||
notify.info {
|
||||
title: 'Disk migration'
|
||||
message: 'Disk migration started'
|
||||
}
|
||||
xo.vdi.migrate id, sr_id
|
||||
return
|
||||
|
||||
$scope.saveDisks = (data) ->
|
||||
# Group data by disk.
|
||||
disks = {}
|
||||
forEach data, (value, key) ->
|
||||
i = key.indexOf '/'
|
||||
(disks[key.slice 0, i] ?= {})[key.slice i + 1] = value
|
||||
return
|
||||
|
||||
promises = []
|
||||
|
||||
# Handle SR change.
|
||||
forEach disks, (attributes, id) ->
|
||||
disk = get id
|
||||
if attributes.$SR isnt disk.$SR
|
||||
promises.push (migrateDisk id, attributes.$SR)
|
||||
|
||||
return
|
||||
|
||||
forEach disks, (attributes, id) ->
|
||||
# Keep only changed attributes.
|
||||
disk = get id
|
||||
forEach attributes, (value, name) ->
|
||||
delete attributes[name] if value is disk[name]
|
||||
return
|
||||
|
||||
unless isEmpty attributes
|
||||
# Inject id.
|
||||
attributes.id = id
|
||||
|
||||
# Ask the server to update the object.
|
||||
promises.push xoApi.call 'vdi.set', attributes
|
||||
return
|
||||
|
||||
# Handle Position changes
|
||||
vbds = xoApi.get($scope.VM.$VBDs)
|
||||
notFreePositions = Object.create(null)
|
||||
forEach vbds, (vbd) ->
|
||||
if vbd.is_cd_drive
|
||||
notFreePositions[vbd.position] = null
|
||||
|
||||
position = 0
|
||||
forEach $scope.VDIs, (vdi) ->
|
||||
oVbd = get(resolveVBD(vdi))
|
||||
unless oVbd
|
||||
return
|
||||
|
||||
while position of notFreePositions
|
||||
++position
|
||||
|
||||
if +oVbd.position isnt position
|
||||
promises.push(
|
||||
xoApi.call('vbd.set', {
|
||||
id: oVbd.id,
|
||||
position: String(position)
|
||||
})
|
||||
)
|
||||
|
||||
++position
|
||||
|
||||
return $q.all promises
|
||||
.catch (err) ->
|
||||
console.log(err);
|
||||
notify.error {
|
||||
title: 'saveDisks'
|
||||
message: err
|
||||
}
|
||||
|
||||
$scope.deleteDisk = (id) ->
|
||||
modal.confirm({
|
||||
title: 'Disk deletion'
|
||||
message: 'Are you sure you want to delete this disk? This operation is irreversible'
|
||||
}).then ->
|
||||
xoApi.call 'vdi.delete', {id: id}
|
||||
return
|
||||
return
|
||||
|
||||
#-----------------------------------------------------------------
|
||||
|
||||
# returns the id of the VBD that links the VDI to the VM
|
||||
$scope.resolveVBD = resolveVBD = (vdi) ->
|
||||
if not vdi?
|
||||
return
|
||||
for vbd in vdi.$VBDs
|
||||
rVbd = vbd if (get vbd).VM is $scope.VM.id
|
||||
return rVbd || null
|
||||
|
||||
$scope.disconnectVBD = (vdi) ->
|
||||
id = resolveVBD(vdi)
|
||||
if id?
|
||||
console.log "Disconnect VBD #{id}"
|
||||
xo.vbd.disconnect id
|
||||
|
||||
$scope.connectVBD = (vdi) ->
|
||||
id = resolveVBD(vdi)
|
||||
if id?
|
||||
console.log "Connect VBD #{id}"
|
||||
xo.vbd.connect id
|
||||
|
||||
$scope.deleteVBD = (vdi) ->
|
||||
id = resolveVBD(vdi)
|
||||
if id?
|
||||
console.log "Delete VBD #{id}"
|
||||
modal.confirm({
|
||||
title: 'VBD deletion'
|
||||
message: 'Are you sure you want to delete this VM disk attachment (the disk will NOT be destroyed)?'
|
||||
}).then ->
|
||||
xo.vbd.delete id
|
||||
|
||||
$scope.connectVIF = (id) ->
|
||||
console.log "Connect VIF #{id}"
|
||||
|
||||
xo.vif.connect id
|
||||
|
||||
$scope.disconnectVIF = (id) ->
|
||||
console.log "Disconnect VIF #{id}"
|
||||
|
||||
xo.vif.disconnect id
|
||||
|
||||
$scope.deleteVIF = (id) ->
|
||||
console.log "Delete VIF #{id}"
|
||||
modal.confirm({
|
||||
title: 'VIF deletion'
|
||||
message: 'Are you sure you want to delete this Virtual Interface (VIF)?'
|
||||
}).then ->
|
||||
xo.vif.delete id
|
||||
|
||||
$scope.cloneVM = (id, vm_name, full_copy) ->
|
||||
clone_name = "#{vm_name}_clone"
|
||||
console.log "Copy VM #{id} #{clone_name} with full copy at #{full_copy}"
|
||||
notify.info {
|
||||
title: 'Clone creation'
|
||||
message: 'Clone creation started'
|
||||
}
|
||||
xo.vm.clone id, clone_name, full_copy
|
||||
|
||||
$scope.snapshotVM = (id, vm_name) ->
|
||||
date = dateFilter Date.now(), 'yyyy-MM-ddTHH:mmZ'
|
||||
snapshot_name = "#{vm_name}_#{date}"
|
||||
console.log "Snapshot #{snapshot_name} from VM #{id}"
|
||||
notify.info {
|
||||
title: 'Snapshot creation'
|
||||
message: 'Snapshot creation started'
|
||||
}
|
||||
xo.vm.createSnapshot id, snapshot_name
|
||||
|
||||
$scope.exportVM = (id) ->
|
||||
console.log "Export VM #{id}"
|
||||
notify.info {
|
||||
title: 'VM export'
|
||||
message: 'VM export started'
|
||||
}
|
||||
xo.vm.export id
|
||||
.then ({$getFrom: url}) ->
|
||||
window.open url
|
||||
|
||||
$scope.convertVM = (id) ->
|
||||
console.log "Convert VM #{id}"
|
||||
modal.confirm({
|
||||
title: 'VM to template'
|
||||
message: 'Are you sure you want to convert this VM into a template?'
|
||||
}).then ->
|
||||
xo.vm.convert id
|
||||
|
||||
$scope.deleteSnapshot = (id) ->
|
||||
console.log "Delete snapshot #{id}"
|
||||
modal.confirm({
|
||||
title: 'Snapshot deletion'
|
||||
message: 'Are you sure you want to delete this snapshot? (including its disks)'
|
||||
}).then ->
|
||||
# FIXME: provides a way to not delete its disks.
|
||||
xo.vm.delete id, true
|
||||
|
||||
$scope.connectPci = (id, pciId) ->
|
||||
console.log "Connect PCI device "+pciId+" on VM "+id
|
||||
xo.vm.connectPci id, pciId
|
||||
|
||||
$scope.disconnectPci = (id) ->
|
||||
xo.vm.disconnectPci id
|
||||
|
||||
$scope.deleteAllLog = ->
|
||||
modal.confirm({
|
||||
title: 'Log deletion'
|
||||
message: 'Are you sure you want to delete all the logs?'
|
||||
}).then ->
|
||||
forEach($scope.VM.messages, (log) =>
|
||||
console.log "Remove log #{log.id}"
|
||||
xo.log.delete log.id
|
||||
return
|
||||
)
|
||||
|
||||
$scope.deleteLog = (id) ->
|
||||
console.log "Remove log #{id}"
|
||||
xo.log.delete id
|
||||
|
||||
$scope.revertSnapshot = (id) ->
|
||||
console.log "Revert snapshot to #{id}"
|
||||
modal.confirm({
|
||||
title: 'Revert to snapshot'
|
||||
message: 'Are you sure you want to revert your VM to this snapshot? The VM will be halted and this operation is irreversible'
|
||||
}).then ->
|
||||
notify.info {
|
||||
title: 'Reverting to snapshot'
|
||||
message: 'VM revert started'
|
||||
}
|
||||
xo.vm.revert id
|
||||
|
||||
$scope.isVMWorking = (VM) ->
|
||||
return false unless VM
|
||||
return true for _ of VM.current_operations
|
||||
false
|
||||
|
||||
$scope.startContainer = (VM,container) ->
|
||||
console.log "Start from VM "+VM+" to container "+container
|
||||
xo.docker.start VM, container
|
||||
|
||||
$scope.stopContainer = (VM,container) ->
|
||||
console.log "Stop from VM "+VM+" to container "+container
|
||||
xo.docker.stop VM, container
|
||||
|
||||
$scope.restartContainer = (VM,container) ->
|
||||
console.log "Restart from VM "+VM+" to container "+container
|
||||
xo.docker.restart VM, container
|
||||
|
||||
$scope.pauseContainer = (VM,container) ->
|
||||
console.log "Pause from VM "+VM+" to container "+container
|
||||
xo.docker.pause VM, container
|
||||
|
||||
$scope.resumeContainer = (VM,container) ->
|
||||
console.log "Unpause from VM "+VM+" to container "+container
|
||||
xo.docker.unpause VM, container
|
||||
|
||||
|
||||
$scope.addVdi = (vdi, readonly, bootable) ->
|
||||
|
||||
$scope.addWaiting = true # disables form fields
|
||||
position = $scope.maxPos + 1
|
||||
|
||||
params = {
|
||||
bootable
|
||||
mode : if (readonly || !isFreeForWriting(vdi)) then 'RO' else 'RW'
|
||||
position: String(position)
|
||||
vdi: vdi.id
|
||||
vm: $scope.VM.id
|
||||
}
|
||||
|
||||
console.log(params)
|
||||
return xoApi.call 'vm.attachDisk', params
|
||||
|
||||
.then -> $scope.adding = false # Closes form block
|
||||
|
||||
.catch (err) ->
|
||||
console.log(err);
|
||||
notify.error {
|
||||
title: 'vm.attachDisk'
|
||||
message: err
|
||||
}
|
||||
|
||||
.finally ->
|
||||
$scope.addWaiting = false
|
||||
|
||||
$scope.isConnected = isConnected = (vdi) -> (get resolveVBD(vdi))?.attached
|
||||
|
||||
$scope.isFreeForWriting = isFreeForWriting = (vdi) ->
|
||||
free = true
|
||||
for vbd in vdi.$VBDs
|
||||
oVbd = get vbd
|
||||
free = free && (!oVbd?.attached || oVbd?.read_only)
|
||||
return free
|
||||
|
||||
$scope.createVdi = (name, size, sr, bootable, readonly) ->
|
||||
|
||||
$scope.createVdiWaiting = true # disables form fields
|
||||
position = $scope.maxPos + 1
|
||||
|
||||
params = {
|
||||
name
|
||||
size: String(size)
|
||||
sr
|
||||
}
|
||||
|
||||
# console.log(params)
|
||||
return xoApi.call 'disk.create', params
|
||||
|
||||
.then (diskUuid) ->
|
||||
params = {
|
||||
bootable,
|
||||
mode: if readonly then 'RO' else 'RW'
|
||||
position: String(position)
|
||||
vdi: diskUuid
|
||||
vm: $scope.VM.id
|
||||
}
|
||||
|
||||
# console.log(params)
|
||||
return xoApi.call 'vm.attachDisk', params
|
||||
|
||||
.then -> $scope.creatingVdi = false # Closes form block
|
||||
|
||||
.catch (err) ->
|
||||
console.log(err);
|
||||
notify.error {
|
||||
title: 'Attach Disk'
|
||||
message: err
|
||||
}
|
||||
|
||||
.catch (err) ->
|
||||
console.log(err);
|
||||
notify.error {
|
||||
title: 'Create Disk'
|
||||
message: err
|
||||
}
|
||||
|
||||
.finally ->
|
||||
$scope.createVdiWaiting = false
|
||||
|
||||
$scope.updateMTU = (network) ->
|
||||
$scope.newInterfaceMTU = network.MTU
|
||||
|
||||
$scope.createInterface = (network, mtu, automac, mac) ->
|
||||
|
||||
$scope.createVifWaiting = true # disables form fields
|
||||
|
||||
position = 0
|
||||
forEach $scope.VM.VIFs, (vf) ->
|
||||
int = get vf
|
||||
position = if int?.device > position then (get vf)?.device else position
|
||||
|
||||
position++
|
||||
|
||||
params = {
|
||||
vm: $scope.VM.id
|
||||
network: network.id
|
||||
position: String(position) # TODO
|
||||
mtu: String(mtu) || String(network.mtu)
|
||||
}
|
||||
|
||||
if !automac
|
||||
params.mac = mac
|
||||
|
||||
# console.log(params)
|
||||
|
||||
return xoApi.call 'vm.createInterface', params
|
||||
.then (id) ->
|
||||
$scope.creatingVif = false
|
||||
# console.log(id)
|
||||
xoApi.call 'vif.connect', {id}
|
||||
.catch (err) ->
|
||||
console.log(err);
|
||||
notify.error {
|
||||
title: 'Create Interface'
|
||||
message: err
|
||||
}
|
||||
.finally ->
|
||||
$scope.createVifWaiting = false
|
||||
|
||||
$scope.statView = {
|
||||
cpuOnly: false,
|
||||
ramOnly: false,
|
||||
netOnly: false,
|
||||
diskOnly: false
|
||||
}
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
@@ -1,771 +0,0 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-vm(ng-if="isVMWorking(VM)", class="xo-color-pending", tooltip="{{VM.power_state}} and {{(VM.current_operations | map)[0]}}")
|
||||
i.xo-icon-vm(class="xo-color-{{VM.power_state | lowercase}}",ng-if="!isVMWorking(VM)", tooltip="{{VM.power_state}}")
|
||||
| {{VM.name_label}}
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs(style="color: #e25440;")
|
||||
| General
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="vmSettings.$show()")
|
||||
i.fa.fa-edit.fa-fw
|
||||
.panel-body
|
||||
form(editable-form="", name="vmSettings", onbeforesave="saveVM($data)")
|
||||
dl.dl-horizontal
|
||||
dt Name
|
||||
dd
|
||||
span(editable-text="VM.name_label", e-name="name_label", e-form="vmSettings")
|
||||
| {{VM.name_label}}
|
||||
dt Description
|
||||
dd
|
||||
span(editable-text="VM.name_description", e-name="name_description", e-form="vmSettings")
|
||||
| {{VM.name_description}}
|
||||
dt(ng-if="VM.power_state === 'Running' || VM.power_state === 'Paused'") Running on
|
||||
dt(ng-if="VM.power_state == 'Halted' || VM.power_state === 'Suspended'") Resident on
|
||||
dd(ng-repeat="container in [VM.$container] | resolve")
|
||||
span(ng-if = 'container.type === "host"')
|
||||
a(xo-sref="hosts_view({id: container.id})")
|
||||
| {{container.name_label}}
|
||||
small
|
||||
span(ng-if="(container.$poolId | resolve).name_label")
|
||||
| (
|
||||
a(ui-sref="pools_view({id: (container.$poolId | resolve).id})") {{(container.$poolId | resolve).name_label}}
|
||||
| )
|
||||
a(
|
||||
ng-if = 'container.type === "pool"'
|
||||
xo-sref="pools_view({id: container.id})"
|
||||
)
|
||||
| {{container.name_label}}
|
||||
dt Addresses
|
||||
dd(ng-if="!VM.addresses") -
|
||||
dd(ng-repeat="IP in VM.addresses") {{IP}}
|
||||
dt(ng-if="!(VM.$poolId | resolve).HA_enabled") Auto Power
|
||||
dd(ng-if="!(VM.$poolId | resolve).HA_enabled")
|
||||
span(
|
||||
editable-select="VM.auto_poweron"
|
||||
e-ng-options="ap.v as ap.t for ap in [{v: true, t:'Yes'}, {v: false, t:'No'}]"
|
||||
e-name="auto_poweron"
|
||||
e-form="vmSettings"
|
||||
)
|
||||
| {{VM.auto_poweron ? 'Yes' : 'No'}}
|
||||
dt(ng-if="(VM.$poolId | resolve).HA_enabled") HA
|
||||
dd(ng-if="(VM.$poolId | resolve).HA_enabled")
|
||||
span(
|
||||
editable-checkbox="VM.high_availability"
|
||||
e-name="high_availability"
|
||||
e-form="vmSettings"
|
||||
)
|
||||
| {{VM.high_availability}}
|
||||
dt vCPUs
|
||||
dd
|
||||
span(
|
||||
editable-text="VM.CPUs.number"
|
||||
e-name="CPUs"
|
||||
e-form="vmSettings"
|
||||
)
|
||||
| {{VM.CPUs.number}}
|
||||
dt RAM
|
||||
dd
|
||||
span(
|
||||
editable-text="memorySize"
|
||||
e-name="memory"
|
||||
e-form="vmSettings"
|
||||
)
|
||||
| {{memorySize}}
|
||||
dt UUID
|
||||
dd {{VM.UUID}}
|
||||
dt(ng-if="refreshStatControl.running && stats") Xen tools
|
||||
dd(ng-if="refreshStatControl.running && stats")
|
||||
span(ng-if="VM.PV_drivers", style="color:green;") Installed
|
||||
span(ng-if="!VM.PV_drivers") NOT installed
|
||||
dt(ng-if="refreshStatControl.running && stats && VM.os_version") OS
|
||||
dd(ng-if="refreshStatControl.running && stats && VM.os_version")
|
||||
| {{VM.os_version.name}} ({{VM.os_version.distro}})
|
||||
dt(ng-if="refreshStatControl.running && stats && VM.os_version") Kernel
|
||||
dd(ng-if="refreshStatControl.running && stats && VM.os_version")
|
||||
| {{VM.os_version.uname}}
|
||||
.btn-form(ng-show="vmSettings.$visible")
|
||||
p.center
|
||||
button.btn.btn-default(
|
||||
type="button"
|
||||
ng-disabled="vmSettings.$waiting"
|
||||
ng-click="vmSettings.$cancel()"
|
||||
)
|
||||
i.fa.fa-times
|
||||
| Cancel
|
||||
|
|
||||
button.btn.btn-primary(
|
||||
type="submit"
|
||||
ng-disabled="vmSettings.$waiting"
|
||||
)
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
|
||||
.panel.panel-default.panel-height.center
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-stats(style="color: #e25440;", xo-click="refreshStats(VM.id)")
|
||||
| Stats
|
||||
.panel-body-stats(ng-if="refreshStatControl.running && stats")
|
||||
div(ng-if="statView.cpuOnly", ng-click="statView.cpuOnly = false")
|
||||
p.stat-name
|
||||
i.fa.fa-tachometer
|
||||
| CPU usage
|
||||
canvas.chart.chart-line.chart-stat-full(
|
||||
id="bigCpu"
|
||||
data="stats.cpus"
|
||||
labels="stats.date"
|
||||
series="stats.cpuSeries"
|
||||
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
|
||||
legend="true"
|
||||
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= Math.round(10*value)/10 %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= Math.round(10*value)/10 %>", pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="statView.ramOnly", ng-click="statView.ramOnly = false")
|
||||
p.stat-name
|
||||
//- i.fa.fa-bar-chart
|
||||
i.fa.fa-tasks
|
||||
//- i.fa.fa-server
|
||||
| RAM usage
|
||||
canvas.chart.chart-line.chart-stat-full(
|
||||
id="bigRam"
|
||||
data="[stats.memoryUsed,stats.memory]"
|
||||
labels="stats.date"
|
||||
series="['Used RAM', 'Total RAM']"
|
||||
colours="['#ff0000', '#ffbbbb']"
|
||||
legend="true"
|
||||
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="statView.netOnly", ng-click="statView.netOnly = false")
|
||||
p.stat-name
|
||||
i.fa.fa-sitemap
|
||||
| Network I/O
|
||||
canvas.chart.chart-line.chart-stat-full(
|
||||
id="bigNet"
|
||||
data="stats.vifs"
|
||||
labels="stats.date"
|
||||
series="stats.vifSeries"
|
||||
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
|
||||
legend="true"
|
||||
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="statView.diskOnly", ng-click="statView.diskOnly = false")
|
||||
p.stat-name
|
||||
i.fa.fa-hdd-o
|
||||
| Disk I/O
|
||||
canvas.chart.chart-line.chart-stat-full(
|
||||
id="bigDisk"
|
||||
data="stats.xvds"
|
||||
labels="stats.date"
|
||||
series="stats.xvdSeries"
|
||||
colours="['#00dd00', '#77dd77', '#007700', '#33dd33', '#003300']"
|
||||
legend="true"
|
||||
options='{responsive: true, maintainAspectRatio: false, multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
|
||||
)
|
||||
div(ng-if="!statView.netOnly && !statView.diskOnly && !statView.cpuOnly && !statView.ramOnly")
|
||||
.row
|
||||
.col-sm-6(ng-click="statView.cpuOnly=true")
|
||||
p.stat-name
|
||||
i.fa.fa-tachometer
|
||||
| CPU usage
|
||||
canvas.chart.chart-line.chart-stat-preview(
|
||||
id="smallCpu"
|
||||
data="stats.cpus"
|
||||
labels="stats.date"
|
||||
series="stats.cpuSeries"
|
||||
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
|
||||
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1}"
|
||||
)
|
||||
.col-sm-6(ng-click="statView.ramOnly=true")
|
||||
p.stat-name
|
||||
//- i.fa.fa-bar-chart
|
||||
i.fa.fa-tasks
|
||||
//- i.fa.fa-server
|
||||
| RAM usage
|
||||
canvas.chart.chart-line.chart-stat-preview(
|
||||
id="smallRam"
|
||||
data="[stats.memoryUsed,stats.memory]"
|
||||
labels="stats.date"
|
||||
series="['Used RAM', 'Total RAM']"
|
||||
colours="['#ff0000', '#ffbbbb']"
|
||||
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
|
||||
)
|
||||
.row
|
||||
.col-sm-6(ng-click="statView.netOnly=true")
|
||||
p.stat-name
|
||||
i.fa.fa-sitemap
|
||||
| Network I/O
|
||||
canvas.chart.chart-line.chart-stat-preview(
|
||||
id="smallNet"
|
||||
data="stats.vifs"
|
||||
labels="stats.date"
|
||||
series="stats.vifSeries"
|
||||
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
|
||||
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
|
||||
)
|
||||
.col-sm-6(ng-click="statView.diskOnly=true")
|
||||
p.stat-name
|
||||
i.fa.fa-hdd-o
|
||||
| Disk I/O
|
||||
canvas.chart.chart-line.chart-stat-preview(
|
||||
id="smallDisk"
|
||||
data="stats.xvds"
|
||||
labels="stats.date"
|
||||
series="stats.xvdSeries"
|
||||
colours="['#00dd00', '#77dd77', '#007700', '#33dd33', '#003300']"
|
||||
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
|
||||
)
|
||||
.panel-body(ng-if="!refreshStatControl.running || !stats")
|
||||
.grid
|
||||
.grid-cell
|
||||
p.stat-name vCPUs
|
||||
p.center.big {{VM.CPUs.number}}
|
||||
.grid-cell
|
||||
p.stat-name RAM
|
||||
p.center.big {{VM.memory.size | bytesToSize}}
|
||||
.grid-cell
|
||||
p.stat-name Disks
|
||||
p.center.big {{VM.$VBDs.length || 0}}
|
||||
br
|
||||
p.center(ng-if="refreshStatControl.running")
|
||||
i.fa.fa-circle-o-notch.fa-spin.fa-2x
|
||||
| Fetching stats...
|
||||
.grid
|
||||
.grid-cell(ng-if="VM.os_version.distro")
|
||||
p.stat-name OS:
|
||||
p.center.big
|
||||
i(class="xo-icon-{{VM.os_version.distro | osFamily}}",tooltip="{{VM.os_version.name}}", style="color: black;")
|
||||
.grid-cell
|
||||
p.stat-name Xen tools:
|
||||
p.center
|
||||
span(ng-if="VM.PV_drivers", style="color:green;") Installed
|
||||
span(ng-if="!VM.PV_drivers") NOT installed
|
||||
|
||||
//- Action panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flash(style="color: #e25440;")
|
||||
| Actions
|
||||
.panel-body.text-center
|
||||
.grid
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Stop VM", type="button", style="width: 90%", xo-click="stopVM(VM.id)")
|
||||
i.fa.fa-stop.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running')")
|
||||
button.btn(tooltip="Suspend VM", type="button", style="width: 90%", xo-click="suspendVM(VM.id)")
|
||||
i.fa.fa-pause.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Suspended')")
|
||||
button.btn(tooltip="Resume VM", type="button", style="width: 90%", xo-click="resumeVM(VM.id)")
|
||||
i.fa.fa-play.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
|
||||
button.btn(tooltip="Start VM", type="button", style="width: 90%", xo-click="startVM(VM.id)")
|
||||
i.fa.fa-play.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Reboot VM", type="button", style="width: 90%", xo-click="rebootVM(VM.id)")
|
||||
i.fa.fa-refresh.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(
|
||||
ng-if="VM.power_state == ('Running' || 'Paused')"
|
||||
dropdown
|
||||
)
|
||||
button.btn.dropdown-toggle(
|
||||
dropdown-toggle
|
||||
tooltip="Migrate VM"
|
||||
type="button"
|
||||
style="width: 90%"
|
||||
)
|
||||
i.fa.fa-share.fa-2x.fa-fw
|
||||
<span class="caret"></span>
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li(ng-repeat="host in hosts.all | orderBy:natural('name_label') track by host.id")
|
||||
a(ng-click="migrateVM(VM.id, host.id)")
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{host.name_label}}
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Force Reboot", type="button", style="width: 90%", xo-click="force_rebootVM(VM.id)")
|
||||
i.fa.fa-flash.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Force Shutdown", type="button", style="width: 90%", xo-click="force_stopVM(VM.id)")
|
||||
i.fa.fa-power-off.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
|
||||
button.btn(tooltip="Delete VM", type="button", style="width: 90%", xo-click="destroyVM(VM.id)")
|
||||
i.fa.fa-trash-o.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(
|
||||
ng-if="VM.power_state == ('Halted')"
|
||||
dropdown
|
||||
)
|
||||
button.btn.dropdown-toggle(
|
||||
dropdown-toggle
|
||||
tooltip="Create a clone"
|
||||
style="width: 90%"
|
||||
type="button"
|
||||
)
|
||||
i.fa.fa-files-o.fa-2x.fa-fw
|
||||
<span class="caret"></span>
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li
|
||||
a(ng-click="cloneVM(VM.id,VM.name_label,false)")
|
||||
i.fa.fa-code-fork.fa-fw
|
||||
| Fast clone
|
||||
li
|
||||
a(ng-click="cloneVM(VM.id,VM.name_label,true)")
|
||||
i.xo-icon-sr.fa-fw
|
||||
| Full disk copy
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Convert to template", type="button", style="width: 90%", xo-click="convertVM(VM.id)")
|
||||
i.fa.fa-thumb-tack.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Create a snapshot", style="width: 90%", type="button", xo-click="snapshotVM(VM.id,VM.name_label)")
|
||||
i.xo-icon-snapshot.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Export the VM", style="width: 90%", type="button", xo-click="exportVM(VM.id)")
|
||||
i.fa.fa-download.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(style="margin-bottom: 0.5em")
|
||||
button.btn(tooltip="VM Console", type="button", style="width: 90%", xo-sref="consoles_view({id: VM.id})")
|
||||
i.xo-icon-console.fa-2x.fa-fw
|
||||
//- Docker Panel (if Docker VM)
|
||||
.grid(ng-if="VM.docker")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-ship(style="color: #e25440;")
|
||||
| Docker containers
|
||||
.panel-body
|
||||
p.text-center(ng-if="!VM.docker.process.item") No Docker container on this VM.
|
||||
table.table.table-hover(ng-if="VM.docker.process.item")
|
||||
tr
|
||||
th.col-sm-2 Name
|
||||
th.col-sm-6 Command
|
||||
th.col-sm-2 Created
|
||||
th.col-sm-2 Status
|
||||
tr(ng-repeat = "container in VM.docker.process.item")
|
||||
td.oneliner {{container.entry.names}}
|
||||
td.oneliner {{container.entry.command}}
|
||||
td.oneliner {{container.entry.created*1e3 | date:"medium"}}
|
||||
td(ng-if="container.entry.status === 'Up'")
|
||||
span.label.label-success Running
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(
|
||||
tooltip="Stop this container"
|
||||
xo-click="stopContainer(VM.id,container.entry.container)"
|
||||
)
|
||||
i.fa.fa-stop
|
||||
a(
|
||||
tooltip="Pause this container"
|
||||
xo-click="pauseContainer(VM.id,container.entry.container)"
|
||||
)
|
||||
i.fa.fa-pause
|
||||
a(
|
||||
tooltip="Restart this container"
|
||||
xo-click="restartContainer(VM.id,container.entry.container)"
|
||||
)
|
||||
i.fa.fa-refresh
|
||||
td(ng-if="container.entry.status === 'Up (Paused)'")
|
||||
span.label.label-info Paused
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(
|
||||
tooltip="Resume this container"
|
||||
xo-click="resumeContainer(VM.id,container.entry.container)"
|
||||
)
|
||||
i.fa.fa-play
|
||||
td(ng-if="container.entry.status !== 'Up' && container.entry.status !== 'Up (Paused)'")
|
||||
span.label.label-danger Halted
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(
|
||||
tooltip="Start this container"
|
||||
xo-click="startContainer(VM.id,container.entry.container)"
|
||||
)
|
||||
i.fa.fa-play
|
||||
//- Disk panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-sr(style="color: #e25440;")
|
||||
| Disk
|
||||
span.quick-edit(
|
||||
ng-if="!disksForm.$visible"
|
||||
tooltip="Edit disks"
|
||||
ng-click="disksForm.$show()"
|
||||
)
|
||||
i.fa.fa-edit.fa-fw
|
||||
span.quick-edit(
|
||||
ng-if="disksForm.$visible"
|
||||
tooltip="Cancel Edition"
|
||||
ng-click="disksForm.$cancel()"
|
||||
)
|
||||
i.fa.fa-undo.fa-fw
|
||||
.panel-body
|
||||
form(name = "disksForm", editable-form = '', onbeforesave = 'saveDisks($data)')
|
||||
table.table.table-hover
|
||||
tr
|
||||
th Name
|
||||
th Description
|
||||
th Size
|
||||
th SR
|
||||
th Status
|
||||
th(ng-show="disksForm.$visible")
|
||||
//- FIXME: ng-init seems to disrupt the implicit $watch.
|
||||
tr(ng-repeat = 'VDI in VDIs track by VDI.id')
|
||||
td.oneliner
|
||||
span(
|
||||
editable-text="VDI.name_label"
|
||||
e-name = '{{VDI.id}}/name_label'
|
||||
)
|
||||
| {{VDI.name_label}}
|
||||
td.oneliner
|
||||
span(
|
||||
editable-text="VDI.name_description"
|
||||
e-name = '{{VDI.id}}/name_description'
|
||||
)
|
||||
| {{VDI.name_description}}
|
||||
td
|
||||
//- FIXME: should be editable, but the server needs first
|
||||
//- to accept a human readable string.
|
||||
| {{VDI.size | bytesToSize}}
|
||||
td.oneliner
|
||||
span(
|
||||
editable-select="(VDI.$SR | resolve).id"
|
||||
e-ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in writable_SRs"
|
||||
e-name = '{{VDI.id}}/$SR'
|
||||
)
|
||||
//- Are SR editable? will trigger moving VDI to the new SR
|
||||
a(xo-sref="SRs_view({id: (VDI.$SR | resolve).id})")
|
||||
| {{(VDI.$SR | resolve).name_label}}
|
||||
td(ng-if="isConnected(VDI)")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(
|
||||
tooltip="Disconnect this disk"
|
||||
xo-click="disconnectVBD(VDI)"
|
||||
)
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!isConnected(VDI)")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(
|
||||
tooltip="Plug this disk"
|
||||
ng-if="VM.power_state == ('Running' || 'Paused')"
|
||||
xo-click="connectVBD(VDI)"
|
||||
)
|
||||
i.fa.fa-plug.fa-lg
|
||||
a(
|
||||
tooltip="Forget this disk"
|
||||
xo-click="deleteVBD(VDI)"
|
||||
)
|
||||
i.fa.fa-ban.fa-lg
|
||||
a(
|
||||
tooltip="Remove this disk"
|
||||
xo-click="deleteDisk(VDI.id)"
|
||||
)
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
td(ng-show="disksForm.$visible")
|
||||
.btn-group
|
||||
button.btn.btn-default(
|
||||
type="button"
|
||||
ng-click="moveDisk($index, -1)"
|
||||
ng-disabled="$first"
|
||||
title="Move this disk up"
|
||||
)
|
||||
i.fa.fa-chevron-up
|
||||
button.btn.btn-default(
|
||||
type="button"
|
||||
ng-click="moveDisk($index, 1)"
|
||||
ng-disabled="$last"
|
||||
title="Move this disk down"
|
||||
)
|
||||
i.fa.fa-chevron-down
|
||||
//- TODO: Ability to create new VDIs.
|
||||
.btn-form(ng-show="disksForm.$visible")
|
||||
p.center
|
||||
button.btn.btn-default(
|
||||
type="reset"
|
||||
ng-disabled="disksForm.$waiting"
|
||||
ng-click="disksForm.$cancel()"
|
||||
)
|
||||
i.fa.fa-times
|
||||
| Cancel
|
||||
|
|
||||
button.btn.btn-primary(
|
||||
type="submit"
|
||||
ng-disabled="disksForm.$waiting"
|
||||
)
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
.grid
|
||||
.col-md-4
|
||||
iso-device(ng-if = 'VM && isoDeviceData', vm = 'VM', isos = 'isoDeviceData')
|
||||
.col-md-8.text-right
|
||||
div
|
||||
button.btn(type="button", ng-class = '{"btn-success": adding, "btn-primary": !adding}', ng-disabled="disksForm.$waiting", ng-click="adding = !adding;creatingVdi = false;bootReordering = false")
|
||||
i.fa.fa-plus(ng-if = '!adding')
|
||||
i.fa.fa-minus(ng-if = 'adding')
|
||||
| Attach Disk
|
||||
|
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingVdi, "btn-primary": !creatingVdi}', ng-disabled="disksForm.$waiting", ng-click="creatingVdi = !creatingVdi;adding = false;bootReordering = false")
|
||||
i.fa.fa-plus(ng-if = '!creatingVdi')
|
||||
i.fa.fa-minus(ng-if = 'creatingVdi')
|
||||
| New Disk
|
||||
|
|
||||
button.btn(type="button", ng-class = '{"btn-success": bootReordering, "btn-primary": !bootReordering}', ng-disabled="disksForm.$waiting", ng-click="bootReordering = !bootReordering;adding = false;creatingVdi = false")
|
||||
i.fa.fa-plus(ng-if = '!bootReordering')
|
||||
i.fa.fa-minus(ng-if = 'bootReordering')
|
||||
| Boot order
|
||||
br
|
||||
form.form-inline#addDiskForm(ng-if = 'adding', name = 'addForm', ng-submit = 'addVdi(vdiToAdd.vdi, vdiReadOnly, vdiBootable)')
|
||||
fieldset(ng-attr-disabled = '{{ addWaiting ? true : undefined }}')
|
||||
.form-group
|
||||
select#vdiToAdd.form-control(ng-model = 'vdiToAdd', ng-options = 'vdi.label group by vdi.sr for vdi in VDIOpts', required)
|
||||
option(value = '', disabled) -- Choose disk --
|
||||
|
|
||||
.form-group(ng-if = 'vdiToAdd')
|
||||
//- .form-group
|
||||
label(for = 'vdiPosition') Position
|
||||
select#vdiPosition.form-control(ng-model = '$parent.vdiPos', ng-options = 'vPos for vPos in vdiFreePos', required)
|
||||
option(value = '', disabled) --
|
||||
|
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', ng-model = '$parent.$parent.vdiBootable')
|
||||
| Bootable
|
||||
.checkbox
|
||||
label
|
||||
input(ng-if = '!isFreeForWriting(vdiToAdd.vdi)', type='checkbox', ng-model = '$parent.$parent.vdiReadOnly', ng-checked = 'true', ng-disabled = 'true')
|
||||
input(ng-if = 'isFreeForWriting(vdiToAdd.vdi)', type='checkbox', ng-model = '$parent.$parent.vdiReadOnly')
|
||||
| Read-only
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit', ng-disabled="disksForm.$waiting")
|
||||
| Add
|
||||
span(ng-if = 'addWaiting')
|
||||
|
|
||||
i.fa.fa-spin.fa-circle-o-notch
|
||||
br
|
||||
form.form-inline#createDiskForm(ng-if = 'creatingVdi', name = 'createForm', ng-submit = 'createVdi(newDiskName, newDiskSize, newDiskSR, newDiskBootable, newDiskReadonly)')
|
||||
fieldset(ng-attr-disabled = '{{ createWaiting ? true : undefined }}')
|
||||
.form-group
|
||||
//- label(for = 'newDiskName') Name
|
||||
input#newDiskName.form-control(type = 'text', ng-model = 'newDiskName', placeholder = 'Disk Name', required)
|
||||
|
|
||||
.form-group
|
||||
//- label(for = 'newDiskSize') Size
|
||||
input#newDiskSize.form-control(type = 'text', ng-model = 'newDiskSize', required, placeholder = 'Size e.g 128MB, 8GB, 2TB...')
|
||||
|
|
||||
.form-group
|
||||
//- label(for = 'newDiskSR') SR
|
||||
select.form-control(ng-model = 'newDiskSR', required, ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in writable_SRs")
|
||||
option(value = '', disabled) Choose your SR
|
||||
|
|
||||
br
|
||||
br
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', ng-model = 'newDiskBootable')
|
||||
| Bootable
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', ng-model = 'newDiskReadonly')
|
||||
| Read-only
|
||||
//- .form-group
|
||||
label(for = 'diskPosition') Position
|
||||
select#diskPosition.form-control(ng-model = 'newDiskPosition', ng-options = 'vPos for vPos in vdiFreePos', required)
|
||||
option(value = '', disabled) --
|
||||
|
|
||||
br
|
||||
br
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit', ng-disabled="disksForm.$waiting")
|
||||
i.fa.fa-plus-square
|
||||
| Create
|
||||
span(ng-if = 'createWaiting')
|
||||
|
|
||||
i.fa.fa-spin.fa-circle-o-notch
|
||||
br
|
||||
form(ng-if = 'bootReordering', ng-submit = 'saveBootParams(VM.id, bootParams)')
|
||||
fieldset(ng-attr-disabled = '{{ savingBootOrder ? true : undefined }}')
|
||||
.form-group(ng-repeat = 'elem in bootParams')
|
||||
label
|
||||
span(ng-class = '{"text-muted": !elem.v}') {{ elem.t }}
|
||||
input(type = 'checkbox', ng-model = 'elem.v')
|
||||
|
|
||||
button.btn.btn-default(type = 'button', ng-click = 'bootMove($index, -1)', ng-class = '{disabled: $first}'): i.fa.fa-chevron-up
|
||||
|
|
||||
button.btn.btn-default(type = 'button', ng-click = 'bootMove($index, 1)', ng-class = '{disabled: $last}'): i.fa.fa-chevron-down
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit', ng-disabled = 'disksForm.$waiting')
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
//- TODO: add interface in this panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-network(style="color: #e25440;")
|
||||
| Interface
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
th Device
|
||||
th MAC
|
||||
th MTU
|
||||
th Network
|
||||
th Link status
|
||||
tr(ng-repeat="VIF in VM.VIFs | resolve | orderBy:natural('device') track by VIF.id")
|
||||
td VIF \#{{VIF.device}}
|
||||
td
|
||||
| {{VIF.MAC}}
|
||||
td
|
||||
| {{VIF.MTU}}
|
||||
td.oneliner
|
||||
| {{(VIF.$network | resolve).name_label}}
|
||||
td(ng-if="VIF.attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this interface", ng-if="VM.power_state == ('Running' || 'Paused')", xo-click="disconnectVIF(VIF.id)")
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!VIF.attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Connect this interface", xo-click="connectVIF(VIF.id)")
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Remove this interface", xo-click="deleteVIF(VIF.id)")
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
.text-right
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingVif, "btn-primary": !creatingVif}', ng-click="creatingVif = !creatingVif")
|
||||
i.fa.fa-plus(ng-if = '!creatingVif')
|
||||
i.fa.fa-minus(ng-if = 'creatingVif')
|
||||
| Create Interface
|
||||
br
|
||||
form.form-inline.text-right#createInterfaceForm(
|
||||
ng-if = 'creatingVif'
|
||||
name = 'createInterfaceForm'
|
||||
ng-submit = 'createInterface(newInterfaceNetwork, newInterfaceMTU, autoMac, newInterfaceMAC)'
|
||||
)
|
||||
fieldset(ng-attr-disabled = '{{ createVifWaiting ? true : undefined }}')
|
||||
.form-group
|
||||
label(for = 'newVifNetwork') Network
|
||||
select.form-control(
|
||||
ng-options='network.name_label for network in networks'
|
||||
ng-model = 'newInterfaceNetwork'
|
||||
ng-change = 'updateMTU(newInterfaceNetwork)'
|
||||
required
|
||||
)
|
||||
option(value = '', disabled) --
|
||||
|
|
||||
.form-group
|
||||
fieldset(ng-attr-disabled = '{{ autoMac ? true : undefined }}')
|
||||
label.control-label(for = 'newInterfaceMAC') MAC address
|
||||
input#newInterfaceMAC.form-control(ng-class = '{hidden: autoMac}', type = 'text', ng-model = 'newInterfaceMAC', ng-required = '!autoMac')
|
||||
|
|
||||
.checkbox
|
||||
label
|
||||
input(type='checkbox', ng-model = 'autoMac')
|
||||
| Auto-generate
|
||||
|
|
||||
.form-group
|
||||
label(for = 'newInterfaceMTU') MTU
|
||||
input#newInterfaceMTU.form-control(type = 'text', ng-model = 'newInterfaceMTU', required)
|
||||
|
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
i.fa.fa-plus-square
|
||||
| Create
|
||||
span(ng-if = 'createVifWaiting')
|
||||
|
|
||||
i.fa.fa-spin.fa-circle-o-notch
|
||||
br
|
||||
//- Snap panel
|
||||
.grid
|
||||
form(editable-form="", name="vmSnap", oncancel="cancel()").panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-snapshot(style="color: #e25440;")
|
||||
| Snapshots
|
||||
span.quick-edit(tooltip="Edit snapshots", ng-click="vmSnap.$show()")
|
||||
i.fa.fa-edit.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="!VM.snapshots.length") No snapshots
|
||||
table.table.table-hover(ng-if="VM.snapshots.length")
|
||||
th Date
|
||||
th Name
|
||||
tr(ng-repeat="snapshot in VM.snapshots | resolve | orderBy:'-snapshot_time' | slice:(5*(currentSnapPage-1)):(5*currentSnapPage) track by snapshot.id")
|
||||
td.oneliner {{snapshot.snapshot_time*1e3 | date:"medium"}}
|
||||
td.oneliner
|
||||
span(editable-text="snapshot.name_label", e-name="name_label", e-form="vmSnap", onbeforesave="saveSnapshot(snapshot.id, $data)")
|
||||
| {{snapshot.name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Export this snapshot", type="button", xo-click="exportVM(snapshot.id)")
|
||||
i.fa.fa-upload.fa-lg
|
||||
a(tooltip="Revert VM to this snapshot", xo-click="revertSnapshot(snapshot.id)")
|
||||
i.fa.fa-undo.fa-lg
|
||||
a(tooltip="Remove this snapshot", xo-click="deleteSnapshot(snapshot.id)")
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
.center(ng-if = '(VM.snpashots | resolve).length > 5')
|
||||
pagination(boundary-links="true", total-items="(VM.snpashots | resolve).length", ng-model="$parent.currentSnapPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.btn-form(ng-show="vmSnap.$visible")
|
||||
p.center
|
||||
button.btn.btn-default(type="button", ng-disabled="vmSnap.$waiting", ng-click="vmSnap.$cancel()")
|
||||
i.fa.fa-times
|
||||
| Cancel
|
||||
|
|
||||
button.btn.btn-primary(type="submit", ng-disabled="vmSnap.$waiting", ng-click="saveChanges()")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
//- Logs panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments(style="color: #e25440;")
|
||||
| Logs
|
||||
span.quick-edit(ng-if="VM.messages | isNotEmpty", tooltip="Remove all logs", xo-click="deleteAllLog()")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="VM.messages | isEmpty") No recent logs
|
||||
table.table.table-hover(ng-if="VM.messages | isNotEmpty")
|
||||
th Date
|
||||
th Name
|
||||
tr(ng-repeat="message in VM.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
|
||||
td.oneliner {{message.time*1e3 | date:"medium"}}
|
||||
td.oneliner
|
||||
| {{message.name}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="deleteLog(message.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
|
||||
.center(ng-if = '(VM.messages | count) > 5')
|
||||
pagination(boundary-links="true", total-items="VM.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-plug(style="color: #e25440;")
|
||||
| PCI Devices
|
||||
.panel-body
|
||||
p.center(ng-if="!(VM.$container | resolve).$PCIs") No PCI devices available
|
||||
table.table.table-hover(ng-if="(VM.$container | resolve).$PCIs")
|
||||
th PCI Info
|
||||
th Device Name
|
||||
th Status
|
||||
tr(ng-repeat="pci in ((VM.$container | resolve).$PCIs | resolve) | orderBy:'pci_id' | slice:(5*(currentPCIPage-1)):(5*currentPCIPage) track by pci.id")
|
||||
td.oneliner {{pci.pci_id}} ({{pci.class_name}})
|
||||
td.oneliner {{pci.device_name}}
|
||||
td(ng-if="pci.pci_id === VM.other.pci")
|
||||
span.label.label-success Attached
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this PCI device", xo-click="disconnectPci(VM.id)")
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="pci.pci_id !== VM.other.pci")
|
||||
span.label.label-default Not attached
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Connect this PCI device", xo-click="connectPci(VM.id, pci.pci_id)")
|
||||
i.fa.fa-link.fa-lg
|
||||
.center(ng-if = '((VM.$container | resolve).$PCIs | resolve).length > 5')
|
||||
pagination(boundary-links="true", total-items="((VM.$container | resolve).$PCIs | resolve).length", ng-model="$parent.currentPCIPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-desktop(style="color: #e25440;")
|
||||
| vGPUs
|
||||
.panel-body
|
||||
p.center(ng-if="!VM.$VGPus") No vGPUs available
|
||||
table.table.table-hover(ng-if="VM.$VGPus")
|
||||
th Device
|
||||
th Status
|
||||
tr(ng-repeat="vgpu in VM.$VGPUs | resolve | orderBy:'device' | slice:(5*(currentGPUPage-1)):(5*currentGPUPage) track by vgpu.id")
|
||||
td.oneliner {{vgpu.device}}
|
||||
td(ng-if="vgu.currentlyAttached")
|
||||
span.label.label-success Attached
|
||||
td(ng-if="!vgu.currentlyAttached")
|
||||
span.label.label-default Not attached
|
||||
.center(ng-if = '(VM.$VGPUs | resolve).length > 5')
|
||||
pagination(boundary-links="true", total-items="(VM.$VGPUs | resolve).length", ng-model="$parent.currentGPUPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
101
app/node_modules/angular-no-vnc/index.js
generated
vendored
@@ -1,101 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import {
|
||||
format as formatUrl,
|
||||
parse as parseUrl,
|
||||
resolve as resolveUrl
|
||||
} from 'url'
|
||||
import {RFB} from 'novnc-node'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function parseRelativeUrl (url) {
|
||||
/* global window: false */
|
||||
return parseUrl(resolveUrl(String(window.location), url))
|
||||
}
|
||||
|
||||
const PROTOCOL_ALIASES = {
|
||||
'http:': 'ws:',
|
||||
'https:': 'wss:'
|
||||
}
|
||||
|
||||
function fixProtocol (url) {
|
||||
let protocol = PROTOCOL_ALIASES[url.protocol]
|
||||
|
||||
if (protocol) {
|
||||
url.protocol = protocol
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('no-vnc', [])
|
||||
.controller('NoVncCtrl', function ($attrs, $element, $scope) {
|
||||
this.height = 480
|
||||
$attrs.$observe('height', (height) => {
|
||||
this.height = height
|
||||
})
|
||||
this.width = 640
|
||||
$attrs.$observe('width', (width) => {
|
||||
this.width = width
|
||||
})
|
||||
|
||||
let rfb
|
||||
function clean () {
|
||||
// If there was a previous connection.
|
||||
if (rfb) {
|
||||
rfb.disconnect()
|
||||
rfb = undefined
|
||||
}
|
||||
}
|
||||
|
||||
this.remoteControl = {
|
||||
sendCtrlAltDel () {
|
||||
if (rfb) {
|
||||
rfb.sendCtrlAltDel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let canvas = $element.find('canvas')[0]
|
||||
|
||||
$attrs.$observe('url', (url) => {
|
||||
// Remove previous connection.
|
||||
clean()
|
||||
|
||||
// If the URL is empty, stop now.
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the URL.
|
||||
url = parseRelativeUrl(url)
|
||||
fixProtocol(url)
|
||||
|
||||
let isSecure = url.protocol === 'wss:'
|
||||
|
||||
rfb = new RFB({
|
||||
encrypt: isSecure,
|
||||
target: canvas,
|
||||
wsProtocols: ['chat']
|
||||
})
|
||||
|
||||
// Connect.
|
||||
rfb.connect(formatUrl(url))
|
||||
})
|
||||
|
||||
$scope.$on('$destroy', clean)
|
||||
})
|
||||
.directive('noVnc', function () {
|
||||
return {
|
||||
bindToController: true,
|
||||
controller: 'NoVncCtrl as noVnc',
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
remoteControl: '='
|
||||
},
|
||||
template: view
|
||||
}
|
||||
})
|
||||
.name
|
||||
9
app/node_modules/angular-no-vnc/package.json
generated
vendored
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify",
|
||||
"browserify-plain-jade"
|
||||
]
|
||||
}
|
||||
}
|
||||
5
app/node_modules/angular-no-vnc/view.jade
generated
vendored
@@ -1,5 +0,0 @@
|
||||
canvas.center-block(
|
||||
height = "{{noVnc.height}}"
|
||||
width = "{{noVnc.width}}"
|
||||
)
|
||||
| Sorry, your browser does not support the canvas element.
|
||||
36
app/node_modules/master-select/demo.js
generated
vendored
@@ -1,36 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import angularUiRouter from 'angular-ui-router'
|
||||
import masterSelect from './'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
class MasterSelectDemoCtrl {
|
||||
constructor () {
|
||||
this.master = null
|
||||
|
||||
const items = this.items = new Array(10)
|
||||
|
||||
for (let i = 0, n = items.length; i < n; ++i) {
|
||||
items[i] = {
|
||||
label: `Item ${i}`,
|
||||
selected: (Math.random() < 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default angular.module('xoWebApp.foo', [
|
||||
angularUiRouter,
|
||||
masterSelect
|
||||
])
|
||||
.config($stateProvider => {
|
||||
$stateProvider.state('master-select-demo', {
|
||||
url: '/master-select-demo',
|
||||
controller: 'MasterSelectDemoCtrl as demo',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('MasterSelectDemoCtrl', MasterSelectDemoCtrl)
|
||||
.name
|
||||
87
app/node_modules/master-select/index.js
generated
vendored
@@ -1,87 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const defaultIsSelected = (object) => object != null && object.selected
|
||||
const defaultSelect = (object) => {
|
||||
if (object != null) {
|
||||
object.selected = true
|
||||
}
|
||||
}
|
||||
const defaultUnselect = (object) => {
|
||||
if (object != null) {
|
||||
object.selected = false
|
||||
}
|
||||
}
|
||||
|
||||
export default angular.module('master-select', [])
|
||||
.directive('masterSelect', function () {
|
||||
return {
|
||||
link ($scope, $elem, $attrs) {
|
||||
const isSelected = defaultIsSelected
|
||||
const select = defaultSelect
|
||||
const unselect = defaultUnselect
|
||||
|
||||
let internalChange = false
|
||||
$scope.$watch('items', items => {
|
||||
if (internalChange) {
|
||||
internalChange = false
|
||||
return
|
||||
}
|
||||
|
||||
const selected = $scope.selectedItems = Object.create(null)
|
||||
let nAll = 0
|
||||
let nSelected = 0
|
||||
forEach(items, (item, key) => {
|
||||
++nAll
|
||||
|
||||
if (isSelected(item, key, items)) {
|
||||
selected[key] = item
|
||||
++nSelected
|
||||
}
|
||||
})
|
||||
|
||||
const indeterminate = nSelected && nSelected !== nAll
|
||||
|
||||
const previousNgModel = $scope.ngModel
|
||||
$scope.ngModel = indeterminate || Boolean(nSelected)
|
||||
if (previousNgModel !== $scope.ngModel) {
|
||||
internalChange = true
|
||||
}
|
||||
|
||||
$elem.prop('indeterminate', indeterminate)
|
||||
}, true)
|
||||
|
||||
$scope.$watch('ngModel', selected => {
|
||||
if (internalChange) {
|
||||
internalChange = false
|
||||
return
|
||||
}
|
||||
internalChange = true
|
||||
|
||||
$elem.prop('indeterminate', false)
|
||||
|
||||
const {items} = $scope
|
||||
const selectedItems = $scope.selectedItems = Object.create(null)
|
||||
|
||||
forEach(items, selected ?
|
||||
(item, key) => {
|
||||
select(item, key, items)
|
||||
selectedItems[key] = item
|
||||
} :
|
||||
(item, key) => {
|
||||
unselect(item, key, items)
|
||||
}
|
||||
)
|
||||
})
|
||||
},
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
items: '=masterSelect',
|
||||
ngModel: '=',
|
||||
selectedItems: '=?'
|
||||
}
|
||||
}
|
||||
})
|
||||
.name
|
||||
10
app/node_modules/master-select/package.json
generated
vendored
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify",
|
||||
"browserify-plain-jade"
|
||||
]
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
31
app/node_modules/master-select/view.jade
generated
vendored
@@ -1,31 +0,0 @@
|
||||
.container-fluid: .row: .grid
|
||||
|
||||
.panel.panel-default: .panel-body
|
||||
p
|
||||
button(
|
||||
ng-click = 'demo.master = !demo.master'
|
||||
) Toggle
|
||||
p
|
||||
input(
|
||||
type = 'checkbox'
|
||||
master-select = 'demo.items'
|
||||
selected-items = 'selectedItems'
|
||||
ng-model = 'demo.master'
|
||||
)
|
||||
| Master
|
||||
|
||||
ul
|
||||
li(
|
||||
ng-repeat = 'item in demo.items'
|
||||
)
|
||||
input(
|
||||
type = 'checkbox'
|
||||
ng-model = 'item.selected'
|
||||
)
|
||||
| {{item.label}}
|
||||
|
||||
.panel.panel-default: .panel-body
|
||||
pre {{demo | json}}
|
||||
|
||||
.panel.panel-default: .panel-body
|
||||
pre {{selectedItems | json}}
|
||||
413
app/node_modules/xo-api/index.js
generated
vendored
@@ -1,413 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import angularCookies from 'angular-cookies'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import forEach from 'lodash.foreach'
|
||||
import indexOf from 'lodash.indexof'
|
||||
import sum from 'lodash.sum'
|
||||
import XoIndex from 'xo-collection/index'
|
||||
import xoLib from 'xo-lib'
|
||||
import XoUniqueIndex from 'xo-collection/unique-index'
|
||||
import XoView from 'xo-collection/view'
|
||||
|
||||
const {defineProperty} = Object
|
||||
const {isArray, isString} = angular
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Low level XO API for Angular.
|
||||
export default angular.module('xo-api', [
|
||||
angularCookies
|
||||
])
|
||||
.run(function ($rootScope) {
|
||||
// Ensure correct integration with Angular.
|
||||
xoLib.setScheduler(function (fn) {
|
||||
$rootScope.$evalAsync(fn)
|
||||
})
|
||||
})
|
||||
.service('xoApi', function (
|
||||
$cookieStore,
|
||||
$rootScope,
|
||||
$timeout
|
||||
) {
|
||||
const xo = new xoLib.Xo()
|
||||
|
||||
// A lots of event listeners are added to the collection.
|
||||
xo.objects.setMaxListeners(0)
|
||||
|
||||
// Notifies Angular about changes in the collection.
|
||||
xo.objects.on('finish', () => {
|
||||
$rootScope.$applyAsync()
|
||||
})
|
||||
|
||||
try {
|
||||
const token = $cookieStore.get('token')
|
||||
|
||||
// If there is a token, sign in with it.
|
||||
if (token) {
|
||||
xo.signIn({ token })
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
$cookieStore.remove('token')
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
const getObject = (function (objects) {
|
||||
const {
|
||||
all: byIds,
|
||||
indexes: {
|
||||
ref: byRefs
|
||||
}
|
||||
} = objects
|
||||
|
||||
return function getObject (id, type) {
|
||||
const object = byIds[id] || byRefs[id]
|
||||
|
||||
if (
|
||||
// The object has been found and …
|
||||
object && (
|
||||
// … no type specified.
|
||||
!type ||
|
||||
|
||||
// … is of the expected type.
|
||||
(type === object.type) ||
|
||||
|
||||
// … is of one of the allowed types.
|
||||
isArray(type) && (indexOf(type, object.type) === -1)
|
||||
)
|
||||
) {
|
||||
return object
|
||||
}
|
||||
}
|
||||
})(xo.objects)
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// TODO: should probably be merged in the main collection in xo-lib.
|
||||
let currentAcls = Object.create(null)
|
||||
|
||||
;(function updateCurrentAcls () {
|
||||
xo.call('acl.getCurrent').then(acls => {
|
||||
currentAcls = Object.create(null)
|
||||
|
||||
forEach(acls, acl => {
|
||||
const object = getObject(acl.object)
|
||||
if (object) {
|
||||
currentAcls[object.id] = true
|
||||
}
|
||||
})
|
||||
|
||||
$timeout(updateCurrentAcls, 1e4)
|
||||
})
|
||||
})()
|
||||
|
||||
function canAccess (id) {
|
||||
// Auto unbox.
|
||||
if (id.id) {
|
||||
id = id.id
|
||||
}
|
||||
|
||||
const {user} = xo
|
||||
let object
|
||||
return (
|
||||
// Administrators can access everything.
|
||||
user && (user.permission === 'admin') ||
|
||||
|
||||
// Check if the id is in the ACLs table.
|
||||
(id in currentAcls) ||
|
||||
|
||||
// Check if the id is in fact not a true id (maybe a ref or a
|
||||
// UUID) and if we can resolve it to an id.
|
||||
(object = getObject(id)) && (object.id in currentAcls)
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
const getView = (function () {
|
||||
const views = Object.create(null)
|
||||
|
||||
function getView (viewName) {
|
||||
let view = views[viewName]
|
||||
if (!view) {
|
||||
// The view name can be plural (ex VMs) but the type is
|
||||
// singular.
|
||||
const type = viewName[viewName.length - 1] === 's' ?
|
||||
viewName.slice(0, -1) :
|
||||
viewName
|
||||
|
||||
const predicate = (object) => object.type === type
|
||||
view = views[viewName] = new XoView(xo.objects, predicate)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
|
||||
function registerLazyView (name, predicate, collection) {
|
||||
defineProperty(views, name, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get () {
|
||||
if (!collection) {
|
||||
collection = xo.objects
|
||||
} else if (isString(collection)) {
|
||||
collection = getView(collection)
|
||||
}
|
||||
|
||||
const view = new XoView(collection, predicate)
|
||||
delete views[name]
|
||||
views[name] = view
|
||||
|
||||
return view
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
registerLazyView(
|
||||
'runningHosts',
|
||||
host => host.power_state === 'Running',
|
||||
'hosts'
|
||||
)
|
||||
|
||||
const RUNNING_VM_STATUSES = {
|
||||
Running: true,
|
||||
Paused: true
|
||||
}
|
||||
registerLazyView(
|
||||
'runningVms',
|
||||
(vm) => RUNNING_VM_STATUSES[vm.power_state],
|
||||
'VMs'
|
||||
)
|
||||
|
||||
const RUNNING_TASK_STATUSES = {
|
||||
cancelling: true,
|
||||
pending: true
|
||||
}
|
||||
registerLazyView(
|
||||
'runningTasks',
|
||||
(task) => RUNNING_TASK_STATUSES[task.status] && canAccess(task.$host),
|
||||
'tasks'
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------
|
||||
|
||||
return getView
|
||||
})()
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
const getIndex = (function (indexes) {
|
||||
function registerLazyIndex (name, computeHash, collection, isUnique) {
|
||||
Object.defineProperty(indexes, name, {
|
||||
configurable: true,
|
||||
get () {
|
||||
if (!collection) {
|
||||
collection = xo.objects
|
||||
} else if (isString(collection)) {
|
||||
collection = getView(collection)
|
||||
}
|
||||
|
||||
const index = new (isUnique ? XoUniqueIndex : XoIndex)(computeHash)
|
||||
index._attachCollection(collection)
|
||||
|
||||
const items = index.items
|
||||
|
||||
delete indexes[name]
|
||||
indexes[name] = items
|
||||
|
||||
return items
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
registerLazyIndex(
|
||||
'hostsByPool',
|
||||
'$poolId',
|
||||
'hosts'
|
||||
)
|
||||
|
||||
registerLazyIndex(
|
||||
'networksByPool',
|
||||
'$poolId',
|
||||
'networks'
|
||||
)
|
||||
|
||||
registerLazyIndex(
|
||||
'poolPatchesByPool',
|
||||
'$poolId',
|
||||
'pool_patch'
|
||||
)
|
||||
|
||||
registerLazyIndex(
|
||||
'runningHostsByPool',
|
||||
'$poolId',
|
||||
'runningHosts'
|
||||
)
|
||||
|
||||
registerLazyIndex(
|
||||
'runningTasksByHost',
|
||||
'$host',
|
||||
'runningTasks'
|
||||
)
|
||||
|
||||
registerLazyIndex(
|
||||
'runningVmsByPool',
|
||||
'$poolId',
|
||||
'runningVms'
|
||||
)
|
||||
|
||||
registerLazyIndex(
|
||||
'srsByContainer',
|
||||
'$container',
|
||||
'SRs'
|
||||
)
|
||||
|
||||
registerLazyIndex(
|
||||
'vmsByContainer',
|
||||
'$container',
|
||||
'VMs'
|
||||
)
|
||||
|
||||
registerLazyIndex(
|
||||
'vmsByPool',
|
||||
'$poolId',
|
||||
'VMs'
|
||||
)
|
||||
|
||||
registerLazyIndex(
|
||||
'vmControllersByContainer',
|
||||
'$container',
|
||||
'VM-controllers',
|
||||
true
|
||||
)
|
||||
|
||||
registerLazyIndex(
|
||||
'vmTemplatesByContainer',
|
||||
'$container',
|
||||
'VM-templates'
|
||||
)
|
||||
|
||||
return function getIndex (name) {
|
||||
const index = indexes[name]
|
||||
if (!index) {
|
||||
throw new Error('no such index ' + name)
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
})(Object.create(null))
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
const stats = {
|
||||
$CPUs: 0,
|
||||
$vCPUs: 0,
|
||||
$memory: {
|
||||
usage: 0,
|
||||
size: 0
|
||||
}
|
||||
}
|
||||
|
||||
getView('hosts').on('finish', function () {
|
||||
stats.$CPUs = sum(this.all, host => +host.CPUs.cpu_count)
|
||||
})
|
||||
|
||||
getView('runningVms').on('finish', function () {
|
||||
stats.$vCPUs = sum(this.all, vm => vm.CPUs.number)
|
||||
})
|
||||
|
||||
// TODO: maybe merge with stats.$CPUs.
|
||||
getView('runningHosts').on('finish', function () {
|
||||
// TODO: merge into a single loop.
|
||||
stats.$memory.usage = sum(this.all, host => host.memory.usage)
|
||||
stats.$memory.size = sum(this.all, host => host.memory.size)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
return {
|
||||
// -----------------
|
||||
// Session
|
||||
// -----------------
|
||||
|
||||
logIn (email, password, persist) {
|
||||
return xo.signIn({ email, password }).then(() => {
|
||||
if (persist) {
|
||||
xo.call('token.create').then(function (token) {
|
||||
$cookieStore.put('token', token)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
logOut () {
|
||||
$cookieStore.remove('token')
|
||||
|
||||
return xo.signOut()
|
||||
},
|
||||
get status () {
|
||||
return xo.status
|
||||
},
|
||||
get user () {
|
||||
return xo.user
|
||||
},
|
||||
|
||||
// -----------------
|
||||
// RPC
|
||||
// -----------------
|
||||
|
||||
call (method, params) {
|
||||
// The params need to be cloned to prevent them from being
|
||||
// changed before the method has really been sent.
|
||||
return xo.call(method, cloneDeep(params))
|
||||
},
|
||||
|
||||
// -----------------
|
||||
// Objects
|
||||
// -----------------
|
||||
|
||||
// Collection of all objects.
|
||||
all: xo.objects.all,
|
||||
|
||||
// Returns an object (or multiple objects) from its id/ref.
|
||||
//
|
||||
// They can be filter by type(s).
|
||||
get (id, types) {
|
||||
if (isArray(id)) {
|
||||
const objects = []
|
||||
|
||||
forEach(id, id => {
|
||||
const object = getObject(id, types)
|
||||
if (object) {
|
||||
objects.push(object)
|
||||
}
|
||||
})
|
||||
|
||||
return objects
|
||||
}
|
||||
|
||||
return getObject(id, types)
|
||||
},
|
||||
|
||||
getIndex,
|
||||
|
||||
// Returns a view (read-only XoCollection).
|
||||
getView,
|
||||
|
||||
// Checks whether the current user has access to an object
|
||||
// (identified via its id or ref).
|
||||
canAccess,
|
||||
|
||||
// -----------------
|
||||
// Various
|
||||
// -----------------
|
||||
|
||||
// Global stats.
|
||||
stats
|
||||
}
|
||||
})
|
||||
.name
|
||||
8
app/node_modules/xo-api/package.json
generated
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify"
|
||||
]
|
||||
}
|
||||
}
|
||||
124
app/node_modules/xo-directives/index.coffee
generated
vendored
@@ -1,124 +0,0 @@
|
||||
angular = require 'angular'
|
||||
|
||||
# TODO: split into multiple modules.
|
||||
module.exports = angular.module 'xoWebApp.directives', []
|
||||
|
||||
# This attribute stops the ascendant propagation of a given event.
|
||||
#
|
||||
# The value of this attribute should be the name of the event to
|
||||
# stop.
|
||||
.directive 'stopEvent', ->
|
||||
(_, $element, attrs) ->
|
||||
$element.on attrs.stopEvent, ($event) ->
|
||||
$event.stopPropagation()
|
||||
|
||||
# This attribute works similarly to `ng-click` but do not handle the
|
||||
# event if the clicked element:
|
||||
# - is an `input`;
|
||||
# - has a `ng-click` attribute;
|
||||
# - has a `xo-click` attribute;
|
||||
# - has a `xo-sref` attribute;
|
||||
# - is a link (`a`) and has a `href` attribute.
|
||||
.directive 'xoClick', ($parse) ->
|
||||
($scope, $element, attrs) ->
|
||||
fn = $parse attrs.xoClick
|
||||
current = $element[0]
|
||||
current.addEventListener(
|
||||
'click'
|
||||
(event) ->
|
||||
|
||||
# Browse all parent elements of the element the event
|
||||
# happened to and abort if one of them should handle the
|
||||
# event itself.
|
||||
el = event.target
|
||||
while el isnt current
|
||||
{attributes: attrs, tagName: tag} = el
|
||||
|
||||
return if (
|
||||
tag is 'INPUT' or
|
||||
attrs['ng-click']? or
|
||||
attrs['xo-click']? or
|
||||
attrs['xo-sref']? or
|
||||
(tag is 'A') and attrs.href?
|
||||
)
|
||||
|
||||
el = el.parentNode
|
||||
|
||||
# Stop the propagation.
|
||||
event.stopPropagation()
|
||||
|
||||
# Apply the `xo-click` attribute.
|
||||
$scope.$applyAsync ->
|
||||
fn $scope, {$event: event}
|
||||
true
|
||||
)
|
||||
|
||||
# TODO: create a directive which allows a link on any element.
|
||||
|
||||
# TODO: Mutualize code with `xoClick`.
|
||||
.directive 'xoSref', ($state, $window) ->
|
||||
($scope, $element, attrs) ->
|
||||
current = $element[0]
|
||||
current.addEventListener(
|
||||
'mouseup'
|
||||
(event) ->
|
||||
|
||||
{which: button} = event
|
||||
return unless button is 1 or button is 2
|
||||
|
||||
# Browse all parent elements of the element the event
|
||||
# happened to and abort if one of them should handle the
|
||||
# event itself.
|
||||
el = event.target
|
||||
while el isnt current
|
||||
{attributes: attrs_, tagName: tag} = el
|
||||
|
||||
return if (
|
||||
tag is 'INPUT' or
|
||||
attrs_['ng-click']? or
|
||||
attrs_['xo-click']? or
|
||||
attrs_['xo-sref']? or
|
||||
(tag is 'A') and attrs_.href?
|
||||
)
|
||||
|
||||
el = el.parentNode
|
||||
|
||||
# Stop the propagation.
|
||||
event.stopPropagation()
|
||||
|
||||
# Extracts the state and its parameters for the `xo-sref`
|
||||
# attribute.
|
||||
match = attrs.xoSref.match /^([^(]+)\s*(?:\((.*)\))?$/
|
||||
throw new Error 'invalid SREF' unless match
|
||||
state = match[1]
|
||||
params = if match[2] then $scope.$eval match[2] else {}
|
||||
|
||||
# Ctrl modifier or middle-button.
|
||||
if event.ctrlKey or button is 2
|
||||
url = $state.href state, params
|
||||
$window.open url
|
||||
else
|
||||
$state.go state, params
|
||||
true
|
||||
)
|
||||
|
||||
.directive 'fixAutofill', ($timeout) ->
|
||||
restrict: 'A'
|
||||
require: 'ngModel'
|
||||
link: ($scope, $elem, attrs, ngModel) ->
|
||||
previous = $elem.val()
|
||||
|
||||
updateValue = ->
|
||||
current = $elem.val()
|
||||
if ngModel.$pristine and current isnt previous
|
||||
previous = current
|
||||
ngModel.$setViewValue current
|
||||
|
||||
# Attempt to update the value.
|
||||
$timeout updateValue, 5e2
|
||||
|
||||
# A refresh can be asked via the fixAutofill event.
|
||||
$scope.$on 'fixAutofill', updateValue
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
8
app/node_modules/xo-directives/package.json
generated
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"coffeeify"
|
||||
]
|
||||
}
|
||||
}
|
||||
174
app/node_modules/xo-filters/index.js
generated
vendored
@@ -1,174 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import isEmpty from 'lodash.isempty'
|
||||
import map from 'lodash.map'
|
||||
import slice from 'lodash.slice'
|
||||
import xoApi from 'xo-api'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.filters', [
|
||||
xoApi
|
||||
])
|
||||
|
||||
// The bytes filters takes a number and formats it using adapted
|
||||
// units (KB, MB, etc.).
|
||||
.filter('bytesToSize', () => {
|
||||
const powers = ['', 'K', 'M', 'G', 'T', 'P']
|
||||
|
||||
return function bytesToSize (bytes, unit = 'B', base = 1024) {
|
||||
let i = 0
|
||||
while (bytes >= base) {
|
||||
bytes /= base
|
||||
++i
|
||||
}
|
||||
|
||||
if (bytes === -1) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
// Maximum 1 decimals.
|
||||
bytes = ((bytes * 10) | 0) / 10
|
||||
|
||||
return `${bytes}${powers[i]}${unit}`
|
||||
}
|
||||
})
|
||||
|
||||
.filter('sizeToBytes', () => {
|
||||
/* eslint no-multi-spaces: 0 */
|
||||
|
||||
const RE = new RegExp('^' +
|
||||
'(\\d+(?:\\.\\d+)?)' + // digits ('.' digits)?
|
||||
'\\s*' + // Optional spaces between the digits and the unit.
|
||||
'([kmgtp])?' + // Optional unit modifier K/M/G/T/P.
|
||||
'b?' + // Optional unit (“b”), not meaningful.
|
||||
'$', 'i')
|
||||
|
||||
const factors = {
|
||||
k: 1024,
|
||||
m: 1048576,
|
||||
g: 1073741824,
|
||||
t: 1099511627776,
|
||||
p: 1125899906842624
|
||||
}
|
||||
|
||||
return function sizeToBytes (size) {
|
||||
const matches = RE.exec(size)
|
||||
|
||||
// If the input is invalid, just returns null.
|
||||
if (!matches) {
|
||||
return null
|
||||
}
|
||||
|
||||
let value = +matches[1]
|
||||
|
||||
const modifier = matches[2]
|
||||
if (modifier) {
|
||||
const factor = factors[modifier.toLowerCase()]
|
||||
if (factor) {
|
||||
value *= factor
|
||||
}
|
||||
}
|
||||
|
||||
return Math.round(value)
|
||||
}
|
||||
})
|
||||
|
||||
// Simply returns the number of elements in the collection.
|
||||
.filter('count', () => {
|
||||
const {hasOwnProperty} = Object.prototype
|
||||
|
||||
return function count (collection) {
|
||||
if (typeof collection !== 'object') {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Array.
|
||||
if (angular.isArray(collection)) {
|
||||
return collection.length
|
||||
}
|
||||
|
||||
// Object.
|
||||
let n = 0
|
||||
for (let key in collection) {
|
||||
if (hasOwnProperty.call(collection, key)) {
|
||||
++n
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
})
|
||||
|
||||
// Resolves links between objects.
|
||||
.filter('resolve', (xoApi) => xoApi.get)
|
||||
|
||||
.filter('isEmpty', () => isEmpty)
|
||||
.filter('isNotEmpty', () => (collection) => !isEmpty(collection))
|
||||
|
||||
.filter('slice', () => slice)
|
||||
|
||||
// Applies a function to a list of items.
|
||||
//
|
||||
// If a string is used instead of a function, it will be used as a
|
||||
// property name to extract from each item.
|
||||
.filter('map', () => map)
|
||||
|
||||
.filter('percentage', () => {
|
||||
return function percentage (value) {
|
||||
// If `value` is an array of two values, divide the first by the
|
||||
// second and multiply by 100.
|
||||
if (value.length === 2) {
|
||||
// Special case, if the divider is 0, simply returns "N/A".
|
||||
if (value[1] === 0) {
|
||||
return 'N/A'
|
||||
}
|
||||
|
||||
const result = 100 * value[0] / value[1]
|
||||
if (isNaN(result)) {
|
||||
return 'N/A'
|
||||
}
|
||||
|
||||
value = result
|
||||
}
|
||||
|
||||
// No decimals at most.
|
||||
value = Math.round(value * 1e0) / 1e0
|
||||
|
||||
return `${value}%`
|
||||
}
|
||||
})
|
||||
|
||||
.filter('osFamily', () => {
|
||||
const osToFamily = (function (osByFamily) {
|
||||
const osToFamily = Object.create(null)
|
||||
|
||||
forEach(osByFamily, (list, family) => {
|
||||
forEach(list, os => {
|
||||
osToFamily[os] = family
|
||||
})
|
||||
})
|
||||
|
||||
return osToFamily
|
||||
})({
|
||||
linux: [
|
||||
'centos',
|
||||
'CoreOS',
|
||||
'debian',
|
||||
'fedora',
|
||||
'gentoo',
|
||||
'oracle',
|
||||
'redhat',
|
||||
'sles',
|
||||
'suse',
|
||||
'ubuntu'
|
||||
],
|
||||
windows: [
|
||||
'windows'
|
||||
]
|
||||
})
|
||||
|
||||
return (osName) => osToFamily[osName] || 'other'
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
8
app/node_modules/xo-filters/package.json
generated
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify"
|
||||
]
|
||||
}
|
||||
}
|
||||
40
app/node_modules/xo-notify/index.js
generated
vendored
@@ -1,40 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import angularAnimate from 'angular-animate'
|
||||
import angularNotifyToaster from 'angular-notify-toaster'
|
||||
|
||||
const {isString} = angular
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// High level integration of XO API in xo-web.
|
||||
export default angular.module('xo-notify', [
|
||||
angularAnimate,
|
||||
angularNotifyToaster
|
||||
])
|
||||
.service('xoNotify', (toaster) => {
|
||||
function makeNotifier (level) {
|
||||
return function notifier (options) {
|
||||
if (isString(options)) {
|
||||
options = { message: options }
|
||||
} else if (!options.message) {
|
||||
throw new Error('missing message')
|
||||
}
|
||||
|
||||
toaster.pop(
|
||||
level,
|
||||
options.title || 'Xen-Orchestra',
|
||||
options.message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
warning: makeNotifier('warning'),
|
||||
error: makeNotifier('error'),
|
||||
info: makeNotifier('info')
|
||||
// TODO: It is probably a bad design to have notification for
|
||||
// successful operations.
|
||||
// success: makeNotifier 'success'
|
||||
}
|
||||
})
|
||||
.name
|
||||
8
app/node_modules/xo-notify/package.json
generated
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify"
|
||||
]
|
||||
}
|
||||
}
|
||||
14
app/node_modules/xo-services/index.js
generated
vendored
@@ -1,14 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import xo from 'xo'
|
||||
import xoNotify from 'xo-notify'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// TODO: remove when no longer used.
|
||||
export default angular.module('xoWebApp.services', [
|
||||
xo,
|
||||
xoNotify
|
||||
])
|
||||
// Alias the service for compatibility.
|
||||
.service('notify', (xoNotify) => xoNotify)
|
||||
.name
|
||||
8
app/node_modules/xo-services/package.json
generated
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify"
|
||||
]
|
||||
}
|
||||
}
|
||||
300
app/node_modules/xo/index.js
generated
vendored
@@ -1,300 +0,0 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import xoApi from 'xo-api'
|
||||
import xoNotify from 'xo-notify'
|
||||
|
||||
const {isString} = angular
|
||||
|
||||
const isPlainObject = (function (toS) {
|
||||
const ref = toS.call({})
|
||||
return (value) => value != null && toS.call(value) === ref
|
||||
})(Object.prototype.toString)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// High level integration of XO API in xo-web.
|
||||
export default angular.module('xo', [
|
||||
xoApi,
|
||||
xoNotify
|
||||
])
|
||||
.service('xo', function (xoApi, xoNotify) {
|
||||
// FIXME: default mapper should be identity.
|
||||
const defaultArgsMapper = (...args) => {
|
||||
const {length} = args
|
||||
|
||||
// No arguments.
|
||||
if (!length) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (length !== 1) {
|
||||
throw new Error('no obvious mapping: more than one argument')
|
||||
}
|
||||
|
||||
const arg = args[0]
|
||||
|
||||
// The only argument is an object, it is probably the arguments,
|
||||
// therefore it should be forwarded.
|
||||
if (isPlainObject(arg)) {
|
||||
return arg
|
||||
}
|
||||
|
||||
// The only argument is a string, attempts to match it as `id`.
|
||||
if (isString(arg)) {
|
||||
return { id: arg }
|
||||
}
|
||||
|
||||
throw new Error('no obvious mapping: the only argument is neither an object or a string')
|
||||
}
|
||||
|
||||
const action = (name, method, options) => {
|
||||
if (!method) {
|
||||
return () => {
|
||||
xoNotify.info({
|
||||
title: name,
|
||||
message: 'This feature has not been implemented yet.'
|
||||
})
|
||||
|
||||
// TODO: A (broken) promise should be returned for
|
||||
// consistency.
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
argsMapper = defaultArgsMapper,
|
||||
notification
|
||||
} = options || {}
|
||||
|
||||
return (...args) => {
|
||||
const promise = xoApi.call(method, argsMapper(...args))
|
||||
|
||||
promise.catch(error => {
|
||||
console.error('Error for %s:', method, error)
|
||||
|
||||
if (notification !== false) {
|
||||
const message = (error && error.code === 2) ?
|
||||
'You don\'t have the permission.' :
|
||||
'The action failed for unknown reason.'
|
||||
|
||||
xoNotify.warning({
|
||||
title: name,
|
||||
message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint key-spacing: 0 */
|
||||
|
||||
// The interface.
|
||||
const xo = {
|
||||
acl: {
|
||||
add: action('Adding an ACL entry', 'acl.add', {
|
||||
argsMapper: (subject, object, action) => ({subject, object, action})
|
||||
}),
|
||||
get: action('Getting ACLs', 'acl.get'),
|
||||
remove: action('Remove an ACL entry', 'acl.remove', {
|
||||
argsMapper: (subject, object, action) => ({subject, object, action})
|
||||
})
|
||||
},
|
||||
|
||||
pool: {
|
||||
disconnect: action('Disconnect pool'),
|
||||
new_sr: action('New SR'), // temp fix before creating SR
|
||||
patch: action('Upload patch', 'pool.patch', {
|
||||
argsMapper: (pool) => ({pool})
|
||||
})
|
||||
},
|
||||
|
||||
host: {
|
||||
attach: action('Attach host'),
|
||||
detach: action('Detach host', 'host.detach'),
|
||||
restart: action('Restart host', 'host.restart'),
|
||||
restartToolStack: action('Restart tool stack', 'host.restart_agent'),
|
||||
start: action('Start host', 'host.start'),
|
||||
enable: action('Enable host', 'host.enable'),
|
||||
stop: action('Stop host', 'host.stop'),
|
||||
disable: action('Disable host', 'host.disable'),
|
||||
new_sr: action('New SR'), // temp fix before creating SR
|
||||
listMissingPatches: action('Check available patches', 'host.listMissingPatches', {
|
||||
argsMapper: (host) => ({host})
|
||||
}),
|
||||
installPatch: action('Install a patch from a patch id', 'host.installPatch', {
|
||||
argsMapper: (host, patch) => ({host, patch})
|
||||
}),
|
||||
refreshStats: action('Get Stats', 'host.stats', {
|
||||
notification: false,
|
||||
argsMapper: (host) => ({host})
|
||||
})
|
||||
// TODO: attach/set
|
||||
},
|
||||
|
||||
log: {
|
||||
delete: action('Delete Log', 'message.delete')
|
||||
},
|
||||
|
||||
message: {
|
||||
delete: action('Delete message')
|
||||
},
|
||||
|
||||
pbd: {
|
||||
delete: action('Delete PBD'),
|
||||
disconnect: action('Disconnect PBD')
|
||||
},
|
||||
|
||||
server: {
|
||||
add: action('Add server', 'server.add'),
|
||||
remove: action('Remove server', 'server.remove'),
|
||||
getAll: action('Getting server', 'server.getAll'),
|
||||
set: action('Save server', 'server.set'),
|
||||
connect: action('Connect to a server', 'server.connect', {
|
||||
notification: false
|
||||
}),
|
||||
disconnect: action('Disconnect from a server', 'server.disconnect')
|
||||
},
|
||||
|
||||
task: {
|
||||
cancel: action('Cancel task', 'task.cancel'),
|
||||
destroy: action('Destroy task', 'task.destroy')
|
||||
},
|
||||
|
||||
user: {
|
||||
create: action('Create user', 'user.create'),
|
||||
delete: action('Delete user', 'user.delete', {
|
||||
argsMapper: (id) => ({ id: String(id) })
|
||||
}),
|
||||
getAll: action('Getting users', 'user.getAll'),
|
||||
set: action('Save user', 'user.set')
|
||||
},
|
||||
|
||||
group: {
|
||||
create: action('Create group', 'group.create'),
|
||||
delete: action('Delete group', 'group.delete', {
|
||||
argsMapper: (id) => ({ id: String(id) })
|
||||
}),
|
||||
getAll: action('Getting groups', 'group.getAll'),
|
||||
set: action('Save group', 'group.set'),
|
||||
setUsers: action('Set group users', 'group.setUsers', {
|
||||
argsMapper: (id, userIds) => ({ id: String(id), userIds})
|
||||
}),
|
||||
addUser: action('Add group user', 'group.addUser', {
|
||||
argsMapper: (id, userId) => ({ id: String(id), userId: String(userId)})
|
||||
}),
|
||||
removeUser: action('Remove group users', 'group.removeUser', {
|
||||
argsMapper: (id, userId) => ({ id: String(id), userId: String(userId)})
|
||||
})
|
||||
},
|
||||
|
||||
role: {
|
||||
getAll: action('Getting roles', 'role.getAll')
|
||||
},
|
||||
|
||||
docker: {
|
||||
start: action('Start a Docker Container', 'docker.start', {
|
||||
argsMapper: (vm, container) => ({vm, container})
|
||||
}),
|
||||
stop: action('Stop a Docker Container', 'docker.stop', {
|
||||
argsMapper: (vm, container) => ({vm, container})
|
||||
}),
|
||||
pause: action('Pause a Docker Container', 'docker.pause', {
|
||||
argsMapper: (vm, container) => ({vm, container})
|
||||
}),
|
||||
unpause: action('Resume a Docker Container', 'docker.unpause', {
|
||||
argsMapper: (vm, container) => ({vm, container})
|
||||
}),
|
||||
restart: action('Restart a Docker Container', 'docker.restart', {
|
||||
argsMapper: (vm, container) => ({vm, container})
|
||||
})
|
||||
},
|
||||
|
||||
vm: {
|
||||
convert: action('Convert VM', 'vm.convert'),
|
||||
clone: action('Copy VM', 'vm.clone', {
|
||||
// todo : sr ref to choose target SR
|
||||
argsMapper: (id, name, full_copy) => ({id, name, full_copy})
|
||||
}),
|
||||
createSnapshot: action('Create VM snapshot', 'vm.snapshot', {
|
||||
argsMapper: (id, name) => ({id, name})
|
||||
}),
|
||||
export: action('Export VM', 'vm.export', {
|
||||
argsMapper: (vm, compress = true) => ({vm, compress})
|
||||
}),
|
||||
delete: action('Delete VM', 'vm.delete', {
|
||||
argsMapper: (id, delete_disks) => ({ id, delete_disks })
|
||||
}),
|
||||
ejectCd: action('Eject disc', 'vm.ejectCd'),
|
||||
insertCd: action('Insert disc', 'vm.insertCd', {
|
||||
argsMapper: (id, cd_id, force = false) => ({ id, cd_id, force })
|
||||
}),
|
||||
import: action('Import VM', 'vm.import', {
|
||||
argsMapper: (host) => ({ host })
|
||||
}),
|
||||
migrate: action('Migrate VM', 'vm.migrate', {
|
||||
argsMapper: (id, host_id) => ({ id, host_id })
|
||||
}),
|
||||
migratePool: action('Migrate VM to another pool', 'vm.migrate_pool'),
|
||||
restart: action('Restart VM', 'vm.restart', {
|
||||
argsMapper: (id, force = false) => ({ id, force })
|
||||
}),
|
||||
start: action('Start VM', 'vm.start'),
|
||||
stop: action('Stop VM', 'vm.stop', {
|
||||
argsMapper: (id, force = false) => ({ id, force })
|
||||
}),
|
||||
revert: action('Revert snapshot', 'vm.revert'),
|
||||
suspend: action('Suspend VM', 'vm.suspend'),
|
||||
resume: action('Resume VM', 'vm.resume', {
|
||||
argsMapper: (id, force = true) => ({ id, force })
|
||||
}),
|
||||
refreshStats: action('Get Stats', 'vm.stats', {
|
||||
notification: false
|
||||
}),
|
||||
// TODO: create/set/pause
|
||||
connectPci: action('Connect PCI device', 'vm.attachPci', {
|
||||
argsMapper: (vm, pciId) => ({vm, pciId})
|
||||
}),
|
||||
disconnectPci: action('Disconnect PCI device', 'vm.detachPci', {
|
||||
argsMapper: (vm) => ({vm})
|
||||
})
|
||||
},
|
||||
|
||||
vdi: {
|
||||
delete: action('Delete VDI', 'vdi.delete'),
|
||||
migrate: action('Migrate VDI', 'vdi.migrate', {
|
||||
argsMapper: (id, sr_id) => ({ id, sr_id })
|
||||
})
|
||||
},
|
||||
|
||||
vif: {
|
||||
delete: action('Delete VIF', 'vif.delete'),
|
||||
disconnect: action('Disconnect VIF', 'vif.disconnect'),
|
||||
connect: action('Connect VIF', 'vif.connect')
|
||||
},
|
||||
|
||||
vbd: {
|
||||
delete: action('Delete VBD', 'vbd.delete'),
|
||||
disconnect: action('Disconnect VBD', 'vbd.disconnect'),
|
||||
connect: action('Connect VBD', 'vbd.connect')
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the dynamic properties.
|
||||
Object.defineProperties(xo, {
|
||||
get: {
|
||||
get () {
|
||||
throw new Error('use xoApi.get() instead')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Returns the interface.
|
||||
return xo
|
||||
})
|
||||
.filter('xoHideUnauthorized', (xoApi) => {
|
||||
const {canAccess} = xoApi
|
||||
|
||||
return (objects) => filter(objects, canAccess)
|
||||
})
|
||||
.name
|
||||
8
app/node_modules/xo/package.json
generated
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,679 +0,0 @@
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Bootstrap
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Font-Awesome 4.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
$fa-font-path: "../";
|
||||
|
||||
@import "../../node_modules/font-awesome/scss/font-awesome";
|
||||
|
||||
// Replace Bootstrap's glyphicons by Font Awesome.
|
||||
.glyphicon {
|
||||
@extend .fa;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Angular Charts.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
@import "../../dist/bower_components/angular-chart.js/dist/angular-chart";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Angular xEditable.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
@import "../../dist/bower_components/angular-xeditable/dist/css/xeditable";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Angular Notify Toaster.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
@import "../../dist/bower_components/angular-notify-toaster/toaster";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// UI Select.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
@import "../../dist/bower_components/angular-ui-select/dist/select";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Style applied to a message for outdated browsers (IE <= 7).
|
||||
.browsehappy {
|
||||
margin: 0.2em 0;
|
||||
background: #cccccc;
|
||||
color: black;
|
||||
padding: 0.2em 0;
|
||||
}
|
||||
|
||||
// Some links are used throught JavaScript and therefore does not have
|
||||
// a `href` attribute, they should nevertheless have a *pointer*
|
||||
// cursor.
|
||||
// Same for all elements with the "ng-click" attribute.
|
||||
a, [ng-click], [xo-click], [xo-sref] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// TODO: We want all our form inputs to be styled by Bootstrap.
|
||||
//input, textarea, select
|
||||
// @extend .form-control
|
||||
|
||||
// Click-through notifications.
|
||||
#toast-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html, body, .view-main {
|
||||
height: 100%
|
||||
}
|
||||
|
||||
// Force our content to be under the fixed navbar.
|
||||
.view-main {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO: The styles in this file should be
|
||||
// - commented;
|
||||
// - converted to SASS;
|
||||
// - move into this file.
|
||||
@import "to-clean";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Colors
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
////
|
||||
// Power states
|
||||
////
|
||||
|
||||
.xo-color-running {
|
||||
@extend .text-success;
|
||||
}
|
||||
|
||||
.xo-color-halted {
|
||||
@extend .text-danger;
|
||||
}
|
||||
|
||||
.xo-color-paused, .xo-color-suspended {
|
||||
@extend .text-info;
|
||||
}
|
||||
|
||||
.xo-color-unknown {
|
||||
@extend .text-muted;
|
||||
}
|
||||
|
||||
.xo-color-pending {
|
||||
@extend .text-warning;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// XO icons
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
////
|
||||
// Alerts
|
||||
////
|
||||
|
||||
.xo-icon-info {
|
||||
@extend .fa;
|
||||
@extend .fa-exclamation-circle;
|
||||
@extend .text-muted;
|
||||
}
|
||||
|
||||
.xo-icon-docker {
|
||||
@extend .fa;
|
||||
@extend .fa-ship;
|
||||
@extend .text-info;
|
||||
}
|
||||
|
||||
.xo-icon-warning {
|
||||
@extend .fa;
|
||||
@extend .fa-exclamation-circle;
|
||||
@extend .text-warning;
|
||||
}
|
||||
|
||||
.xo-icon-error {
|
||||
@extend .fa;
|
||||
@extend .fa-exclamation-circle;
|
||||
@extend .text-danger;
|
||||
}
|
||||
|
||||
.xo-icon-success {
|
||||
@extend .fa;
|
||||
@extend .fa-check-circle;
|
||||
@extend .text-success;
|
||||
}
|
||||
|
||||
////
|
||||
// Objects
|
||||
////
|
||||
|
||||
.xo-icon-console {
|
||||
@extend .fa;
|
||||
@extend .fa-terminal;
|
||||
}
|
||||
|
||||
.xo-icon-pool {
|
||||
@extend .fa;
|
||||
@extend .fa-cloud;
|
||||
}
|
||||
|
||||
.xo-icon-host {
|
||||
@extend .fa;
|
||||
@extend .fa-server;
|
||||
}
|
||||
|
||||
.xo-icon-vm {
|
||||
@extend .fa;
|
||||
@extend .fa-desktop;
|
||||
}
|
||||
|
||||
.xo-icon-memory {
|
||||
@extend .fa;
|
||||
@extend .fa-sliders;
|
||||
}
|
||||
|
||||
.xo-icon-cpu {
|
||||
@extend .fa;
|
||||
@extend .fa-dashboard;
|
||||
}
|
||||
|
||||
.xo-icon-network {
|
||||
@extend .fa;
|
||||
@extend .fa-sitemap;
|
||||
}
|
||||
|
||||
.xo-icon-sr {
|
||||
@extend .fa;
|
||||
@extend .fa-hdd-o;
|
||||
}
|
||||
|
||||
.xo-icon-snapshot {
|
||||
@extend .fa;
|
||||
@extend .fa-camera;
|
||||
}
|
||||
|
||||
.xo-icon-task {
|
||||
@extend .fa;
|
||||
@extend .fa-tasks;
|
||||
}
|
||||
|
||||
.xo-icon-stats {
|
||||
@extend .fa;
|
||||
@extend .fa-line-chart;
|
||||
}
|
||||
|
||||
.xo-icon-user {
|
||||
@extend .fa;
|
||||
@extend .fa-user;
|
||||
}
|
||||
|
||||
.xo-icon-group {
|
||||
@extend .fa-users;
|
||||
}
|
||||
|
||||
////
|
||||
// Power states
|
||||
////
|
||||
|
||||
.xo-icon-running {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@extend .xo-color-running;
|
||||
}
|
||||
|
||||
.xo-icon-halted {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@extend .xo-color-halted;
|
||||
}
|
||||
|
||||
.xo-icon-suspended {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@extend .xo-color-paused;
|
||||
}
|
||||
|
||||
.xo-icon-paused {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@extend .xo-color-paused;
|
||||
}
|
||||
|
||||
.xo-icon-unknown {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@extend .xo-color-unknown;
|
||||
}
|
||||
|
||||
// if current operation
|
||||
.xo-icon-working {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@extend .text-warning;
|
||||
}
|
||||
|
||||
////
|
||||
// Others.
|
||||
////
|
||||
|
||||
.xo-icon-cpu-low {
|
||||
@extend .xo-icon-cpu;
|
||||
@extend .text-success;
|
||||
}
|
||||
|
||||
.xo-icon-cpu-medium {
|
||||
@extend .xo-icon-cpu;
|
||||
@extend .text-warning;
|
||||
}
|
||||
|
||||
.xo-icon-cpu-high {
|
||||
@extend .xo-icon-cpu;
|
||||
@extend .text-danger;
|
||||
}
|
||||
|
||||
////
|
||||
// Guest OS
|
||||
////
|
||||
|
||||
.xo-icon-linux {
|
||||
@extend .fa;
|
||||
@extend .fa-linux;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.xo-icon-windows {
|
||||
@extend .fa;
|
||||
@extend .fa-windows;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.xo-icon-other {
|
||||
@extend .fa;
|
||||
@extend .fa-question-circle;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Navbar
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// The login form
|
||||
.login-form {
|
||||
padding: 1em;
|
||||
width: 20em;
|
||||
// small space between fields
|
||||
.input-group {
|
||||
margin: 0.2em;
|
||||
}
|
||||
// adapt button for the global theme (dark/blue/grey)
|
||||
.input-group-addon {
|
||||
background-color: #3a87ad;
|
||||
color: #f8f8f8;
|
||||
border: 0;
|
||||
}
|
||||
button {
|
||||
margin-top: 1em;
|
||||
}
|
||||
// adapt form for the global theme
|
||||
.form-control {
|
||||
background-color: #666666;
|
||||
color: #f8f8f8;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// background dark for the whole form
|
||||
.login-form-dark {
|
||||
@extend .login-form;
|
||||
background-color: #2e3133;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Main view
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// FIXME: Class names are too generic and styles might be applied to
|
||||
// other views as well, all styles should be namespaced (e.g.
|
||||
// `.view-main`).
|
||||
|
||||
// Notice messages in place of the VMs list in the main page.
|
||||
.vms-notice {
|
||||
@extend .text-center;
|
||||
@extend .text-muted;
|
||||
font-size: 1.5em;
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.quick-buttons {
|
||||
display: inline;
|
||||
float: right;
|
||||
opacity: 0;
|
||||
a {
|
||||
color: #666666;
|
||||
margin: 0 0.3em;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Quick actions buttons are only visible when over their row.
|
||||
tr:hover .quick-buttons {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// remove icon/button in last row on a table
|
||||
.quick-remove {
|
||||
display: inline;
|
||||
float: right;
|
||||
opacity: 0;
|
||||
a {
|
||||
color: #666666;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove only visible when over its row
|
||||
tr:hover .quick-remove {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// edit icon/button in a panel title
|
||||
.quick-edit {
|
||||
display: inline;
|
||||
float: right;
|
||||
opacity: 1;
|
||||
color: #666666;
|
||||
&:hover {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
// Substats (less important host stats)
|
||||
.substats {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// Substat displayed on hover
|
||||
div.host-cell:hover .substats {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// Default SRs (where a new VM will be created).
|
||||
.default-sr {
|
||||
@extend .text-primary;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// Prevents a host name from overflowing outside its box.
|
||||
.host-cell, .pool-cell, .vm-cell {
|
||||
@extend .panel;
|
||||
& div {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.host-name, .pool-name, .sr-name {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 80%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Pool block
|
||||
// A block display a whole pool
|
||||
.pool-block {
|
||||
margin-right: 0.8em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pool-cell {
|
||||
// margin-bottom: 0.5em;
|
||||
// margin-left: 2em;
|
||||
// margin-right: 1em;
|
||||
min-height: 6em;
|
||||
max-width: 190px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* row for all hosts/vms in a pool */
|
||||
.hosts-vms-cells {
|
||||
min-height: 6em;
|
||||
margin-right: -0.8em;
|
||||
}
|
||||
|
||||
/* individual host element */
|
||||
.host-cell {
|
||||
max-width: 156px;
|
||||
background-color: white;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
/* With gutters */
|
||||
.grid--gutters {
|
||||
// margin-left: -0.5em;
|
||||
}
|
||||
.grid--gutters > .grid-cell {
|
||||
padding-left: 0.1em;
|
||||
}
|
||||
|
||||
/* vm name in table */
|
||||
.vm {
|
||||
display: inline;
|
||||
font-size: 10pt;
|
||||
margin: 0;
|
||||
}
|
||||
.no-vm {
|
||||
font-size: 1.5em;
|
||||
margin-top: 1.8em;
|
||||
color: #999;
|
||||
}
|
||||
.vm-cell table {
|
||||
margin: 0;
|
||||
}
|
||||
.vm-cell td {
|
||||
border-bottom: 1px solid #edece4 !important;
|
||||
border-top: 0px !important;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Pool view
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Allow progress bar in small spaces, like tables
|
||||
.progress-condensed {
|
||||
@extend .progress;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Host view
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Memory progress bar stacked for host
|
||||
.progress-bar-host {
|
||||
@extend .progress-bar;
|
||||
border-right: 1px solid white;
|
||||
}
|
||||
|
||||
// Memory progress bar stacked for VM
|
||||
.progress-bar-vm {
|
||||
@extend .progress-bar-success;
|
||||
border-right: 1px solid white;
|
||||
}
|
||||
|
||||
// Hover color for progress bar
|
||||
.progress-bar:hover {
|
||||
background-color: #e25440;
|
||||
}
|
||||
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.stat-simple {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// General object view
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
.panel {
|
||||
flex: 1;
|
||||
margin: 0.4em;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
text-align: center;
|
||||
font-variant: small-caps;
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.panel-body-stats {
|
||||
@extend .panel-body;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.chart-stat-preview {
|
||||
max-height: 8em;
|
||||
text-align: center;
|
||||
border-left: 1px solid #ddd;
|
||||
border-right: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.chart-stat-full {
|
||||
max-height: 16em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// display a big number for panel stats
|
||||
.big-stat {
|
||||
font-size: 5em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
// not so big stats
|
||||
.mid-stat {
|
||||
font-size: 2.5em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Flat view
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
.flat-object { @extend .panel; margin: 0.5em;}
|
||||
.flat-object:hover {background-color: #f8f8f8;}
|
||||
.flat-cell {border-radius: 0px; margin-left: 1em; margin-right: 1em; margin-top: 0.5em; margin-bottom: 0.5em;}
|
||||
.flat-cell-name {max-width: 150px; min-width: 150px; font-weight: bold;}
|
||||
.flat-cell-description {max-width: 250px; min-width: 250px;}
|
||||
.flat-cell-type {max-width: 40px; font-size: 2em; text-align: center; margin: auto; margin-left: 0.5em;}
|
||||
.flat-cell-tag {font-size: 11pt; margin: auto; min-height: 2.5em; padding-top: 0.4em; padding-left: 1em; opacity: 0.8;}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Login view
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
.row-login {
|
||||
@extend .panel;
|
||||
background: #fff;
|
||||
//padding: 20px 50px 20px 50px;
|
||||
padding: 1em 5em 1em 5em;
|
||||
max-width: 35em;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
img.navbar-logo {
|
||||
margin-top: -0.2em;
|
||||
display: inline;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-variant: small-caps;
|
||||
padding-top: 0.8em;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Settings
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
.settings-menu {
|
||||
background-color: #242628;
|
||||
border-right: 1px solid #eee;
|
||||
width: 212px;
|
||||
}
|
||||
|
||||
.settings-menu li a {
|
||||
font-size: 1em;
|
||||
padding-left: 2em;
|
||||
display: block;
|
||||
color: #f8f8f8;
|
||||
&:hover, &:focus {
|
||||
background-color: #2e3133;
|
||||
}
|
||||
}
|
||||
|
||||
// smaller settings menu on smaller screen
|
||||
@media (max-width: 970px) {
|
||||
.settings-menu {
|
||||
// 2em of padding + 2.29em of fa-fw
|
||||
width: 3.29em;
|
||||
}
|
||||
.settings-menu li a {
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
.menu-entry {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fa-menu {
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Object view
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// For table with only one line, e.g Docker list table etc
|
||||
.oneliner {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
/* Brand color */
|
||||
.navbar-brand {color: #bf0000;}
|
||||
|
||||
input.form-control.inverse {background-color: #666; color: #f8f8f8; border: 0px;}
|
||||
|
||||
/* Search bar */
|
||||
.popover { width: 200px; }
|
||||
/* search button in main bar */
|
||||
/* WARNING: hack because of strange Chrome behavior! Remove static height ASAP */
|
||||
.btn-search {background-color: #3a87ad; color: #f8f8f8; height: 34px;}
|
||||
.btn-search:hover {background-color: #1d4457; color: #f8f8f8;}
|
||||
|
||||
html {
|
||||
background-color: #edece4;
|
||||
/* Possibility to get a wallpaper for the background: see later in admin maybe, for the lulz */
|
||||
/*background: url(bg.jpg) no-repeat center center fixed;
|
||||
-webkit-background-size: cover;
|
||||
-moz-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
background-size: cover;*/
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.navbar-under { background-color: #f8f8f8;}
|
||||
|
||||
|
||||
td.vcenter {
|
||||
vertical-align: middle;
|
||||
}
|
||||
input[type="checkbox"]{
|
||||
padding-top: 0px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
input.inverse {margin: 0;}
|
||||
/* progress bar */
|
||||
.progress-small {
|
||||
|
||||
height: 0.6em;
|
||||
width: 4em;
|
||||
display: inline-block;
|
||||
float: center;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.progress-bar-black {
|
||||
color: black;
|
||||
}
|
||||
// FIXME: What is it?
|
||||
//.i-progress { float: center; margin-right: 0em;}
|
||||
|
||||
.grey {color: #666; font-size: 0.9em;}
|
||||
|
||||
/* drodown head link for pools/hosts */
|
||||
.dropdown-pool { border-bottom: 1px solid #edece4; text-align: center;}
|
||||
.dropdown-pool a {
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
.pool-name {font-size: 1.8em;}
|
||||
.host-name {font-size: 20px;}
|
||||
.big-caret {font-size: 1.4em;}
|
||||
|
||||
/* for dropdown menu not centered */
|
||||
.dropdown-menu {background-color: white;}
|
||||
.left {text-align: left;}
|
||||
|
||||
/* VM TABLE */
|
||||
/* grab zone on VM table, for drag and drop */
|
||||
td.grab {padding: 0 !important; margin: 0 !important; width: 6px !important; cursor: move;}
|
||||
|
||||
tr:hover .grab {background: url("../images/grip.png") no-repeat scroll 1px 50% transparent !important}
|
||||
|
||||
table { table-layout: fixed; }
|
||||
table th, table td { overflow: hidden; }
|
||||
|
||||
td.vm-power-state {width: 20px; text-align: center;}
|
||||
|
||||
td.select-vm {width: 1.5em; height: 20px;}
|
||||
|
||||
td.vm-memory-stat {text-align: right;}
|
||||
|
||||
|
||||
|
||||
/* the main bar */
|
||||
.navbar-inverse
|
||||
{
|
||||
background-color: #242628;
|
||||
border-color: #2e3133;
|
||||
//font-variant:small-caps;
|
||||
}
|
||||
.fa {font-variant: normal;}
|
||||
/* the big subbar */
|
||||
.sub-bar
|
||||
{
|
||||
height:50px;
|
||||
top:50px;
|
||||
position: fixed;
|
||||
background:#242628;
|
||||
border-bottom:1px solid #2e3133;
|
||||
width:100%;
|
||||
margin:0px auto;
|
||||
z-index: 1020;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Black theme: inversed colors for drop down menus */
|
||||
a.dropdown-toggle.inverse:hover {background-color:#2e3133 !important;}
|
||||
a.dropdown-toggle.inverse:active {background-color:#2e3133 !important;}
|
||||
a.dropdown-toggle.inverse:focus {background-color:#2e3133 !important;}
|
||||
ul.inverse {background:#2e3133;}
|
||||
ul.inverse a {color:#f8f8f8;}
|
||||
ul.inverse a:hover {background-color:#242628; color: #f8f8f8 ;}
|
||||
ul.inverse li.divider {background-color:#222;}
|
||||
|
||||
|
||||
/* Inversed (black) buttons */
|
||||
button.btn.navbar-btn.btn-default.inversed {background-color:#444; border-color: #222; color: #999999;}
|
||||
a.btn.navbar-btn.btn-default.dropdown-toggle.inversed {background-color:#444; border-color: #222; color: #999999; margin-left: 0.5em;}
|
||||
|
||||
/* change caret color for inversed button */
|
||||
.grey-caret {border-top-color: #999999 !important; border-bottom-color: #999999 !important;}
|
||||
|
||||
/* stats bar */
|
||||
.overview { padding: 1em; color: #a6a6a6 ; max-width: 346px;}
|
||||
.overview i {font-weight: normal;}
|
||||
.small {font-size: 10pt; font-style: normal;}
|
||||
.task-overview {
|
||||
@extend .overview;
|
||||
text-align: right;
|
||||
}
|
||||
.task-menu {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* stats pool and host */
|
||||
.stats {text-align: center; margin-top: 1em; margin-bottom: 0.4em; padding-bottom: 0.4em; }
|
||||
.stats a {text-decoration: none; color: #a6a6a6; margin-right: 0.6em;}
|
||||
.stats a:hover {text-decoration: none; color: #333;}
|
||||
.sr-name {font-size: 10pt;}
|
||||
/* "actionbar" which is select and buttons when one or more VM selected */
|
||||
.before-action-bar { margin-left: 1em;}
|
||||
.action-bar {display: inline;}
|
||||
|
||||
/* SR table (in host and pool) */
|
||||
.no-border {border-top: 0px !important;}
|
||||
/* ip display */
|
||||
|
||||
.cpu {display: inline;}
|
||||
.cpu a {text-decoration: none; color: #a6a6a6; margin-left: 0.9em;}
|
||||
.cpu a:hover {text-decoration: none; color: #333;}
|
||||
|
||||
/* tooltip hack to avoid be hidden by other elements */
|
||||
/*
|
||||
.tooltip {
|
||||
position: fixed;
|
||||
}
|
||||
*/
|
||||
|
||||
/* useful global class */
|
||||
.center {text-align: center;}
|
||||
.right {text-align: right;}
|
||||
.small-caps {font-variant:small-caps;}
|
||||
.big {font-size: 2em;}
|
||||
|
||||
|
||||
.grid-cell {
|
||||
flex: 1;
|
||||
// border-radius: 3px 3px 3px 3px;
|
||||
}
|
||||
|
||||
/* stats name in a grid cell */
|
||||
.stat-name {text-align: center; font-variant: small-caps; margin-bottom: -0.3em;}
|
||||
|
||||
.grid-button {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
padding: 0.5em;
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
.grid-button:hover {
|
||||
background-color: #e25440;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
border-bottom: 1px solid #FFFFFF;
|
||||
padding-bottom: 9px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-control-login {
|
||||
border: 1px solid #D6D6D6;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
height: 50px;
|
||||
padding: 6px 15px;
|
||||
}
|
||||
|
||||
|
||||
legend.login {
|
||||
border: medium none;
|
||||
color: #111;
|
||||
display: block;
|
||||
font-size: 20px;
|
||||
line-height: inherit;
|
||||
margin-bottom: 15px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
padding: 10px;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 2em;
|
||||
}
|
||||
16
bower.json
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "xo-web",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"angular-natural-sort": "https://bitbucket.org/OverZealous/angularjs-naturalsort/raw/f6e7ea30bf71324125549bd40a7cf9043abc2449/src/naturalSortVersionDates.js",
|
||||
"angular-notify-toaster": "~0.4.8",
|
||||
"angular-xeditable": "~0.1.8",
|
||||
"ng-file-upload": "~1.6.12",
|
||||
"angular-ui-select": "~0.9.9",
|
||||
"Chart.js": "~1.0.2",
|
||||
"angular-chart.js": "~0.5.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"angular": "~1.x"
|
||||
}
|
||||
}
|
||||
485
gulpfile.js
@@ -1,46 +1,15 @@
|
||||
// Julien Fontanet gulpfile.js
|
||||
//
|
||||
// https://gist.github.com/julien-f/4af9f3865513efeff6ab
|
||||
|
||||
'use strict'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
var gulp = require('gulp')
|
||||
|
||||
// All plugins are loaded (on demand) by gulp-load-plugins.
|
||||
var $ = require('gulp-load-plugins')()
|
||||
|
||||
var pipe = require('nice-pipe')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
var DIST_DIR = __dirname +'/dist'
|
||||
var SRC_DIR = __dirname +'/app'
|
||||
|
||||
// Bower directory is read from its configuration.
|
||||
var BOWER_DIR = (function () {
|
||||
var cfg
|
||||
|
||||
try {
|
||||
cfg = JSON.parse(require('fs').readFileSync(__dirname +'/.bowerrc'))
|
||||
} catch (error) {
|
||||
cfg = {}
|
||||
}
|
||||
|
||||
cfg.cwd || (cfg.cwd = __dirname)
|
||||
cfg.directory || (cfg.directory = 'bower_components')
|
||||
|
||||
return cfg.cwd +'/'+ cfg.directory
|
||||
})()
|
||||
|
||||
var PRODUCTION = process.argv.indexOf('--production') !== -1
|
||||
var SRC_DIR = __dirname + '/src' // eslint-disable-line no-path-concat
|
||||
var DIST_DIR = __dirname + '/dist' // eslint-disable-line no-path-concat
|
||||
|
||||
// Port to use for the livereload server.
|
||||
//
|
||||
// It must be available and if possible unique to not conflict with other projects.
|
||||
// http://www.random.org/integers/?num=1&min=1024&max=65535&col=1&base=10&format=plain&rnd=new
|
||||
var LIVERELOAD_PORT = 46417
|
||||
var LIVERELOAD_PORT = 26242
|
||||
|
||||
// Port to use for the embedded web server.
|
||||
//
|
||||
@@ -53,274 +22,304 @@ var SERVER_PORT = LIVERELOAD_PORT + 1
|
||||
// - `null` to make it accessible for the whole network
|
||||
var SERVER_ADDR = 'localhost'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
var PRODUCTION = process.env.NODE_ENV === 'production'
|
||||
var DEVELOPMENT = !PRODUCTION
|
||||
|
||||
// Create a noop duplex stream.
|
||||
var noop = function () {
|
||||
var PassThrough = require('stream').PassThrough
|
||||
|
||||
noop = function () {
|
||||
return new PassThrough({
|
||||
objectMode: true
|
||||
})
|
||||
}
|
||||
|
||||
return noop.apply(this, arguments)
|
||||
if (!process.env.XOA_PLAN) {
|
||||
process.env.XOA_PLAN = '5' // Open Source
|
||||
}
|
||||
|
||||
// Browserify plugin for gulp.js which uses watchify in development
|
||||
// mode.
|
||||
function browserify (path, opts) {
|
||||
opts || (opts = {})
|
||||
// ===================================================================
|
||||
|
||||
var bundler = require('browserify')({
|
||||
var gulp = require('gulp')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function lazyFn (factory) {
|
||||
var fn = function () {
|
||||
fn = factory()
|
||||
return fn.apply(this, arguments)
|
||||
}
|
||||
|
||||
return function () {
|
||||
return fn.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var livereload = lazyFn(function () {
|
||||
var livereload = require('gulp-refresh')
|
||||
livereload.listen({
|
||||
port: LIVERELOAD_PORT
|
||||
})
|
||||
|
||||
return livereload
|
||||
})
|
||||
|
||||
var pipe = lazyFn(function () {
|
||||
var current
|
||||
function pipeCore (streams) {
|
||||
var i, n, stream
|
||||
for (i = 0, n = streams.length; i < n; ++i) {
|
||||
stream = streams[i]
|
||||
if (!stream) {
|
||||
// Nothing to do
|
||||
} else if (stream instanceof Array) {
|
||||
pipeCore(stream)
|
||||
} else {
|
||||
current = current
|
||||
? current.pipe(stream)
|
||||
: stream
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var push = Array.prototype.push
|
||||
return function (streams) {
|
||||
try {
|
||||
if (!(streams instanceof Array)) {
|
||||
streams = []
|
||||
push.apply(streams, arguments)
|
||||
}
|
||||
|
||||
pipeCore(streams)
|
||||
|
||||
return current
|
||||
} finally {
|
||||
current = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var resolvePath = lazyFn(function () {
|
||||
return require('path').resolve
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Similar to `gulp.src()` but the pattern is relative to `SRC_DIR`
|
||||
// and files are automatically watched when not in production mode.
|
||||
var src = lazyFn(function () {
|
||||
function resolve (path) {
|
||||
return path
|
||||
? resolvePath(SRC_DIR, path)
|
||||
: SRC_DIR
|
||||
}
|
||||
|
||||
return PRODUCTION
|
||||
? function src (pattern, opts) {
|
||||
var base = resolve(opts && opts.base)
|
||||
|
||||
return gulp.src(pattern, {
|
||||
base: base,
|
||||
cwd: base,
|
||||
passthrough: opts && opts.passthrough,
|
||||
sourcemaps: opts && opts.sourcemaps
|
||||
})
|
||||
}
|
||||
: function src (pattern, opts) {
|
||||
var base = resolve(opts && opts.base)
|
||||
|
||||
return pipe(
|
||||
gulp.src(pattern, {
|
||||
base: base,
|
||||
cwd: base,
|
||||
passthrough: opts && opts.passthrough,
|
||||
sourcemaps: opts && opts.sourcemaps
|
||||
}),
|
||||
require('gulp-watch')(pattern, {
|
||||
base: base,
|
||||
cwd: base
|
||||
}),
|
||||
require('gulp-plumber')()
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Similar to `gulp.dest()` but the output directory is relative to
|
||||
// `DIST_DIR` and default to `./`, and files are automatically live-
|
||||
// reloaded when not in production mode.
|
||||
var dest = lazyFn(function () {
|
||||
function resolve (path) {
|
||||
return path
|
||||
? resolvePath(DIST_DIR, path)
|
||||
: DIST_DIR
|
||||
}
|
||||
|
||||
var opts = {
|
||||
sourcemaps: {
|
||||
path: '.'
|
||||
}
|
||||
}
|
||||
|
||||
return PRODUCTION
|
||||
? function dest (path) {
|
||||
return gulp.dest(resolve(path), opts)
|
||||
}
|
||||
: function dest (path) {
|
||||
var stream = gulp.dest(resolve(path), opts)
|
||||
stream.pipe(livereload())
|
||||
return stream
|
||||
}
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function browserify (path, opts) {
|
||||
if (opts == null) {
|
||||
opts = {}
|
||||
}
|
||||
|
||||
var bundler = require('browserify')(path, {
|
||||
basedir: SRC_DIR,
|
||||
debug: !PRODUCTION,
|
||||
entries: [path],
|
||||
debug: DEVELOPMENT, // TODO: enable also in production but need to make it work with gulp-uglify.
|
||||
extensions: opts.extensions,
|
||||
fullPaths: false,
|
||||
paths: SRC_DIR + '/common',
|
||||
standalone: opts.standalone,
|
||||
|
||||
// Required by Watchify.
|
||||
cache: {},
|
||||
packageCache: {},
|
||||
fullPaths: !PRODUCTION
|
||||
packageCache: {}
|
||||
})
|
||||
|
||||
if (!PRODUCTION) {
|
||||
var plugins = opts.plugins
|
||||
for (var i = 0, n = plugins && plugins.length; i < n; ++i) {
|
||||
var plugin = plugins[i]
|
||||
bundler.plugin(require(plugin[0]), plugin[1])
|
||||
}
|
||||
|
||||
if (PRODUCTION) {
|
||||
// FIXME: does not work with react-intl (?!)
|
||||
// bundler.plugin('bundle-collapser/plugin')
|
||||
} else {
|
||||
bundler = require('watchify')(bundler)
|
||||
bundler.plugin('bundle-collapser/plugin')
|
||||
}
|
||||
|
||||
// Append the extension if necessary.
|
||||
if (!/\.js$/.test(path)) {
|
||||
path += '.js'
|
||||
}
|
||||
path = resolvePath(SRC_DIR, path)
|
||||
|
||||
// Absolute path.
|
||||
path = require('path').resolve(SRC_DIR, path)
|
||||
|
||||
var proxy = noop()
|
||||
var stream = new (require('readable-stream'))({
|
||||
objectMode: true
|
||||
})
|
||||
|
||||
var write
|
||||
function bundle () {
|
||||
bundler.bundle(function onBundleComplete (err, buf) {
|
||||
if (err) {
|
||||
proxy.emit('error', err)
|
||||
bundler.bundle(function onBundle (error, buffer) {
|
||||
if (error) {
|
||||
stream.emit('error', error)
|
||||
return
|
||||
}
|
||||
|
||||
write(new (require('vinyl'))({
|
||||
base: SRC_DIR,
|
||||
path: path,
|
||||
contents: buf
|
||||
contents: buffer,
|
||||
path: path
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
if (PRODUCTION) {
|
||||
write = proxy.end.bind(proxy)
|
||||
write = function (data) {
|
||||
stream.push(data)
|
||||
stream.push(null)
|
||||
}
|
||||
} else {
|
||||
proxy = $.plumber().pipe(proxy)
|
||||
write = proxy.write.bind(proxy)
|
||||
stream = require('gulp-plumber')().pipe(stream)
|
||||
write = function (data) {
|
||||
stream.push(data)
|
||||
}
|
||||
|
||||
bundler.on('update', bundle)
|
||||
}
|
||||
bundle()
|
||||
|
||||
return proxy
|
||||
stream._read = function () {
|
||||
this._read = function () {}
|
||||
bundle()
|
||||
}
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
// Combine multiple streams together and can be handled as a single
|
||||
// stream.
|
||||
var combine = function () {
|
||||
// `event-stream` is required only when necessary to maximize
|
||||
// performance.
|
||||
combine = require('event-stream').pipe
|
||||
return combine.apply(this, arguments)
|
||||
}
|
||||
|
||||
// Merge multiple readable streams into a single one.
|
||||
var merge = function () {
|
||||
// `event-stream` is required only when necessary to maximize
|
||||
// performance.
|
||||
merge = require('event-stream').merge
|
||||
return merge.apply(this, arguments)
|
||||
}
|
||||
|
||||
// Similar to `gulp.src()` but the pattern is relative to `SRC_DIR`
|
||||
// and files are automatically watched when not in production mode.
|
||||
var src = (function () {
|
||||
var resolvePath = require('path').resolve
|
||||
function resolve (path) {
|
||||
if (path) {
|
||||
return resolvePath(SRC_DIR, path)
|
||||
}
|
||||
return SRC_DIR
|
||||
}
|
||||
|
||||
if (PRODUCTION) {
|
||||
return function src (pattern, base) {
|
||||
base = resolve(base)
|
||||
|
||||
return gulp.src(pattern, {
|
||||
base: base,
|
||||
cwd: base
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// gulp-plumber prevents streams from disconnecting when errors.
|
||||
// See: https://gist.github.com/floatdrop/8269868#file-thoughts-md
|
||||
return function src (pattern, base) {
|
||||
base = resolve(base)
|
||||
|
||||
return gulp.src(pattern, {
|
||||
base: base,
|
||||
cwd: base
|
||||
})
|
||||
.pipe($.watch(pattern, {
|
||||
base: base,
|
||||
cwd: base
|
||||
}))
|
||||
.pipe($.plumber())
|
||||
}
|
||||
})()
|
||||
|
||||
// Similar to `gulp.dest()` but the output directory is relative to
|
||||
// `DIST_DIR` and default to `./`, and files are automatically live-
|
||||
// reloaded when not in production mode.
|
||||
var dest = (function () {
|
||||
var resolvePath = require('path').resolve
|
||||
function resolve (path) {
|
||||
if (path) {
|
||||
return resolvePath(DIST_DIR, path)
|
||||
}
|
||||
return DIST_DIR
|
||||
}
|
||||
|
||||
if (PRODUCTION) {
|
||||
return function dest (path) {
|
||||
return gulp.dest(resolve(path))
|
||||
}
|
||||
}
|
||||
|
||||
var livereload = function () {
|
||||
$.livereload.listen(LIVERELOAD_PORT)
|
||||
|
||||
livereload = $.livereload
|
||||
return livereload()
|
||||
}
|
||||
|
||||
return function dest (path) {
|
||||
return combine(
|
||||
gulp.dest(resolve(path)),
|
||||
livereload()
|
||||
)
|
||||
}
|
||||
})()
|
||||
|
||||
// ===================================================================
|
||||
|
||||
gulp.task('buildPages', function buildPages () {
|
||||
return pipe([
|
||||
src('[i]ndex.jade'),
|
||||
$.jade(),
|
||||
!PRODUCTION && $.embedlr({ port: LIVERELOAD_PORT }),
|
||||
dest(),
|
||||
])
|
||||
gulp.task(function buildPages () {
|
||||
return pipe(
|
||||
src('index.jade', { sourcemaps: true }),
|
||||
require('gulp-jade')(),
|
||||
DEVELOPMENT && require('gulp-embedlr')({
|
||||
port: LIVERELOAD_PORT
|
||||
}),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
|
||||
gulp.task('buildScripts', [
|
||||
'installBowerComponents'
|
||||
], function buildScripts () {
|
||||
return pipe([
|
||||
browserify('./app.js', {
|
||||
extensions: '.coffee .jade'.split(' ')
|
||||
gulp.task(function buildScripts () {
|
||||
return pipe(
|
||||
browserify('./index.js', {
|
||||
plugins: [
|
||||
// ['css-modulesify', {
|
||||
['modular-css/browserify', {
|
||||
css: DIST_DIR + '/modules.css'
|
||||
}]
|
||||
]
|
||||
}),
|
||||
PRODUCTION && $.uglify({ mangle: false }),
|
||||
dest(),
|
||||
])
|
||||
PRODUCTION && require('gulp-uglify')(),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
|
||||
gulp.task('buildStyles', [
|
||||
'installBowerComponents'
|
||||
], function buildStyles () {
|
||||
return pipe([
|
||||
src('styles/[m]ain.scss'),
|
||||
!PRODUCTION && $.sourcemaps.init({
|
||||
loadMaps: true
|
||||
}),
|
||||
$.sass(),
|
||||
$.autoprefixer([
|
||||
gulp.task(function buildStyles () {
|
||||
return pipe(
|
||||
src('index.scss', { sourcemaps: true }),
|
||||
require('gulp-sass')(),
|
||||
require('gulp-autoprefixer')([
|
||||
'last 1 version',
|
||||
'> 1%'
|
||||
]),
|
||||
PRODUCTION && $.minifyCss(),
|
||||
!PRODUCTION && $.sourcemaps.write(),
|
||||
dest(),
|
||||
])
|
||||
PRODUCTION && require('gulp-csso')(),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
|
||||
gulp.task('copyAssets', [
|
||||
'installBowerComponents'
|
||||
], function copyAssets () {
|
||||
var imgStream
|
||||
if (PRODUCTION) {
|
||||
var imgFilter = $.filter('**/*.{gif,jpg,jpeg,png,svg}')
|
||||
|
||||
imgStream = combine(
|
||||
imgFilter,
|
||||
$.imagemin({
|
||||
progressive: true
|
||||
}),
|
||||
imgFilter.restore()
|
||||
)
|
||||
} else {
|
||||
imgStream = noop()
|
||||
}
|
||||
|
||||
return pipe([
|
||||
merge(
|
||||
src([
|
||||
'[f]avicon.ico',
|
||||
'images/**/*'
|
||||
]).pipe(imgStream),
|
||||
src(
|
||||
'fontawesome-webfont.*',
|
||||
__dirname + '/node_modules/font-awesome/fonts/'
|
||||
)
|
||||
),
|
||||
dest(),
|
||||
])
|
||||
gulp.task(function copyAssets () {
|
||||
return pipe(
|
||||
src(['assets/**/*', 'favicon.*']),
|
||||
src('fontawesome-webfont.*', {
|
||||
base: __dirname + '/node_modules/font-awesome/fonts', // eslint-disable-line no-path-concat
|
||||
passthrough: true
|
||||
}),
|
||||
src(['!*.css', 'font-mfizz.*'], {
|
||||
base: __dirname + '/node_modules/font-mfizz/dist', // eslint-disable-line no-path-concat
|
||||
passthrough: true
|
||||
}),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
|
||||
gulp.task('installBowerComponents', function installBowerComponents (done) {
|
||||
require('bower').commands.install()
|
||||
.on('error', done)
|
||||
.on('end', function () {
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
gulp.task('build', [
|
||||
gulp.task('build', gulp.parallel(
|
||||
'buildPages',
|
||||
'buildScripts',
|
||||
'buildStyles',
|
||||
'copyAssets'
|
||||
])
|
||||
))
|
||||
|
||||
gulp.task('clean', function clear (done) {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
gulp.task(function clean (done) {
|
||||
require('rimraf')(DIST_DIR, done)
|
||||
})
|
||||
|
||||
gulp.task('distclean', ['clean'], function distclean (done) {
|
||||
require('rimraf')(BOWER_DIR, done)
|
||||
})
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
gulp.task('server', function server (done) {
|
||||
gulp.task(function server (done) {
|
||||
require('connect')()
|
||||
.use(require('serve-static')(DIST_DIR))
|
||||
.listen(SERVER_PORT, SERVER_ADDR, function serverOnListen () {
|
||||
.listen(SERVER_PORT, SERVER_ADDR, function onListen () {
|
||||
var address = this.address()
|
||||
|
||||
var port = address.port
|
||||
@@ -331,13 +330,11 @@ gulp.task('server', function server (done) {
|
||||
address = '[' + address + ']'
|
||||
}
|
||||
|
||||
/* jshint devel: true*/
|
||||
console.log('Listening on http://' + address + ':' + port)
|
||||
})
|
||||
.on('close', function serverOnClose () {
|
||||
.on('error', done)
|
||||
.on('close', function onClose () {
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
gulp.task('default', ['build'])
|
||||
|
||||
17
node_modules/angular-file-upload.js
generated
vendored
@@ -1,17 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//====================================================================
|
||||
|
||||
/* global window: false */
|
||||
window.FileApi = {
|
||||
jsPath: '/bower_components/ng-file-upload/',
|
||||
staticPath: '/bower_components/ng-file-upload/',
|
||||
};
|
||||
|
||||
// Must be loaded before Angular.
|
||||
require('../dist/bower_components/ng-file-upload/angular-file-upload-shim');
|
||||
|
||||
require('angular');
|
||||
require('../dist/bower_components/ng-file-upload/angular-file-upload');
|
||||
|
||||
module.exports = 'angularFileUpload';
|
||||
4
node_modules/angular-natural-sort.js
generated
vendored
@@ -1,4 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
require('../dist/bower_components/angular-natural-sort/index');
|
||||
module.exports = 'naturalSort';
|
||||
4
node_modules/angular-notify-toaster.js
generated
vendored
@@ -1,4 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
require('../dist/bower_components/angular-notify-toaster/toaster.js');
|
||||
module.exports = 'toaster';
|
||||
7
node_modules/angular-ui-bootstrap.js
generated
vendored
@@ -1,7 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//====================================================================
|
||||
|
||||
require('angular-bootstrap');
|
||||
|
||||
module.exports = 'ui.bootstrap';
|
||||
4
node_modules/angular-ui-event.js
generated
vendored
@@ -1,4 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
require('./angular-ui-utils/modules/event/event');
|
||||
module.exports = 'ui.event';
|
||||
4
node_modules/angular-ui-indeterminate.js
generated
vendored
@@ -1,4 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
require('./angular-ui-utils/modules/indeterminate/indeterminate');
|
||||
module.exports = 'ui.indeterminate';
|
||||
5
node_modules/angular-ui-select.js
generated
vendored
@@ -1,5 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
require('../dist/bower_components/angular-ui-select/dist/select');
|
||||
|
||||
module.exports = 'ui.select';
|
||||
4
node_modules/angular-xeditable.js
generated
vendored
@@ -1,4 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
require('../dist/bower_components/angular-xeditable/dist/js/xeditable');
|
||||
module.exports = 'xeditable';
|
||||
8
node_modules/angular.js
generated
vendored
@@ -1,8 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//====================================================================
|
||||
|
||||
// Force Angular to use jQuery.
|
||||
require('jquery');
|
||||
|
||||
module.exports = require('./angular/index.js');
|
||||
10
node_modules/jquery.js
generated
vendored
@@ -1,10 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//====================================================================
|
||||
|
||||
var jQuery = require('./jquery/dist/jquery');
|
||||
|
||||
/* global window: false */
|
||||
window.jQuery = window.$ = jQuery;
|
||||
|
||||
module.exports = jQuery;
|
||||
214
package.json
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "4.0.1",
|
||||
"version": "5.0.5",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -9,67 +10,8 @@
|
||||
"xen-orchestra",
|
||||
"web"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@julien-f/json-rpc": "^0.4.3",
|
||||
"angular": "^1.4.0",
|
||||
"angular-animate": "^1.4.0",
|
||||
"angular-bootstrap": "^0.12.0",
|
||||
"angular-cookies": "^1.4.0",
|
||||
"angular-ui-router": "^0.2.13",
|
||||
"angular-ui-utils": "^0.1.1",
|
||||
"ansi_up": "^1.1.3",
|
||||
"babelify": "^6.0.2",
|
||||
"bluebird": "^2.9.14",
|
||||
"bootstrap-sass": "^3.3.4",
|
||||
"bower": "^1.3.12",
|
||||
"browserify": "^10.2.1",
|
||||
"browserify-plain-jade": "^0.2.2",
|
||||
"bundle-collapser": "^1.1.4",
|
||||
"coffeeify": "^1.0.0",
|
||||
"event-stream": "^3.3.0",
|
||||
"font-awesome": "^4.3.0",
|
||||
"gulp": "^3.8.11",
|
||||
"gulp-autoprefixer": "^2.1.0",
|
||||
"gulp-embedlr": "^0.5.2",
|
||||
"gulp-filter": "^2.0.2",
|
||||
"gulp-imagemin": "^2.2.1",
|
||||
"gulp-jade": "^1.0.0",
|
||||
"gulp-livereload": "^3.8.0",
|
||||
"gulp-load-plugins": "^0.10.0",
|
||||
"gulp-minify-css": "^1.1.1",
|
||||
"gulp-plumber": "^1.0.0",
|
||||
"gulp-sass": "^2.0.1",
|
||||
"gulp-sourcemaps": "^1.5.2",
|
||||
"gulp-uglify": "^1.1.0",
|
||||
"gulp-watch": "^4.2.0",
|
||||
"in-publish": "^1.1.1",
|
||||
"jquery": "^2.1.3",
|
||||
"lodash.assign": "^3.1.0",
|
||||
"lodash.clonedeep": "^3.0.1",
|
||||
"lodash.difference": "^3.0.1",
|
||||
"lodash.filter": "^3.0.0",
|
||||
"lodash.foreach": "^3.0.3",
|
||||
"lodash.includes": "^3.1.1",
|
||||
"lodash.indexof": "^3.0.2",
|
||||
"lodash.intersection": "^3.1.0",
|
||||
"lodash.isempty": "^3.0.3",
|
||||
"lodash.map": "^3.1.2",
|
||||
"lodash.omit": "^3.1.0",
|
||||
"lodash.slice": "^3.0.0",
|
||||
"lodash.sortby": "^3.1.0",
|
||||
"lodash.sum": "^3.6.1",
|
||||
"lodash.throttle": "^3.0.1",
|
||||
"make-error": "^1.0.2",
|
||||
"nice-pipe": "^0.2.2",
|
||||
"novnc-node": "^0.5.1",
|
||||
"rimraf": "^2.3.2",
|
||||
"socket.io-client": "^1.3.5",
|
||||
"vinyl": "^0.4.6",
|
||||
"watchify": "^3.1.1",
|
||||
"ws": "^0.7.2",
|
||||
"xo-collection": "^0.3.2",
|
||||
"xo-lib": "^0.7.2"
|
||||
},
|
||||
"homepage": "https://github.com/vatesfr/xo-web",
|
||||
"bugs": "https://github.com/vatesfr/xo-web/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xo-web"
|
||||
@@ -78,27 +20,155 @@
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
"node": ">=4",
|
||||
"npm": ">=3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ansi_up": "^1.3.0",
|
||||
"asap": "^2.0.4",
|
||||
"ava": "^0.15.0",
|
||||
"babel-eslint": "^6.0.0",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-react-constant-elements": "^6.5.0",
|
||||
"babel-plugin-transform-react-inline-elements": "^6.6.5",
|
||||
"babel-plugin-transform-runtime": "^6.6.0",
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"babel-runtime": "^6.6.1",
|
||||
"babelify": "^7.2.0",
|
||||
"benchmark": "^2.1.0",
|
||||
"bootstrap": "github:twbs/bootstrap#v4-dev",
|
||||
"browserify": "^13.0.0",
|
||||
"bundle-collapser": "^1.2.1",
|
||||
"chartist-plugin-legend": "^0.3.1",
|
||||
"chartist-plugin-tooltip": "0.0.11",
|
||||
"classnames": "^2.2.3",
|
||||
"connect": "^3.4.1",
|
||||
"cookies-js": "^1.2.2",
|
||||
"d3": "^4.0.0-alpha.50",
|
||||
"dependency-check": "^2.5.1",
|
||||
"font-awesome": "^4.5.0",
|
||||
"font-mfizz": "github:fizzed/font-mfizz",
|
||||
"ghooks": "^1.1.1",
|
||||
"globby": "^5.0.0",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-autoprefixer": "^3.1.0",
|
||||
"gulp-csso": "^2.0.0",
|
||||
"gulp-embedlr": "^0.5.2",
|
||||
"gulp-jade": "^1.1.0",
|
||||
"gulp-plumber": "^1.1.0",
|
||||
"gulp-refresh": "^1.1.0",
|
||||
"gulp-sass": "^2.2.0",
|
||||
"gulp-uglify": "^1.5.3",
|
||||
"gulp-watch": "^4.3.5",
|
||||
"human-format": "^0.6.0",
|
||||
"jsonrpc-websocket-client": "0.0.1-5",
|
||||
"later": "^1.2.0",
|
||||
"lodash": "^4.6.1",
|
||||
"loose-envify": "^1.1.0",
|
||||
"marked": "^0.3.5",
|
||||
"modular-css": "^0.22.1",
|
||||
"moment": "^2.13.0",
|
||||
"notifyjs": "^2.0.1",
|
||||
"novnc-node": "^0.5.3",
|
||||
"promise-toolbox": "^0.4.0",
|
||||
"random-password": "^0.1.2",
|
||||
"react": "^15.0.0",
|
||||
"react-addons-shallow-compare": "^15.1.0",
|
||||
"react-bootstrap-4": "^0.29.1",
|
||||
"react-chartist": "^0.10.1",
|
||||
"react-copy-to-clipboard": "^4.0.2",
|
||||
"react-debounce-input": "^2.4.0",
|
||||
"react-dnd": "^2.1.4",
|
||||
"react-dnd-html5-backend": "^2.1.2",
|
||||
"react-dom": "^15.0.0",
|
||||
"react-dropzone": "^3.5.0",
|
||||
"react-intl": "^2.0.1",
|
||||
"react-key-handler": "^0.2.0",
|
||||
"react-notify": "^2.0.1",
|
||||
"react-redux": "^4.4.0",
|
||||
"react-router": "^3.0.0-alpha.1",
|
||||
"react-select": "^1.0.0-beta13",
|
||||
"react-sparklines": "^1.5.0",
|
||||
"react-virtualized": "^7.4.0",
|
||||
"readable-stream": "^2.0.6",
|
||||
"redux": "^3.3.1",
|
||||
"redux-devtools": "^3.1.1",
|
||||
"redux-devtools-dock-monitor": "^1.1.0",
|
||||
"redux-devtools-log-monitor": "^1.0.5",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.2.1",
|
||||
"serve-static": "^1.10.2",
|
||||
"standard": "^7.0.0",
|
||||
"superagent": "^2.0.0",
|
||||
"vinyl": "^1.1.1",
|
||||
"watchify": "^3.7.0",
|
||||
"xo-acl-resolver": "^0.2.1",
|
||||
"xo-lib": "^0.8.0-1",
|
||||
"xo-remote-parser": "^0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp distclean && gulp build --production",
|
||||
"dev": "gulp build",
|
||||
"prepublish": "in-publish && npm run build || in-install"
|
||||
"benchmarks": "./tools/run-benchmarks.js 'src/**/*.bench.js'",
|
||||
"build": "npm run build-indexes && NODE_ENV=production gulp build",
|
||||
"build-indexes": "./tools/generate-index src/common/intl/locales",
|
||||
"dev": "npm run build-indexes && gulp build server",
|
||||
"dev-test": "ava --watch",
|
||||
"lint": "standard",
|
||||
"posttest": "npm run lint",
|
||||
"prepublish": "npm run build",
|
||||
"test": "ava"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify",
|
||||
"browserify-plain-jade",
|
||||
"coffeeify"
|
||||
"loose-envify"
|
||||
]
|
||||
},
|
||||
"ava": {
|
||||
"babel": "inherit",
|
||||
"files": [
|
||||
"src/**/*.spec.js"
|
||||
],
|
||||
"require": [
|
||||
"babel-register"
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"env": {
|
||||
"production": {
|
||||
"plugins": [
|
||||
"transform-react-constant-elements",
|
||||
"transform-react-inline-elements"
|
||||
]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"transform-decorators-legacy",
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
"es2015",
|
||||
"react",
|
||||
"stage-0"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"ghooks": {
|
||||
"commit-msg": "npm test"
|
||||
}
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
]
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
|
||||
1
src/assets/loading.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><svg width='62px' height='62px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-ring-alt"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><circle cx="50" cy="50" r="40" stroke="#cfcfcf" fill="none" stroke-width="10" stroke-linecap="round"></circle><circle cx="50" cy="50" r="40" stroke="#366e98" fill="none" stroke-width="6" stroke-linecap="round"><animate attributeName="stroke-dashoffset" dur="1s" repeatCount="indefinite" from="0" to="502"></animate><animate attributeName="stroke-dasharray" dur="1s" repeatCount="indefinite" values="150.6 100.4;1 250;150.6 100.4"></animate></circle></svg>
|
||||
|
After Width: | Height: | Size: 707 B |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
123
src/chartist.scss
Normal file
@@ -0,0 +1,123 @@
|
||||
|
||||
// CHARTIST ===================================================================
|
||||
|
||||
$ct-series-colors: (
|
||||
$brand-success,
|
||||
$brand-primary,
|
||||
#60bd68,
|
||||
#f17cb0,
|
||||
#b2912f,
|
||||
#b276b2,
|
||||
#decf3f,
|
||||
#f15854,
|
||||
#4d4d4d,
|
||||
#dda458,
|
||||
#eacf7d,
|
||||
#86797d,
|
||||
#b2c326,
|
||||
#6188e2,
|
||||
#a748ca
|
||||
) !default;
|
||||
|
||||
@import "../node_modules/chartist/dist/scss/settings/_chartist-settings";
|
||||
@import "../node_modules/chartist/dist/scss/chartist";
|
||||
|
||||
// Line in charts with only 2px in width
|
||||
.ct-line {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.ct-bar {
|
||||
stroke-width: 10%;
|
||||
}
|
||||
|
||||
.ct-point {
|
||||
stroke-width: 30px;
|
||||
stroke-opacity: 0!important;
|
||||
}
|
||||
|
||||
.ct-point:hover {
|
||||
stroke-opacity: 0.2!important;
|
||||
stroke-width: 20px;
|
||||
}
|
||||
|
||||
.ct-tooltip {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
min-width: 5em;
|
||||
padding: 8px 10px;
|
||||
background: #383838;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
font-weight: 700;
|
||||
|
||||
// Arrow!
|
||||
&:before {
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
border: solid transparent;
|
||||
content: '';
|
||||
height: 0;
|
||||
width: 0;
|
||||
pointer-events: none;
|
||||
border-color: rgba(251, 249, 228, 0);
|
||||
border-top-color: #383838;
|
||||
border-width: 7px;
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
&.hide {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// CHARTIST LEGEND =============================================================
|
||||
|
||||
.ct-legend {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin-bottom: -1em;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
padding-left: 1.4em;
|
||||
list-style-type: none;
|
||||
display: inline;
|
||||
margin-right: 0.5em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
li:before {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
content: '';
|
||||
border: 3px solid transparent;
|
||||
border-radius: 2px;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
li.inactive:before {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.ct-legend-inside {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@for $i from 0 to length($ct-series-colors) {
|
||||
.ct-series-#{$i}:before {
|
||||
background-color: nth($ct-series-colors, $i + 1);
|
||||
border-color: nth($ct-series-colors, $i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/common/action-bar.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// import _ from 'intl' TODO: fix tooltip
|
||||
import ActionButton from 'action-button'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
// import Tooltip from 'tooltip' TODO: fix tooltip
|
||||
import {
|
||||
ButtonGroup
|
||||
} from 'react-bootstrap-4/lib'
|
||||
|
||||
const ActionBar = ({ actions, param }) => (
|
||||
<ButtonGroup>
|
||||
{map(actions, ({ handler, handlerParam = param, label, icon }, index) => (
|
||||
/* <Tooltip key={index} content={_(label)}> TODO: fix tooltip */
|
||||
<ActionButton
|
||||
key={index}
|
||||
btnStyle='secondary'
|
||||
handler={handler}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
size='large'
|
||||
/>
|
||||
/* </Tooltip> */
|
||||
))}
|
||||
</ButtonGroup>
|
||||
)
|
||||
ActionBar.propTypes = {
|
||||
actions: React.PropTypes.arrayOf(
|
||||
React.PropTypes.shape({
|
||||
label: React.PropTypes.string.isRequired,
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
handler: React.PropTypes.func
|
||||
})
|
||||
).isRequired,
|
||||
display: React.PropTypes.oneOf(['icon', 'text', 'both'])
|
||||
}
|
||||
export { ActionBar as default }
|
||||
119
src/common/action-button.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import Icon from 'icon'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import React from 'react'
|
||||
import { Button } from 'react-bootstrap-4/lib'
|
||||
|
||||
import Component from './base-component'
|
||||
import logError from './log-error'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
@propTypes({
|
||||
btnStyle: propTypes.string,
|
||||
disabled: propTypes.bool,
|
||||
form: propTypes.string,
|
||||
handler: propTypes.func.isRequired,
|
||||
handlerParam: propTypes.any,
|
||||
icon: propTypes.string.isRequired,
|
||||
redirectOnSuccess: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
]),
|
||||
size: propTypes.oneOf([
|
||||
'large',
|
||||
'small'
|
||||
])
|
||||
})
|
||||
export default class ActionButton extends Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
|
||||
async _execute () {
|
||||
if (this.state.working) {
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
handler,
|
||||
handlerParam
|
||||
} = this.props
|
||||
|
||||
try {
|
||||
this.setState({
|
||||
error: null,
|
||||
working: true
|
||||
})
|
||||
|
||||
const result = await handler(handlerParam)
|
||||
|
||||
let { redirectOnSuccess } = this.props
|
||||
if (redirectOnSuccess) {
|
||||
if (isFunction(redirectOnSuccess)) {
|
||||
redirectOnSuccess = redirectOnSuccess(result)
|
||||
}
|
||||
return this.context.router.push(redirectOnSuccess)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
working: false
|
||||
})
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
error,
|
||||
working: false
|
||||
})
|
||||
logError(error)
|
||||
}
|
||||
}
|
||||
_execute = ::this._execute
|
||||
|
||||
_eventListener = event => {
|
||||
event.preventDefault()
|
||||
this._execute()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { form } = this.props
|
||||
|
||||
if (form) {
|
||||
document.getElementById(form).addEventListener('submit', this._eventListener)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { form } = this.props
|
||||
|
||||
if (form) {
|
||||
document.getElementById(form).removeEventListener('submit', this._eventListener)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
props: {
|
||||
btnStyle,
|
||||
children,
|
||||
className,
|
||||
disabled,
|
||||
form,
|
||||
icon,
|
||||
size: bsSize,
|
||||
style
|
||||
},
|
||||
state: { error, working }
|
||||
} = this
|
||||
|
||||
return <Button
|
||||
bsStyle={error ? 'warning' : btnStyle}
|
||||
form={form}
|
||||
onClick={!form && this._execute}
|
||||
disabled={working || disabled}
|
||||
type={form ? 'submit' : 'button'}
|
||||
{...{ bsSize, className, style }}
|
||||
>
|
||||
<Icon icon={working ? 'loading' : icon} fixedWidth />
|
||||
{children && ' '}
|
||||
{children}
|
||||
</Button>
|
||||
}
|
||||
}
|
||||
7
src/common/action-row-button/index.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.button {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
tr:hover .button, tr:focus .button {
|
||||
opacity: 1;
|
||||
}
|
||||
14
src/common/action-row-button/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
import ActionButton from '../action-button'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const ActionRowButton = props => (
|
||||
<ActionButton
|
||||
{...props}
|
||||
className={styles.button}
|
||||
size='small'
|
||||
/>
|
||||
)
|
||||
export { ActionRowButton as default }
|
||||