Compare commits
617 Commits
xo-web/v3.
...
v5.x.all.i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93e7bdaeb0 | ||
|
|
877e471b10 | ||
|
|
32478e470b | ||
|
|
ccbcaa94fe | ||
|
|
0ffa9d4225 | ||
|
|
8e5dee79e0 | ||
|
|
9abd9d20ec | ||
|
|
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 | ||
|
|
a55e7ed34f | ||
|
|
e355e4d35d | ||
|
|
6dcaf80f3f | ||
|
|
a465114d36 | ||
|
|
07fbcb3488 | ||
|
|
534fbe1b6e | ||
|
|
f5c9c1ba0e | ||
|
|
5d5485f569 | ||
|
|
3d3fa5d18a | ||
|
|
312c41f229 | ||
|
|
2df1dc9028 | ||
|
|
222f245e63 | ||
|
|
2aa7702aed | ||
|
|
0b185c35c2 | ||
|
|
48dcec3cc3 | ||
|
|
8567179fa3 | ||
|
|
79d15ecd7e | ||
|
|
837c7e4bc7 | ||
|
|
2ae7e9920d | ||
|
|
8cf955b674 | ||
|
|
33f897d43e | ||
|
|
ddb0946a0d | ||
|
|
0f5beac4a8 | ||
|
|
974e2f71f9 | ||
|
|
3c427d7e28 | ||
|
|
0f10c8f5df | ||
|
|
7840b51f5c | ||
|
|
6578855182 | ||
|
|
58d68497a4 | ||
|
|
bddcf42a54 | ||
|
|
6318f4e7ac | ||
|
|
0c6cced7ee | ||
|
|
925bf47c9e | ||
|
|
8472b991ff | ||
|
|
ed59c32d96 | ||
|
|
b1981d7499 | ||
|
|
8983dfea57 | ||
|
|
5231b9b22b | ||
|
|
55846a2314 | ||
|
|
1c94f5749d | ||
|
|
90bacd9d31 | ||
|
|
0053cbf782 | ||
|
|
5d120a79e8 | ||
|
|
3389569ea0 | ||
|
|
f546606de0 | ||
|
|
fef95b3aae | ||
|
|
5ba2b72439 | ||
|
|
4bb849f7c9 | ||
|
|
21b5e7e701 | ||
|
|
34a1965497 | ||
|
|
1701682636 | ||
|
|
5d826972f1 | ||
|
|
2467b336e5 | ||
|
|
4f78414c7f | ||
|
|
4532714bae | ||
|
|
352c23b0ba | ||
|
|
8e432ee818 | ||
|
|
47bb2d24f5 | ||
|
|
f3fd4c607d | ||
|
|
0610ceafdf | ||
|
|
032fcdce40 | ||
|
|
636bacd637 | ||
|
|
3f3fbd8bbc | ||
|
|
955e88b4fb | ||
|
|
5954b552c9 | ||
|
|
aaad4c5d20 | ||
|
|
a24c8526ea | ||
|
|
a533535520 | ||
|
|
badded3aa4 | ||
|
|
3055e612d4 | ||
|
|
525cb1a2b6 | ||
|
|
4dd70abc3b | ||
|
|
2ea4c214df | ||
|
|
0a0174a79d | ||
|
|
3db031be1b | ||
|
|
6d3a87fe7d | ||
|
|
8cfd2cdd79 | ||
|
|
9e874e076f | ||
|
|
28192bf184 | ||
|
|
a54957b4de | ||
|
|
f4b1a076b7 | ||
|
|
27a3296d6e | ||
|
|
1aaaee128f | ||
|
|
15a16a2c35 | ||
|
|
db23fe5a58 | ||
|
|
620c88b615 | ||
|
|
99f2fb9764 | ||
|
|
d5a3e67dbd | ||
|
|
55ef81f3e7 | ||
|
|
41699fab1e | ||
|
|
32a1195157 | ||
|
|
f53db2ddfa | ||
|
|
e060f9172b | ||
|
|
4adef88e61 | ||
|
|
d734f2cf89 | ||
|
|
3e81d14bd8 | ||
|
|
e88a94d9e0 | ||
|
|
f4f16e4e87 | ||
|
|
6268f3a3d9 | ||
|
|
06e7c8d19a | ||
|
|
32395232ea | ||
|
|
65d6ef91ff | ||
|
|
4aecc875d1 | ||
|
|
0e649a626c | ||
|
|
5fa249b0f3 | ||
|
|
24ca86aad3 | ||
|
|
8a4f413289 | ||
|
|
6dbad4501d | ||
|
|
9ab6490fee | ||
|
|
a413efa550 | ||
|
|
cd337d444c | ||
|
|
45e1ce0a42 | ||
|
|
e5ef1e6efe | ||
|
|
b1ce3be3d2 | ||
|
|
e13ab73a29 | ||
|
|
aede952b12 | ||
|
|
acc1476b29 | ||
|
|
138bf56624 | ||
|
|
c608de4183 | ||
|
|
ccb6c02c31 | ||
|
|
5cc457b28c | ||
|
|
a353b3d40d | ||
|
|
6f7aca8e5b | ||
|
|
92b0d4561e | ||
|
|
ef8b8346dc | ||
|
|
058058a015 | ||
|
|
fddba7315a | ||
|
|
a5e964ea19 | ||
|
|
3d2152e559 | ||
|
|
50f9c68c26 | ||
|
|
b40207b367 | ||
|
|
6c9305d2b1 | ||
|
|
9fda3c911d | ||
|
|
473c3601ef | ||
|
|
fde8a3720d | ||
|
|
13a6d6b458 | ||
|
|
29d9ba0446 | ||
|
|
71e271774e | ||
|
|
c9db49e255 | ||
|
|
22f35f0e86 | ||
|
|
375f3ac3ac | ||
|
|
b60a02bc34 | ||
|
|
5a4d821c98 | ||
|
|
cd0305c71d | ||
|
|
371459ff5e | ||
|
|
5a8a7c6a0f | ||
|
|
69db541300 | ||
|
|
94949866ee | ||
|
|
c22b3e7449 | ||
|
|
096dde922b | ||
|
|
00f26d854f | ||
|
|
6c8ff1717e | ||
|
|
c7288c1d8a | ||
|
|
2e52fe369d | ||
|
|
df3430add5 | ||
|
|
7af848c94b | ||
|
|
f57c462b5f | ||
|
|
6018035908 | ||
|
|
c8b0351786 | ||
|
|
26cc812f82 | ||
|
|
67f98950e6 | ||
|
|
8ba8537b9f | ||
|
|
a7f05a68e0 | ||
|
|
a0db228154 | ||
|
|
eec6fabe58 | ||
|
|
501c038f97 | ||
|
|
e0a0f717fd | ||
|
|
4dd3f7487c | ||
|
|
99d1cddaa5 | ||
|
|
2158e1a47e | ||
|
|
059238759a | ||
|
|
8d3ea7548a | ||
|
|
221b411b63 | ||
|
|
e2c173990f | ||
|
|
a609a8d5d6 | ||
|
|
c5c2afddc2 | ||
|
|
409d87f210 | ||
|
|
78baa4b01e | ||
|
|
b41c66c4af | ||
|
|
d6b7388c2e | ||
|
|
b0dc681d48 | ||
|
|
c20b460f2d | ||
|
|
cd0a46fd7f | ||
|
|
0a9c868678 | ||
|
|
027d1e8cb1 | ||
|
|
a5c9880318 |
12
.babelrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"comments": false,
|
||||
"compact": true,
|
||||
"plugins": [
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
"es2015",
|
||||
"stage-0",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
@@ -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/*/
|
||||
|
||||
94
.jshintrc
@@ -1,94 +0,0 @@
|
||||
{
|
||||
// Julien Fontanet JSHint configuration
|
||||
// https://gist.github.com/julien-f/8095615
|
||||
//
|
||||
// Changes from defaults:
|
||||
// - all enforcing options (except `++` & `--`) enabled
|
||||
// - single quotes
|
||||
// - indentation set to 2 instead of 4
|
||||
// - almost all relaxing options disabled
|
||||
// - allow expression statements (necessary for chai.expect())
|
||||
// - environments are set to Browserify, mocha & Node.js
|
||||
//
|
||||
// See http://jshint.com/docs/ for more details
|
||||
|
||||
"maxerr" : 50, // {int} Maximum error before stopping
|
||||
|
||||
// Enforcing
|
||||
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
|
||||
"camelcase" : true, // true: Identifiers must be in camelCase
|
||||
"curly" : true, // true: Require {} for every new block or scope
|
||||
"eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
|
||||
"freeze" : true, // true: Prohibit overwriting prototypes of native objects (Array, Date, ...)
|
||||
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
|
||||
"indent" : 2, // {int} Number of spaces to use for indentation
|
||||
"latedef" : true, // true: Require variables/functions to be defined before being used
|
||||
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
|
||||
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
|
||||
"noempty" : true, // true: Prohibit use of empty blocks
|
||||
"nonbsp" : true, // true: Prohibit use of non breakable spaces
|
||||
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
|
||||
"plusplus" : false, // true: Prohibit use of `++` & `--`
|
||||
"quotmark" : "single", // Quotation mark consistency:
|
||||
// false : do nothing (default)
|
||||
// true : ensure whatever is used is consistent
|
||||
// "single" : require single quotes
|
||||
// "double" : require double quotes
|
||||
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
|
||||
"unused" : true, // true: Require all defined variables be used
|
||||
"strict" : false, // true: Requires all functions run in ES5 Strict Mode
|
||||
"maxcomplexity" : 7, // {int} Max cyclomatic complexity per function
|
||||
"maxdepth" : 3, // {int} Max depth of nested blocks (within functions)
|
||||
"maxlen" : 80, // {int} Max number of characters per line
|
||||
"maxparams" : 4, // {int} Max number of formal params allowed per function
|
||||
"maxstatements" : 20, // {int} Max number statements per function
|
||||
|
||||
// Relaxing
|
||||
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
|
||||
"boss" : false, // true: Tolerate assignments where comparisons would be expected
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
"eqnull" : false, // true: Tolerate use of `== null`
|
||||
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
|
||||
"funcscope" : false, // true: Tolerate defining variables inside control statements
|
||||
"globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
|
||||
"iterator" : false, // true: Tolerate using the `__iterator__` property
|
||||
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
|
||||
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
|
||||
"laxcomma" : false, // true: Tolerate comma-first style coding
|
||||
"loopfunc" : false, // true: Tolerate functions being defined in loops
|
||||
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// (ex: `for each`, multiple try/catch, function expression…)
|
||||
"multistr" : false, // true: Tolerate multi-line strings
|
||||
"notypeof" : false, // true: Tolerate typeof comparison with unknown values.
|
||||
"proto" : false, // true: Tolerate using the `__proto__` property
|
||||
"scripturl" : false, // true: Tolerate script-targeted URLs
|
||||
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
|
||||
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
|
||||
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
|
||||
"validthis" : false, // true: Tolerate using this in a non-constructor function
|
||||
"noyield" : false, // true: Tolerate generators without yields
|
||||
|
||||
// Environments
|
||||
"browser" : false, // Web Browser (window, document, etc)
|
||||
"browserify" : true, // Browserify (node.js code in the browser)
|
||||
"couch" : false, // CouchDB
|
||||
"devel" : false, // Development/debugging (alert, confirm, etc)
|
||||
"dojo" : false, // Dojo Toolkit
|
||||
"jquery" : false, // jQuery
|
||||
"mocha" : false, // mocha
|
||||
"mootools" : false, // MooTools
|
||||
"node" : false, // Node.js
|
||||
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
|
||||
"phantom" : false, // PhantomJS
|
||||
"prototypejs" : false, // Prototype and Scriptaculous
|
||||
"rhino" : false, // Rhino
|
||||
"worker" : false, // Web Workers
|
||||
"wsh" : false, // Windows Scripting Host
|
||||
"yui" : false, // Yahoo User Interface
|
||||
|
||||
// Custom Globals
|
||||
"globals" : {} // additional predefined global variables
|
||||
}
|
||||
5
.mocha.js
Normal file
@@ -0,0 +1,5 @@
|
||||
Error.stackTraceLimit = 100
|
||||
|
||||
try { require('trace') } catch (_) {}
|
||||
try { require('clarify') } catch (_) {}
|
||||
try { require('source-map-support/register') } catch (_) {}
|
||||
1
.mocha.opts
Normal file
@@ -0,0 +1 @@
|
||||
--require ./.mocha.js
|
||||
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
|
||||
10
.travis.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 'stable'
|
||||
- '4'
|
||||
- '0.12'
|
||||
- '0.10'
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
sudo: false
|
||||
277
CHANGELOG.md
@@ -1,15 +1,284 @@
|
||||
# ChangeLog
|
||||
|
||||
## **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
|
||||
|
||||
- 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))
|
||||
- docker control ([xo-web#211](https://github.com/vatesfr/xo-web/issues/211))
|
||||
- better responsive design ([xo-web#252](https://github.com/vatesfr/xo-web/issues/252))
|
||||
- host stats ([xo-web#255](https://github.com/vatesfr/xo-web/issues/255))
|
||||
- pagination ([xo-web#221](https://github.com/vatesfr/xo-web/issues/221))
|
||||
- web updater
|
||||
- better VM creation process([xo-web#256](https://github.com/vatesfr/xo-web/issues/256))
|
||||
- VM boot order([xo-web#251](https://github.com/vatesfr/xo-web/issues/251))
|
||||
- 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
|
||||
|
||||
- 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))
|
||||
- VM stats behavior more robust ([xo-web#250](https://github.com/vatesfr/xo-web/issues/250))
|
||||
- XO not on the root of domain ([xo-web#254](https://github.com/vatesfr/xo-web/issues/254))
|
||||
|
||||
|
||||
## **3.9.1** (2015-04-21)
|
||||
|
||||
A few bugs hve made their way into *3.9.0*, this minor release fixes
|
||||
them.
|
||||
|
||||
### 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))
|
||||
- do not fetch stats if the VM state is transitioning ([a5c9880](https://github.com/vatesfr/xo-web/commit/a5c98803182792d2fe5ceb840ae1e23a8b767923))
|
||||
- fix broken Angular due to new version of Babel which breaks ngAnnotate ([0a9c868](https://github.com/vatesfr/xo-web/commit/0a9c868678d239e5b3e54b4d2bc3bd14b5400120))
|
||||
- feedback when connecting/disconnecting a server ([027d1e8](https://github.com/vatesfr/xo-web/commit/027d1e8cb1f2431e67042e1eec51690b2bc54ad7))
|
||||
- clearer error message if a server is unreachable ([06ca007](https://github.com/vatesfr/xo-server/commit/06ca0079b321e757aaa4112caa6f92a43193e35d))
|
||||
|
||||
## **3.9.0** (2015-04-20)
|
||||
|
||||
[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))
|
||||
- better feedback when connecting to a server ([xo-web#210](https://github.com/vatesfr/xo-web/issues/210))
|
||||
- ability to add a local LVM SR ([xo-web#219](https://github.com/vatesfr/xo-web/issues/219))
|
||||
- display virtual GPUs in VM view ([xo-web#223](https://github.com/vatesfr/xo-web/issues/223))
|
||||
- ability to automatically start a VM with its host ([xo-web#224](https://github.com/vatesfr/xo-web/issues/224))
|
||||
- ability to create networks ([xo-web#225](https://github.com/vatesfr/xo-web/issues/225))
|
||||
- live charts for a VM CPU/disk/network & RAM ([xo-web#228](https://github.com/vatesfr/xo-web/issues/228) & [xo-server#51](https://github.com/vatesfr/xo-server/issues/51))
|
||||
@@ -17,7 +286,7 @@
|
||||
- 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
|
||||
### 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))
|
||||
@@ -27,7 +296,7 @@
|
||||
|
||||
[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))
|
||||
@@ -36,7 +305,7 @@
|
||||
- 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
|
||||
### 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))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Xen Orchestra Web
|
||||
|
||||

|
||||
|
||||
XO-Web is part of [Xen Orchestra](https://github.com/vatesfr/xo), a web interface for XenServer or XAPI enabled hosts.
|
||||
|
||||
It is a web client for [XO-Server](https://github.com/vatesfr/xo-server).
|
||||
@@ -39,6 +41,9 @@ Otherwise, please consider using the [bugtracker of the general repository](http
|
||||
# Switch to the master branch.
|
||||
git checkout master
|
||||
|
||||
# Fetches latest changes.
|
||||
git pull --ff-only
|
||||
|
||||
# Merge changes of the next-release branch.
|
||||
git merge next-release
|
||||
|
||||
@@ -50,10 +55,10 @@ git checkout next-release
|
||||
|
||||
# Fetches the last changes (the merge and version bump) from master to
|
||||
# next-release.
|
||||
git pull --fast-forward master
|
||||
git merge --ff-only master
|
||||
|
||||
# Push the changes on git.
|
||||
git push origin master:master next-release:next-release
|
||||
git push --follow-tags origin master next-release
|
||||
|
||||
# Publish this release to npm.
|
||||
npm publish
|
||||
|
||||
181
app/app.js
@@ -1,181 +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 vmState from './modules/vm';
|
||||
import isoDevice from './modules/iso-device';
|
||||
|
||||
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,
|
||||
vmState,
|
||||
isoDevice,
|
||||
'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,
|
||||
xoApi,
|
||||
xo
|
||||
) {
|
||||
$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 && !xo.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: 5.3 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 34 KiB |
@@ -1,64 +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")
|
||||
link(rel="stylesheet", href="bower_components/angular-chart.js/dist/angular-chart.css")
|
||||
body(
|
||||
ng-app = 'xoWebApp'
|
||||
ng-strict-di
|
||||
)
|
||||
|
||||
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,23 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
|
||||
import pkg from '../../../package';
|
||||
|
||||
//====================================================================
|
||||
|
||||
module.exports = 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,81 +0,0 @@
|
||||
angular = require 'angular'
|
||||
|
||||
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
|
||||
|
||||
$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.poolRef
|
||||
return unless pool
|
||||
|
||||
$scope.consoleUrl = "/consoles/#{id}"
|
||||
|
||||
host = get VM.$container # host because the VM is running.
|
||||
return unless host
|
||||
|
||||
# FIXME: We should filter on connected SRs (PBDs)!
|
||||
SRs = get (merge host.SRs, pool.SRs)
|
||||
$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.UUID
|
||||
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.UUID})'
|
||||
) {{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.UUID 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.UUID)"
|
||||
)
|
||||
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.UUID)"
|
||||
)
|
||||
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.UUID)"
|
||||
)
|
||||
i.fa.fa-refresh.fa-fw
|
||||
//- Console
|
||||
.list-group-item
|
||||
no-vnc(
|
||||
url = '{{consoleUrl}}'
|
||||
remote-control = 'vncRemote'
|
||||
)
|
||||
@@ -1,61 +0,0 @@
|
||||
import angular from 'angular';
|
||||
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 = {};
|
||||
angular.forEach(VMsIds, id => {
|
||||
disks[id] = true;
|
||||
});
|
||||
|
||||
$scope.delete = function () {
|
||||
let value = [];
|
||||
angular.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 = [];
|
||||
|
||||
angular.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.UUID")
|
||||
td {{VM.name_label}}
|
||||
td {{VM.name_description}}
|
||||
td
|
||||
input(type="checkbox", ng-model="disks[VM.UUID]")
|
||||
.modal-footer
|
||||
button.btn.btn-primary(type="submit")
|
||||
| Delete
|
||||
button.btn.btn-warning(type="button", ng-click="$dismiss()")
|
||||
| Cancel
|
||||
@@ -1,40 +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) {
|
||||
var 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,214 +0,0 @@
|
||||
angular = require 'angular'
|
||||
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
|
||||
$upload
|
||||
$window
|
||||
xoApi, xo, modal, notify, bytesToSizeFilter
|
||||
) ->
|
||||
$window.bytesToSize = bytesToSizeFilter # FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
host = null
|
||||
$scope.$watch(
|
||||
-> xoApi.get $stateParams.id
|
||||
(host) ->
|
||||
$scope.host = host
|
||||
return unless host?
|
||||
|
||||
$scope.pool = xoApi.get host.poolRef
|
||||
|
||||
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.removeMessage = xo.message.delete
|
||||
|
||||
$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.UUID
|
||||
}
|
||||
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
|
||||
$data.enabled = host.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}"
|
||||
xo.log.delete log
|
||||
|
||||
$scope.deleteLog = (id) ->
|
||||
console.log "Remove log #{id}"
|
||||
xo.log.delete id
|
||||
|
||||
$scope.connectPBD = (UUID) ->
|
||||
console.log "Connect PBD #{UUID}"
|
||||
|
||||
xoApi.call 'pbd.connect', {id: UUID}
|
||||
|
||||
$scope.disconnectPBD = (UUID) ->
|
||||
console.log "Disconnect PBD #{UUID}"
|
||||
|
||||
xoApi.call 'pbd.disconnect', {id: UUID}
|
||||
|
||||
$scope.removePBD = (UUID) ->
|
||||
console.log "Remove PBD #{UUID}"
|
||||
|
||||
xoApi.call 'pbd.delete', {id: UUID}
|
||||
|
||||
$scope.connectPIF = (UUID) ->
|
||||
console.log "Connect PIF #{UUID}"
|
||||
|
||||
xoApi.call 'pif.connect', {id: UUID}
|
||||
|
||||
$scope.disconnectPIF = (UUID) ->
|
||||
console.log "Disconnect PIF #{UUID}"
|
||||
|
||||
xoApi.call 'pif.disconnect', {id: UUID}
|
||||
|
||||
$scope.removePIF = (UUID) ->
|
||||
console.log "Remove PIF #{UUID}"
|
||||
|
||||
xoApi.call 'pif.delete', {id: UUID}
|
||||
|
||||
$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.UUID
|
||||
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
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
@@ -1,382 +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.UUID})") {{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-checkbox="host.enabled", e-name="enabled", e-form="hostSettings")
|
||||
| {{host.enabled}}
|
||||
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}}
|
||||
.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
|
||||
.grid
|
||||
.grid-cell
|
||||
p.stat-name CPU usage:
|
||||
canvas(
|
||||
id="bar"
|
||||
class="chart chart-bar"
|
||||
data="[[host.$vCPUs], [host.CPUs['cpu_count']]]"
|
||||
labels="['']"
|
||||
series="['vCPUs','CPUs']"
|
||||
options="{scaleShowGridLines: false, barDatasetSpacing : 10, showScale: false}"
|
||||
)
|
||||
.grid-cell
|
||||
p.stat-name RAM used:
|
||||
canvas(id="doughnut", class="chart chart-doughnut", data="[(host.memory.usage), (host.memory.size - host.memory.usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
|
||||
.grid-cell
|
||||
p.stat-name Running VMs:
|
||||
p.center.big-stat {{host.VMs.length}}
|
||||
//- 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.UUID})")
|
||||
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.UUID})")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)'
|
||||
)
|
||||
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%", ng-repeat="controller in [host.controller] | resolve track by controller.UUID", xo-sref="consoles_view({id: controller.UUID})")
|
||||
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(ng-repeat="controller in [host.controller] | resolve track by controller.UUID", role="progressbar", aria-valuemin="0", aria-valuenow="{{controller.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[controller.memory.size, host.memory.size] | %}}", tooltip="{{host.name_label}}: {{[controller.memory.size, host.memory.size] | %}}")
|
||||
small {{host.name_label}}
|
||||
.progress-bar.progress-bar-vm(ng-repeat="VM in host.VMs | resolve | orderBy:natural('name_label') track by VM.UUID", role="progressbar", aria-valuemin="0", aria-valuenow="{{VM.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[VM.memory.size, host.memory.size] | %}}", xo-sref="VMs_view({id: VM.UUID})", tooltip="{{VM.name_label}}: {{[VM.memory.size, host.memory.size] | %}}")
|
||||
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.UUID})", ng-repeat="SR in pool.SRs | resolve | orderBy:natural('name_label') track by SR.UUID")
|
||||
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] | %}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | %}}")
|
||||
.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] | %}}", tooltip="Allocated: {{[(SR.usage), SR.size] | %}}")
|
||||
td
|
||||
span.label.label-primary Shared
|
||||
td(ng-if="SRsToPBDs[SR.ref].attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.ref].ref)")
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!SRsToPBDs[SR.ref].attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.ref].ref)")
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.ref].ref)")
|
||||
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.UUID})", ng-repeat="SR in host.SRs | resolve | orderBy:natural('name_label') track by SR.UUID")
|
||||
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] | %}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | %}}")
|
||||
.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] | %}}", tooltip="Allocated: {{[(SR.usage), SR.size] | %}}")
|
||||
td
|
||||
span.label.label-info Local
|
||||
td(ng-if="SRsToPBDs[SR.ref].attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.ref].ref)")
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!SRsToPBDs[SR.ref].attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.ref].ref)")
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.ref].ref)")
|
||||
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.UUID")
|
||||
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 {{PIF.IP}} ({{PIF.mode}})
|
||||
td {{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.ref)")
|
||||
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.ref)")
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Remove this interface", xo-click="removePIF(PIF.ref)")
|
||||
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="host.tasks.length")
|
||||
i.fa.fa-spinner.fa-pulse(style="color: #e25440;")
|
||||
| Pending tasks
|
||||
.panel-heading.panel-title(ng-if="!host.tasks.length")
|
||||
i.fa.fa-spinner(style="color: #e25440;")
|
||||
| Pending tasks
|
||||
.panel-body
|
||||
p.center(ng-if="!host.tasks.length") No recent tasks
|
||||
table.table.table-hover(ng-if="host.tasks.length")
|
||||
th Date
|
||||
th Progress
|
||||
th Name
|
||||
//- TODO: working reverse order, from recent to oldest
|
||||
tr(ng-repeat="task in host.tasks | resolve | orderBy:'created':true track by task.UUID")
|
||||
td {{task.created}}
|
||||
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
|
||||
| {{task.name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="cancelTask(task.UUID)")
|
||||
i.fa.fa-times.fa-lg(tooltip="Cancel this task")
|
||||
a(xo-click="destroyTask(task.UUID)")
|
||||
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.length", tooltip="Remove all logs", ng-click="deleteAllLog()")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="!host.messages.length") No recent logs
|
||||
table.table.table-hover(ng-if="host.messages.length")
|
||||
th Date
|
||||
th Name
|
||||
tr(ng-repeat="message in host.messages | resolve | orderBy:'-time' track by message.UUID")
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="deleteLog(message.UUID)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
|
||||
|
||||
.grid
|
||||
//- Patches panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-file-code-o(style="color: #e25440;")
|
||||
| Patches
|
||||
.panel-body
|
||||
p.center(ng-if="!host.patches.length") No patches
|
||||
table.table.table-hover(ng-if="host.patches.length")
|
||||
th Applied on
|
||||
th Name
|
||||
th Description
|
||||
th Status
|
||||
tr(ng-repeat="patch in host.patches | resolve | orderBy:'-time'")
|
||||
td {{patch.time*1e3 | date:"medium"}}
|
||||
td {{(patch.pool_patch | resolve).name_label}}
|
||||
td {{(patch.pool_patch | resolve).name_description}}
|
||||
//- TODO: allow patch application and removal
|
||||
td
|
||||
span(ng-if="patch.applied")
|
||||
span.label.label-success Applied
|
||||
span(ng-if="!patch.applied")
|
||||
span.label.label-error Not applied
|
||||
//- span.pull-right.btn-group.quick-buttons
|
||||
//- a(xo-click="deletePatch(patch.UUID)")
|
||||
//- i.fa.fa-trash-o.fa-lg(tooltip="Remove this patch")
|
||||
.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' track by pci.UUID")
|
||||
td {{pci.pci_id}} ({{pci.class_name}})
|
||||
td {{pci.device_name}}
|
||||
.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' track by pgpu.UUID")
|
||||
td {{pgpu.device}}
|
||||
@@ -1,27 +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.UUID
|
||||
|
||||
this.insert = (VM, disc_id) ->
|
||||
xo.vm.insertCd VM.UUID, disc_id, true
|
||||
|
||||
# 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.UUID 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,27 +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.byTypes = xoApi.byTypes;
|
||||
})
|
||||
|
||||
// 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.byTypes.pool | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by pool.UUID", ng-if="pool.name_label", xo-sref="pools_view({id: pool.UUID})")
|
||||
//- 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.UUID})") {{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.UUID})") {{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
|
||||
| {{pool.$running_hosts.length}}/{{pool.hosts.length}} 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.byTypes.host | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by host.UUID", xo-sref="hosts_view({id: host.UUID})")
|
||||
//- 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] | %}}", tooltip="RAM: {{[host.memory.usage, host.memory.size] | %}} allocated")
|
||||
| {{[host.memory.usage, host.memory.size] | %}}
|
||||
.grid-cell.flat-cell
|
||||
| {{host.VMs.length}} 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.byTypes.VM | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by VM.UUID", xo-sref="VMs_view({id: VM.UUID})")
|
||||
//- 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.UUID})") {{container.name_label}}
|
||||
div(ng-if="'host' === container.type", ng-init="pool = (container.poolRef | resolve)")
|
||||
| Resident on:
|
||||
a(ui-sref="hosts_view({id: container.UUID})") {{container.name_label}}
|
||||
small(ng-if="pool.name_label")
|
||||
| (
|
||||
a(ui-sref="pools_view({id: pool.UUID})") {{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.byTypes.SR | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by SR.UUID", xo-sref="SRs_view({id: SR.UUID})")
|
||||
//- 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] | %}} ({{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.UUID})") {{container.name_label}}
|
||||
div(ng-if="'host' === container.type")
|
||||
| Connected to
|
||||
a(ui-sref="hosts_view({id: container.UUID})") {{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) {
|
||||
var toState, toStateParams;
|
||||
{
|
||||
let tmp = $rootScope._login;
|
||||
if (tmp) {
|
||||
toState = tmp.state.name;
|
||||
toStateParams = tmp.stateParams;
|
||||
delete $rootScope._login;
|
||||
} else {
|
||||
toState = 'index';
|
||||
}
|
||||
}
|
||||
|
||||
$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,59 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import filter from 'lodash.filter';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
|
||||
import xoServices from 'xo-services';
|
||||
|
||||
import view from './view';
|
||||
|
||||
//====================================================================
|
||||
|
||||
export default angular.module('xoWebApp.navbar', [
|
||||
uiRouter,
|
||||
|
||||
xoServices,
|
||||
])
|
||||
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope) {
|
||||
// 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');
|
||||
};
|
||||
|
||||
const ALIVE_STATUS = {
|
||||
cancelling: true,
|
||||
pending: true,
|
||||
};
|
||||
let {canAccess} = xo;
|
||||
let sieve = (task) => ALIVE_STATUS[task.status] && canAccess(task.$host);
|
||||
$scope.$watchCollection(() => xoApi.byTypes.task, (tasks) => {
|
||||
this.tasks = filter(tasks, sieve);
|
||||
});
|
||||
})
|
||||
.directive('navbar', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
controller: 'NavbarCtrl as navbar',
|
||||
template: view,
|
||||
scope: {},
|
||||
};
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
@@ -1,123 +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")
|
||||
i.xo-icon-info.text-danger
|
||||
| Unregistered version: no support provided!
|
||||
//- 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.length", tooltip="No running tasks")
|
||||
a.dropdown-toggle.inverse
|
||||
i.xo-icon-task
|
||||
li.dropdown(dropdown, ng-if="navbar.tasks.length")
|
||||
a.dropdown-toggle.inverse(dropdown-toggle)
|
||||
i.xo-icon-task
|
||||
ul.dropdown-menu.inverse
|
||||
li.task-menu(
|
||||
ng-repeat="task in navbar.tasks | 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
|
||||
i.fa.fa-user
|
||||
| {{navbar.user.email}}
|
||||
li
|
||||
a(ng-click = 'navbar.logOut()')
|
||||
i.fa.fa-sign-out
|
||||
|
|
||||
|
|
||||
|
||||
//- /Right items.
|
||||
//- /Navbar items.
|
||||
//- /Navbar.
|
||||
@@ -1,476 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
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 = !(
|
||||
('Local' === data.srType) &&
|
||||
(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 ('NFS' === type || 'NFS_ISO' === type) {
|
||||
|
||||
xoApi.call('sr.probeNfs', {
|
||||
host: this.container.UUID,
|
||||
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 ('iSCSI' === type) {
|
||||
|
||||
let params = {
|
||||
host: this.container.UUID
|
||||
};
|
||||
|
||||
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.UUID,
|
||||
target: iqn.ip,
|
||||
targetIqn: iqn.iqn
|
||||
};
|
||||
|
||||
if (auth) {
|
||||
params.chapUser = user;
|
||||
params.chapPassword = password;
|
||||
}
|
||||
|
||||
xoApi.call('sr.probeIscsiLuns', params)
|
||||
.then(response => {
|
||||
|
||||
response.forEach(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 (-1 < index) {
|
||||
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.UUID,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
server: server.host,
|
||||
serverPath: data.srPath.path
|
||||
};
|
||||
|
||||
return params;
|
||||
|
||||
};
|
||||
|
||||
this._prepareScsiParams = function(data) {
|
||||
|
||||
let params = {
|
||||
host: this.container.UUID,
|
||||
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.UUID,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
device
|
||||
});
|
||||
break;
|
||||
|
||||
case 'NFS_ISO':
|
||||
case 'Local':
|
||||
|
||||
let server = this._parseAddress(data.srServer || '');
|
||||
|
||||
let path = (('NFS_ISO' === data.srType) ?
|
||||
server.host + ':' :
|
||||
'') + data.srPath.path;
|
||||
|
||||
operationToPromise = xoApi.call('sr.createIso', {
|
||||
host: this.container.UUID,
|
||||
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?'
|
||||
});
|
||||
} else {
|
||||
return Bluebird.resolve(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?'
|
||||
});
|
||||
} else {
|
||||
return Bluebird.resolve(true);
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
};
|
||||
|
||||
this._gatherConnectedUuids = function() {
|
||||
|
||||
let SRs = [];
|
||||
|
||||
let pool = xoApi.get(this.container.poolRef);
|
||||
pool.SRs.forEach(ref => SRs.push(xoApi.get(ref).UUID));
|
||||
let hosts = [];
|
||||
pool.hosts.forEach(ref => hosts.push(xoApi.get(ref)));
|
||||
hosts.forEach(h => h.SRs.forEach(ref => SRs.push(xoApi.get(ref).UUID)));
|
||||
|
||||
return SRs;
|
||||
|
||||
};
|
||||
|
||||
this._processSRList = function (list) {
|
||||
|
||||
let inUse = false;
|
||||
let SRs = this._gatherConnectedUuids();
|
||||
|
||||
list.forEach(item => {
|
||||
inUse = (item.used = _indexOf(SRs, item.uuid) > -1) || inUse;
|
||||
});
|
||||
|
||||
this.lockCreation = inUse;
|
||||
|
||||
return list;
|
||||
|
||||
};
|
||||
|
||||
this.loadScsiList = function(data) {
|
||||
|
||||
this.resetLists();
|
||||
|
||||
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
|
||||
});
|
||||
})
|
||||
;
|
||||
|
||||
};
|
||||
|
||||
this.loadNfsList = function (data) {
|
||||
|
||||
this.resetLists();
|
||||
|
||||
let server = this._parseAddress(data.srServer);
|
||||
|
||||
xoApi.call('sr.probeNfsExists', {
|
||||
host: this.container.UUID,
|
||||
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.UUID,
|
||||
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.UUID})")
|
||||
| {{newSr.container.name_label}}
|
||||
a(ng-if="'host' === newSr.container.type", ui-sref="hosts_view({id: newSr.container.UUID})")
|
||||
| {{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"')
|
||||
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,232 +0,0 @@
|
||||
angular = require 'angular'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
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
|
||||
$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.poolRef) ? {}
|
||||
else
|
||||
host = {}
|
||||
pool = container
|
||||
|
||||
default_SR = get pool.default_SR
|
||||
default_SR = if default_SR
|
||||
default_SR.UUID
|
||||
else
|
||||
''
|
||||
|
||||
# Computes the list of templates.
|
||||
$scope.templates = get (merge pool.templates, host.templates)
|
||||
|
||||
# FIXME: We should filter on connected SRs (PBDs)!
|
||||
# Computes the list of SRs.
|
||||
SRs = get (merge pool.SRs, host.SRs)
|
||||
|
||||
# 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')
|
||||
|
||||
# Computes the list of networks.
|
||||
$scope.networks = get pool.networks
|
||||
)
|
||||
|
||||
$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.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 = angular.copy template.template_info.disks
|
||||
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 = angular.copy 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 = angular.copy 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.UUID
|
||||
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,203 +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.UUID})")
|
||||
| {{container.name_label}}
|
||||
a(ng-if="'host' === container.type", ui-sref="hosts_view({id: container.UUID})")
|
||||
| {{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.UUID", 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
|
||||
//- 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 = 'installation_method'
|
||||
value = 'cdrom'
|
||||
required
|
||||
)
|
||||
select.form-control.disabled(
|
||||
ng-disabled="'cdrom' !== installation_method"
|
||||
ng-model="installation_cdrom"
|
||||
required
|
||||
)
|
||||
option(value = '') Please select
|
||||
optgroup(ng-repeat="SR in ISO_SRs | orderBy:natural('name_label') track by SR.UUID", ng-if="SR.VDIs.length", label="{{SR.name_label}}")
|
||||
option(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.UUID", ng-value="VDI.UUID")
|
||||
| {{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 = '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="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.UUID as network.name_label for network in (networks | orderBy:natural("name_label"))'
|
||||
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
|
||||
.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.UUID 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.stat-name
|
||||
| Name:
|
||||
p.center.big {{name_label}}
|
||||
.grid-cell
|
||||
p.stat-name
|
||||
| Template:
|
||||
p.center {{template.name_label}}
|
||||
.grid
|
||||
.grid-cell
|
||||
p.stat-name vCPUs
|
||||
p.center.big {{CPUs || template.CPUs.number}}
|
||||
.grid-cell
|
||||
p.stat-name RAM
|
||||
p.center.big {{(memory) || (template.memory.size | bytesToSize)}}
|
||||
.grid-cell
|
||||
p.stat-name Disks
|
||||
p.center.big {{(VDIs.length) || (template.$VBDs.length) || 0}}
|
||||
.grid-cell
|
||||
p.stat-name Interfaces
|
||||
p.center.big {{VIFs.length}}
|
||||
p.center
|
||||
button.btn.btn-lg.btn-primary(type="submit")
|
||||
i.fa.fa-play
|
||||
| Create VM
|
||||
@@ -1,87 +0,0 @@
|
||||
import angular from 'angular';
|
||||
|
||||
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) {
|
||||
$scope.$watch(() => xoApi.get($stateParams.id), function (pool) {
|
||||
$scope.pool = pool;
|
||||
});
|
||||
|
||||
$scope.savePool = function ($data) {
|
||||
let {pool} = $scope;
|
||||
let {name_label, name_description} = $data;
|
||||
|
||||
$data = {
|
||||
id: pool.UUID,
|
||||
}
|
||||
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.
|
||||
angular.forEach($scope.pool.messages, function(value, key) {
|
||||
xo.log.delete(value);
|
||||
console.log('Remove log', value);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$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,147 +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.UUID})")
|
||||
| {{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.UUID})") {{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
|
||||
.grid
|
||||
.grid-cell
|
||||
p.stat-name Hosts:
|
||||
p.center.big-stat {{pool.hosts.length}}
|
||||
.grid-cell
|
||||
p.stat-name Running:
|
||||
p.center.big-stat {{pool.$running_hosts.length}}
|
||||
|
||||
//- 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%")
|
||||
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.UUID})")
|
||||
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.UUID)")
|
||||
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.UUID})", ng-repeat="host in pool.hosts | resolve | orderBy:natural('name_label') track by host.UUID")
|
||||
td {{host.name_label}}
|
||||
td {{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] | %}}")
|
||||
|
||||
//- 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(xo-sref="SRs_view({id: SR.UUID})", ng-repeat="SR in pool.SRs | resolve | orderBy:natural('name_label') track by SR.UUID")
|
||||
td {{SR.name_label}}
|
||||
td {{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] | %}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | %}}")
|
||||
.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] | %}}", tooltip="Allocated: {{[(SR.usage), SR.size] | %}}")
|
||||
|
||||
//- 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.length", tooltip="Remove all logs", xo-click="deleteAllLog()")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="!pool.messages.length") No recent logs
|
||||
table.table.table-hover(ng-if="pool.messages.length")
|
||||
th Date
|
||||
th Name
|
||||
tr(ng-repeat="message in pool.messages | resolve | orderBy:'-time' track by message.UUID")
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="deleteLog(message.UUID)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
|
||||
@@ -1,80 +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();
|
||||
},
|
||||
},
|
||||
template: view,
|
||||
});
|
||||
})
|
||||
.controller('SettingsAcls', function ($scope, acls, users, xoApi, xo) {
|
||||
this.acls = acls;
|
||||
|
||||
this.users = users;
|
||||
{
|
||||
let usersById = this.usersById = Object.create(null);
|
||||
for (let user of users) {
|
||||
usersById[user.id] = user;
|
||||
}
|
||||
}
|
||||
|
||||
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).then(refreshAcls);
|
||||
};
|
||||
this.removeAcl = (subject, object) => {
|
||||
xo.acl.remove(subject, object).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,66 +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'
|
||||
)
|
||||
div
|
||||
i.fa.fa-user
|
||||
| {{$select.selected.email}}
|
||||
ui-select-choices(
|
||||
repeat = 'user in ctrl.users | filter:{ permission: "!admin" } | filter:$select.search'
|
||||
)
|
||||
div
|
||||
i.fa.fa-user
|
||||
| {{user.email}}
|
||||
.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}}
|
||||
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 Action
|
||||
tr(ng-repeat = 'acl in ctrl.acls')
|
||||
td {{ctrl.usersById[acl.subject].email}}
|
||||
td {{(acl.object | resolve).name_label}}
|
||||
td
|
||||
button.btn.btn-sm.btn-danger(ng-click = 'ctrl.removeAcl(acl.subject, acl.object)')
|
||||
i.fa.fa-trash
|
||||
@@ -1,33 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
|
||||
import acls from './acls';
|
||||
import servers from './servers';
|
||||
import users from './users';
|
||||
|
||||
import view from './view';
|
||||
|
||||
export default angular.module('settings', [
|
||||
uiRouter,
|
||||
|
||||
acls,
|
||||
servers,
|
||||
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,119 +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.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) => {
|
||||
xo.server.connect(id).catch(error => {
|
||||
notify.error({
|
||||
title: 'Server connection error',
|
||||
message: error.message
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.disconnectServer = (id) => {
|
||||
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,109 +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.users', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
|
||||
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 = {};
|
||||
const newUsers = this.newUsers = [];
|
||||
|
||||
const refreshUsers = () => {
|
||||
xo.user.getAll().then(users => {
|
||||
this.users = users;
|
||||
});
|
||||
};
|
||||
|
||||
const interval = $interval(refreshUsers, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
this.addUser = () => {
|
||||
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.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('email') track by user.id")
|
||||
td
|
||||
input.form-control(type="text", ng-model="user.email")
|
||||
td
|
||||
select.form-control(ng-options="p.value as p.label for p in ctrl.permissions", ng-model="user.permission")
|
||||
td
|
||||
input.form-control(type="password", ng-model="user.password", placeholder="Fill to change the password")
|
||||
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,20 +0,0 @@
|
||||
.grid
|
||||
//- Side menu
|
||||
.settings-menu
|
||||
ul.nav
|
||||
li
|
||||
a(ui-sref = '.servers', ui-sref-active = 'active')
|
||||
i.fa.fa-fw.fa-cloud.fa-menu
|
||||
| Servers
|
||||
a(ui-sref = '.users')
|
||||
i.fa.fa-fw.fa-user.fa-menu
|
||||
| Users
|
||||
a.disabled(ui-sref = '.groups')
|
||||
i.fa.fa-fw.fa-users.fa-menu
|
||||
| Groups
|
||||
a(ui-sref = '.acls')
|
||||
i.fa.fa-fw.fa-key.fa-menu
|
||||
| ACLs
|
||||
|
||||
//- Content
|
||||
div.settings-content(ui-view = '')
|
||||
@@ -1,189 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import isEmpty from '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
|
||||
|
||||
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.UUID,
|
||||
};
|
||||
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 (UUID) {
|
||||
console.log('Delete VDI', UUID);
|
||||
|
||||
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(UUID);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.disconnectVBD = function (UUID) {
|
||||
console.log('Disconnect VBD', UUID);
|
||||
|
||||
return xoApi.call('vbd.disconnect', {id: UUID});
|
||||
};
|
||||
|
||||
$scope.connectPBD = function (UUID) {
|
||||
console.log('Connect PBD', UUID);
|
||||
|
||||
return xoApi.call('pbd.connect', {id: UUID});
|
||||
};
|
||||
|
||||
$scope.disconnectPBD = function (UUID) {
|
||||
console.log('Disconnect PBD', UUID);
|
||||
|
||||
return xoApi.call('pbd.disconnect', {id: UUID});
|
||||
};
|
||||
|
||||
$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.ref});
|
||||
}
|
||||
};
|
||||
|
||||
$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.ref});
|
||||
console.log(pbd.ref)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.rescanSr = function (UUID) {
|
||||
console.log('Rescan SR', UUID);
|
||||
|
||||
return xoApi.call('sr.scan', {id: UUID});
|
||||
};
|
||||
|
||||
$scope.removeSR = function (UUID) {
|
||||
console.log('Remove SR', UUID);
|
||||
|
||||
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: UUID});
|
||||
}).then(function () {
|
||||
$state.go('index');
|
||||
notify.info({
|
||||
title: 'SR remove',
|
||||
message: 'SR is removed',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.forgetSR = function (UUID) {
|
||||
console.log('Forget SR', UUID);
|
||||
|
||||
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: UUID});
|
||||
}).then(function () {
|
||||
$state.go('index');
|
||||
notify.info({
|
||||
title: 'SR forget',
|
||||
message: 'SR is forgotten',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.saveDisks = function (data) {
|
||||
// Group data by disk.
|
||||
let disks = {};
|
||||
angular.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 = [];
|
||||
angular.forEach(disks, function (attributes, id) {
|
||||
// Keep only changed attributes.
|
||||
let disk = get(id);
|
||||
|
||||
angular.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,203 +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.UUID})") {{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
|
||||
.grid
|
||||
.grid-cell
|
||||
p.stat-name Physical Alloc:
|
||||
canvas(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) %>"}')
|
||||
.grid-cell
|
||||
p.stat-name Virtual Alloc:
|
||||
canvas(id="doughnut", class="chart chart-doughnut", data="[(SR.usage), (SR.size - SR.usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
|
||||
.grid-cell
|
||||
p.stat-name VDIs:
|
||||
p.center.big-stat {{SR.VDIs.length}}
|
||||
//- 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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID", role="progressbar", aria-valuemin="0", aria-valuenow="{{VDI.size}}", aria-valuemax="{{SR.size}}", style="width: {{[VDI.size, SR.size] | %}}", tooltip="{{VDI.name_label}} ({{[VDI.size, SR.size] | %}})")
|
||||
//- 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.UUID)")
|
||||
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')")
|
||||
td
|
||||
span(
|
||||
editable-text="VDI.name_label"
|
||||
e-name = '{{VDI.UUID}}/name_label'
|
||||
)
|
||||
| {{VDI.name_label}}
|
||||
span.label.label-info(ng-if="VDI.$snapshot_of") snapshot
|
||||
td
|
||||
span(
|
||||
editable-text="VDI.name_description"
|
||||
e-name = '{{VDI.UUID}}/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 {{((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.UUID)")
|
||||
i.fa.fa-unlink.fa-lg(tooltip="Disconnect this disk")
|
||||
a(ng-if="!(VDI.$VBD | resolve).attached", xo-click="deleteVDI(VDI.UUID)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this disk")
|
||||
//- 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
|
||||
//- /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).UUID})")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.length") No recent logs
|
||||
table.table.table-hover(ng-if="SR.messages.length")
|
||||
th.col-md-1 Date
|
||||
th.col-md-1 Name
|
||||
tr(ng-repeat="message in SR.messages | resolve | orderBy:'-time' track by message.UUID")
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
a.quick-remove(tooltip="Remove log")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
//- /Hosts.
|
||||
@@ -1,255 +0,0 @@
|
||||
angular = require 'angular'
|
||||
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
|
||||
) ->
|
||||
Object.defineProperties($scope, {
|
||||
xo: { get: -> xoApi.byTypes.xo?[0] },
|
||||
pools: { get: -> xoApi.byTypes.pool },
|
||||
hosts: { get: -> xoApi.byTypes.host },
|
||||
VMs: { get: -> xoApi.byTypes.VM },
|
||||
})
|
||||
|
||||
$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
|
||||
|
||||
# extract a value in a object
|
||||
$scope.values = (object) ->
|
||||
value for _, value of object
|
||||
|
||||
$scope.deleteVMs = ->
|
||||
{selected_VMs} = $scope
|
||||
|
||||
deleteVmsModal (id for id, selected of selected_VMs when selected)
|
||||
|
||||
$scope.osType = (osName) ->
|
||||
switch osName
|
||||
when 'debian','ubuntu','centos','redhat','oracle','gentoo','suse','fedora','sles'
|
||||
'linux'
|
||||
when 'windows'
|
||||
'windows'
|
||||
else
|
||||
'other'
|
||||
|
||||
# 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 = (xoApi.byTypes.VM?.length 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) ->
|
||||
VMs = xoApi.byTypes.VM
|
||||
|
||||
if (sieve is true) or (sieve is false)
|
||||
$scope.n_selected_VMs = if sieve then VMs.length else 0
|
||||
selected_VMs[VM.UUID] = sieve for VM in VMs
|
||||
return
|
||||
|
||||
n = 0
|
||||
|
||||
matcher = make_matcher sieve
|
||||
++n for VM in VMs when (selected_VMs[VM.UUID] = matcher VM)
|
||||
|
||||
$scope.n_selected_VMs = n
|
||||
|
||||
$scope.updateVMSelection = (UUID) ->
|
||||
if selected_VMs[UUID]
|
||||
++$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 UUID, selected of selected_VMs
|
||||
fn UUID, 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,400 +0,0 @@
|
||||
//- @todo Remove code duplication for the VMs listing by using a macro.
|
||||
.sub-bar
|
||||
.grid
|
||||
.grid-cell.overview
|
||||
//- Stats
|
||||
i(tooltip="{{xo.pools.length}} pools")
|
||||
i.small {{xo.pools.length}}x
|
||||
|
|
||||
i.xo-icon-pool
|
||||
|
|
||||
|
|
||||
i(tooltip="{{hosts.length}} hosts")
|
||||
i.small {{hosts.length}}x
|
||||
|
|
||||
i.xo-icon-host
|
||||
|
|
||||
|
|
||||
i(tooltip="{{xo.$running_VMs.length}} of {{VMs.length}} VMs running")
|
||||
i.small {{xo.$running_VMs.length}}x
|
||||
|
|
||||
i.xo-icon-vm
|
||||
|
|
||||
|
|
||||
i(tooltip="{{xo.$vCPUs}} vCPUs used of {{xo.$CPUs}} CPUs")
|
||||
i.small {{xo.$vCPUs}}x
|
||||
|
|
||||
i.xo-icon-cpu
|
||||
|
|
||||
|
|
||||
i(tooltip="{{xo.$memory.usage | bytesToSize}} RAM allocated of {{xo.$memory.size | bytesToSize}}")
|
||||
i.small {{xo.$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-repeat="host in hosts | orderBy:natural('name_label') track by host.UUID", ng-if="host.VMs.length")
|
||||
a(ng-click="selectVMs({$container: host.ref})")
|
||||
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 | orderBy:natural('name_label') track by host.UUID")
|
||||
a(ng-click="bulkAction('migrateVM',host.UUID)")
|
||||
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.length")
|
||||
.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.length")
|
||||
//- Contains a pool and all its children (hosts).
|
||||
.grid.pool-block(ng-repeat="pool in pools | orderBy:[natural('name_label'), 'id'] track by pool.UUID")
|
||||
//- 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
|
||||
//- Header (name + dropdown menu).
|
||||
.dropdown.dropdown-pool(dropdown)
|
||||
a.pool-name(ui-sref="pools_view({id: pool.UUID})")
|
||||
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.UUID})")
|
||||
//- i.xo-icon-sr.fa-fw
|
||||
//- | Add SR
|
||||
li
|
||||
a(xo-sref="VMs_new({container: pool.UUID})")
|
||||
i.xo-icon-vm.fa-fw
|
||||
| Create VM
|
||||
//- TODO: solve the "a" problem for ng-file-select
|
||||
li(ng-file-select="patchPool($files, pool.UUID)")
|
||||
a
|
||||
i.fa.fa-file-code-o.fa-fw
|
||||
| Patch
|
||||
li.divider
|
||||
li
|
||||
a.disabled(xo-click="pool_disconnect(pool.UUID)")
|
||||
i.fa.fa-unlink.fa-fw
|
||||
| Disconnect
|
||||
//- /Header.
|
||||
//- Stats & SRs list.
|
||||
div
|
||||
//- Stats.
|
||||
ul.list-unstyled.stats
|
||||
li
|
||||
i(tooltip="{{pool.hosts.length}} hosts connected")
|
||||
i.small {{pool.hosts.length}}x
|
||||
|
|
||||
i.xo-icon-host
|
||||
|
|
||||
|
|
||||
i(tooltip="{{pool.$running_VMs.length}} of {{pool.$VMs.length}} VMs running")
|
||||
i.small {{pool.$running_VMs.length}}x
|
||||
|
|
||||
i.xo-icon-vm
|
||||
li(ng-if="pool.master")
|
||||
| Master:
|
||||
|
|
||||
a(ui-sref="hosts_view({id: (pool.master | resolve).UUID})") {{(pool.master | resolve).name_label}}
|
||||
//- /Stats.
|
||||
//- SRs.
|
||||
div(ng-if="pool.SRs.length")
|
||||
p.center.small-caps SRs:
|
||||
table.table.table-hover.table-condensed
|
||||
tr(ng-repeat="SR in pool.SRs | resolve | orderBy:natural('name_label') track by SR.UUID", xo-sref="SRs_view({id: SR.UUID})")
|
||||
td.col-md-6.sr-name.no-border(ng-class="{'default-sr': SR.ref === 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] | %}} allocated")
|
||||
.progress-bar(role="progressbar", aria-valuenow="{{100*SR.usage/SR.size}}", aria-valuemin="0", aria-valuemax="100", style="width: {{[SR.usage, SR.size] | %}}")
|
||||
//- 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 pool.hosts | resolve | orderBy:natural('name_label') track by host.UUID")
|
||||
//- 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.UUID})")
|
||||
| {{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.UUID})")
|
||||
i.xo-icon-sr.fa-fw
|
||||
| Add SR
|
||||
li
|
||||
a(xo-sref="VMs_new({container: host.UUID})")
|
||||
i.xo-icon-vm.fa-fw
|
||||
| Create VM
|
||||
//- TODO: solve the "a" problem for ng-file-select
|
||||
li(ng-file-select="importVm($files, host.UUID)")
|
||||
a
|
||||
i.fa.fa-upload.fa-fw
|
||||
| Import VM
|
||||
li.divider
|
||||
li
|
||||
a(ng-repeat="controller in [host.controller] | resolve track by controller.UUID", xo-sref="consoles_view({id: controller.UUID})")
|
||||
i.xo-icon-console.fa-fw
|
||||
| Console
|
||||
li(ng-if="!host.enabled")
|
||||
a(xo-click="enableHost(host.UUID)")
|
||||
i.fa.fa-check-circle.fa-fw
|
||||
| Enable
|
||||
li(ng-if="host.enabled")
|
||||
a(xo-click="disableHost(host.UUID)")
|
||||
i.fa.fa-times-circle.fa-fw
|
||||
| Disable
|
||||
li
|
||||
a(xo-click="rebootHost(host.UUID)")
|
||||
i.fa.fa-refresh.fa-fw
|
||||
| Reboot
|
||||
li(ng-if="host.power_state === 'Halted'")
|
||||
a(xo-click="startHost(host.UUID)")
|
||||
i.fa.fa-power-off.fa-fw
|
||||
| Start
|
||||
li(ng-if="host.power_state === 'Running'")
|
||||
a(xo-click="shutdownHost(host.UUID)")
|
||||
i.fa.fa-power-off.fa-fw
|
||||
| Shutdown
|
||||
li
|
||||
a(xo-click="restartToolStack(host.UUID)")
|
||||
i.fa.fa-retweet.fa-fw
|
||||
| Restart toolstack
|
||||
li(ng-if="pool.name_label")
|
||||
a(xo-click="pool_removeHost(host.UUID)")
|
||||
i.fa.fa-cloud-upload.fa-fw
|
||||
| Remove from pool
|
||||
li(ng-if="!pool.name_label")
|
||||
a(xo-click="pool_addHost(host.UUID)")
|
||||
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] | %}} 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] | %}}")
|
||||
//- 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="!host.VMs.length")
|
||||
//- | 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="host.VMs.length")
|
||||
table.table.table-hover.table-condensed
|
||||
//- Contains a VM.
|
||||
tr(ng-repeat="VM in host.VMs | resolve | orderBy:natural('name_label') track by VM.UUID", xo-sref="VMs_view({id: VM.UUID})")
|
||||
//- Handle used for drag & drop.
|
||||
td.grab
|
||||
//- Checkbox used for selection.
|
||||
td.select-vm
|
||||
input(type="checkbox", ng-model="selected_VMs[VM.UUID]", ng-change="updateVMSelection(VM.UUID)")
|
||||
//- Power state
|
||||
td.vm-power-state
|
||||
i.xo-icon-working(ng-if="isVMWorking(VM)", tooltip="{{VM.power_state}} and {{values(VM.current_operations)[0]}}")
|
||||
i(class="xo-icon-{{VM.power_state | lowercase}}",ng-if="!isVMWorking(VM)", tooltip="{{VM.power_state}}")
|
||||
//- VM name.
|
||||
td.vm-name.col-md-2
|
||||
p.vm {{VM.name_label}}
|
||||
//- Quick actions.
|
||||
td.vm-quick-buttons.col-md-2
|
||||
.quick-buttons
|
||||
a(tooltip="Shutdown VM", xo-click="stopVM(VM.UUID)")
|
||||
i.fa.fa-stop
|
||||
a(tooltip="Start VM", xo-click="startVM(VM.UUID)")
|
||||
i.fa.fa-play
|
||||
a(tooltip="Reboot VM", xo-click="rebootVM(VM.UUID)")
|
||||
i.fa.fa-refresh
|
||||
a(tooltip="VM Console", xo-sref="consoles_view({id: VM.UUID})")
|
||||
i.xo-icon-console
|
||||
//- Description.
|
||||
td.vm-description.col-md-4
|
||||
i(class="xo-icon-{{osType(VM.os_version.distro)}}",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
|
||||
.cpu
|
||||
| {{VM.memory.size | bytesToSize}}
|
||||
i.fa.fa-fw(ng-if="VM.PV_drivers")
|
||||
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
|
||||
| {{VM.addresses["0/ip"]}}
|
||||
//- Contains a pseudo-host which contains all VMs not in any hosts.
|
||||
.grid(ng-if="pool.VMs.length")
|
||||
//- 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 pool.VMs | resolve | orderBy:natural('name_label') track by VM.UUID", xo-sref="VMs_view({id: VM.UUID})")
|
||||
//- Handle used for drag & drop.
|
||||
td.grab
|
||||
//- Checkbox used for selection.
|
||||
td.select-vm
|
||||
input(type="checkbox", ng-model="selected_VMs[VM.UUID]", ng-change="updateVMSelection(VM.UUID)")
|
||||
//- Power state
|
||||
td.vm-power-state
|
||||
i.xo-icon-working(ng-if="isVMWorking(VM)", tooltip="{{VM.power_state}} and {{values(VM.current_operations)[0]}}")
|
||||
i(class="xo-icon-{{VM.power_state | lowercase}}",ng-if="!isVMWorking(VM)", tooltip="{{VM.power_state}}")
|
||||
//- VM name.
|
||||
td.vm-name.col-md-2
|
||||
p.vm {{VM.name_label}}
|
||||
//- Quick actions.
|
||||
td.vm-quick-buttons.col-md-2
|
||||
.quick-buttons
|
||||
a(tooltip="Shutdown VM", xo-click="stopVM(VM.UUID)")
|
||||
i.fa.fa-stop
|
||||
a(ng-if="VM.power_state == 'Suspended'", tooltip="Resume VM", xo-click="resumeVM(VM.UUID)")
|
||||
i.fa.fa-play
|
||||
a(ng-if="VM.power_state != 'Suspended'", tooltip="Start VM", xo-click="startVM(VM.UUID)")
|
||||
i.fa.fa-play
|
||||
a(tooltip="Reboot VM", xo-click="rebootVM(VM.UUID)")
|
||||
i.fa.fa-refresh
|
||||
a(tooltip="VM Console")
|
||||
i.xo-icon-console
|
||||
//- Description.
|
||||
td.vm-description.col-md-4
|
||||
i(class="xo-icon-{{osType(VM.os_version.distro)}}",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
|
||||
.cpu
|
||||
| {{VM.memory.size | bytesToSize}}
|
||||
i.fa.fa-fw(ng-if="VM.PV_drivers")
|
||||
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
|
||||
| {{VM.addresses["0/ip"]}}
|
||||
//- /Pseudo host containing VMs not on any hosts.
|
||||
//- /Hosts of this pool.
|
||||
//- /Pool with its children.
|
||||
@@ -1,681 +0,0 @@
|
||||
angular = require 'angular'
|
||||
isEmpty = require 'isempty'
|
||||
_difference = require 'lodash.difference'
|
||||
_sortBy = require 'lodash.sortby'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.vm', [
|
||||
require 'angular-ui-router'
|
||||
]
|
||||
.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
|
||||
|
||||
# Provides a fibonacci behaviour for stats refresh on failure
|
||||
$scope.refreshStatControl = refreshStatControl = {
|
||||
baseStatInterval: 5000
|
||||
timeout: null
|
||||
running: false
|
||||
|
||||
start: () ->
|
||||
return if this.running
|
||||
this.running = true
|
||||
this._reset()
|
||||
$scope.$on('$destroy', () =>
|
||||
this.stop()
|
||||
)
|
||||
$scope.refreshStats($scope.VM.UUID)
|
||||
return this._trig(Date.now())
|
||||
_trig: (t1) ->
|
||||
if this.running
|
||||
t2 = Date.now()
|
||||
timeLeft = Math.max(this.baseStatInterval * this._factor() - Math.max(t2 - t1 - (this.baseStatInterval * this._factor(true)), 0), 0)
|
||||
return this.timeout = $timeout(
|
||||
() => $scope.refreshStats($scope.VM.UUID),
|
||||
timeLeft
|
||||
)
|
||||
|
||||
.then () =>
|
||||
this._reset()
|
||||
return this._trig(t2)
|
||||
|
||||
.catch (err) =>
|
||||
if !this.running || $scope.VM.power_state isnt 'Running'
|
||||
this.stop()
|
||||
else
|
||||
this._next()
|
||||
this._trig(t2)
|
||||
if this.running
|
||||
throw err
|
||||
_reset: () ->
|
||||
this.terms = [1,1]
|
||||
_next: () ->
|
||||
this.terms = [this.terms[1], this.terms[0] + this.terms[1]]
|
||||
_factor: (p) ->
|
||||
return this.terms[if p then 0 else 1]
|
||||
stop: () ->
|
||||
if this.timeout
|
||||
$timeout.cancel(this.timeout)
|
||||
this.running = false
|
||||
return
|
||||
}
|
||||
|
||||
$scope.$watch(
|
||||
-> get $stateParams.id, 'VM'
|
||||
(VM) ->
|
||||
$scope.VM = VM
|
||||
|
||||
{byTypes} = xoApi
|
||||
$scope.hosts = byTypes.host
|
||||
|
||||
return unless VM?
|
||||
|
||||
# For the edition of this VM.
|
||||
$scope.memorySize = bytesToSizeFilter VM.memory.size
|
||||
|
||||
# build VDI list of this VM
|
||||
mountedIso = ''
|
||||
VDIs = []
|
||||
for VBD in VM.$VBDs
|
||||
oVbd = get VBD
|
||||
oVdi = get oVbd?.VDI
|
||||
VDIs.push oVdi if oVdi? && not oVbd.is_cd_drive
|
||||
if oVbd.is_cd_drive && oVdi? # "Load" the cd drive
|
||||
mountedIso = oVdi.UUID
|
||||
|
||||
$scope.VDIs = _sortBy(VDIs, (value) -> (get resolveVBD(value))?.position);
|
||||
|
||||
container = get VM.$container
|
||||
|
||||
if container.type is 'host'
|
||||
host = container
|
||||
pool = (get container.poolRef) ? {}
|
||||
else
|
||||
host = {}
|
||||
pool = container
|
||||
|
||||
$scope.networks = get pool.networks
|
||||
|
||||
default_SR = get pool.default_SR
|
||||
default_SR = if default_SR
|
||||
default_SR.UUID
|
||||
else
|
||||
''
|
||||
|
||||
SRs = $scope.SRs = get (merge pool.SRs, host.SRs)
|
||||
# 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'
|
||||
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
|
||||
}
|
||||
|
||||
$scope.refreshStats = (id) ->
|
||||
return xo.vm.refreshStats id
|
||||
|
||||
.then (result) ->
|
||||
result.cpuSeries = []
|
||||
result.cpus.forEach (v,k) ->
|
||||
result.cpuSeries.push 'CPU ' + k
|
||||
return
|
||||
result.vifSeries = []
|
||||
result.vifs.forEach (v,k) ->
|
||||
result.vifSeries.push '#' + Math.floor(k/2) + ' ' + if k % 2 then 'out' else 'in'
|
||||
return
|
||||
result.xvdSeries = []
|
||||
result.xvds.forEach (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
|
||||
result.date.forEach (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.UUID
|
||||
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.UUID
|
||||
}
|
||||
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 = {}
|
||||
angular.forEach data, (value, key) ->
|
||||
i = key.indexOf '/'
|
||||
(disks[key.slice 0, i] ?= {})[key.slice i + 1] = value
|
||||
return
|
||||
|
||||
promises = []
|
||||
|
||||
# Handle SR change.
|
||||
angular.forEach disks, (attributes, id) ->
|
||||
disk = get id
|
||||
if attributes.$SR isnt disk.$SR
|
||||
promises.push (migrateDisk id, attributes.$SR)
|
||||
|
||||
return
|
||||
|
||||
angular.forEach disks, (attributes, id) ->
|
||||
# Keep only changed attributes.
|
||||
disk = get id
|
||||
angular.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
|
||||
mountedPos = (get resolveVBD(get $scope.isoDeviceData.mounted))?.position
|
||||
{VDIs} = $scope
|
||||
VDIs.forEach (vdi, index) ->
|
||||
oVbd = get resolveVBD(vdi)
|
||||
offset = if (mountedPos? && index >= mountedPos) then 1 else 0
|
||||
if oVbd? && index isnt oVbd.position
|
||||
promises.push xoApi.call 'vbd.set', {id: oVbd.id, position: String(index + offset)}
|
||||
|
||||
return $q.all promises
|
||||
.catch (err) ->
|
||||
console.log(err);
|
||||
notify.error {
|
||||
title: 'saveDisks'
|
||||
message: err
|
||||
}
|
||||
|
||||
$scope.deleteDisk = (UUID) ->
|
||||
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: UUID}
|
||||
return
|
||||
return
|
||||
|
||||
#-----------------------------------------------------------------
|
||||
|
||||
# returns the ref 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.ref
|
||||
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 ->
|
||||
for log in $scope.VM.messages
|
||||
console.log "Remove log #{log}"
|
||||
xo.log.delete log
|
||||
|
||||
$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.osType = (osName) ->
|
||||
switch osName
|
||||
when 'debian','ubuntu','centos','redhat','oracle','gentoo','suse','fedora','sles'
|
||||
'linux'
|
||||
when 'windows'
|
||||
'windows'
|
||||
else
|
||||
'other'
|
||||
|
||||
$scope.isVMWorking = (VM) ->
|
||||
return false unless VM
|
||||
return true for _ of VM.current_operations
|
||||
false
|
||||
|
||||
# extract a value in a object
|
||||
$scope.values = (object) ->
|
||||
value for _, value of object
|
||||
|
||||
$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.UUID
|
||||
vm: $scope.VM.UUID
|
||||
}
|
||||
|
||||
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.UUID
|
||||
}
|
||||
|
||||
# console.log(params)
|
||||
return xoApi.call 'vm.attachDisk', params
|
||||
|
||||
.then -> $scope.creating = 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
|
||||
$scope.VM.VIFs.forEach (vf) ->
|
||||
int = get vf
|
||||
position = if int?.device > position then (get vf)?.device else position
|
||||
|
||||
position++
|
||||
|
||||
params = {
|
||||
vm: $scope.VM.UUID
|
||||
network: network.UUID
|
||||
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,683 +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 {{values(VM.current_operations)[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' || 'Paused')") Running on:
|
||||
dt(ng-if="VM.power_state == ('Halted')") Resident on:
|
||||
dd(ng-repeat="container in [VM.$container] | resolve")
|
||||
span(ng-if = 'container.type === "host"')
|
||||
a(xo-sref="hosts_view({id: container.UUID})")
|
||||
| {{container.name_label}}
|
||||
small
|
||||
span(ng-if="(container.poolRef | resolve).name_label")
|
||||
| (
|
||||
a(ui-sref="pools_view({id: (container.poolRef | resolve).UUID})") {{(container.poolRef | resolve).name_label}}
|
||||
| )
|
||||
a(
|
||||
ng-if = 'container.type === "pool"'
|
||||
xo-sref="pools_view({id: container.UUID})"
|
||||
)
|
||||
| {{container.name_label}}
|
||||
dt(ng-if="VM.addresses") Address
|
||||
dd(ng-repeat="IP in VM.addresses") {{IP}}
|
||||
dt(ng-if="!(VM.poolRef | resolve).HA_enabled") Auto Power
|
||||
dd(ng-if="!(VM.poolRef | resolve).HA_enabled")
|
||||
span(
|
||||
editable-checkbox="VM.auto_poweron"
|
||||
e-name="auto_poweron"
|
||||
e-form="vmSettings"
|
||||
)
|
||||
| {{VM.auto_poweron}}
|
||||
dt(ng-if="(VM.poolRef | resolve).HA_enabled") HA
|
||||
dd(ng-if="(VM.poolRef | 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.distro") OS:
|
||||
dd(ng-if="refreshStatControl.running && stats && VM.os_version.distro")
|
||||
| {{VM.os_version.name}} ({{VM.os_version.distro}})
|
||||
dt(ng-if="refreshStatControl.running && stats && VM.os_version.uname") Kernel:
|
||||
dd(ng-if="refreshStatControl.running && stats && VM.os_version.uname")
|
||||
| {{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.UUID)")
|
||||
| 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-md-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-md-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-md-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-md-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-{{osType(VM.os_version.distro)}}",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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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 | orderBy:natural('name_label') track by host.UUID")
|
||||
a(ng-click="migrateVM(VM.UUID, host.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID,VM.name_label,false)")
|
||||
i.fa.fa-code-fork.fa-fw
|
||||
| Fast clone
|
||||
li
|
||||
a(ng-click="cloneVM(VM.UUID,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.UUID)")
|
||||
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.UUID,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.UUID)")
|
||||
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.UUID})")
|
||||
i.xo-icon-console.fa-2x.fa-fw
|
||||
//- 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.UUID')
|
||||
td
|
||||
span(
|
||||
editable-text="VDI.name_label"
|
||||
e-name = '{{VDI.UUID}}/name_label'
|
||||
)
|
||||
| {{VDI.name_label}}
|
||||
td
|
||||
span(
|
||||
editable-text="VDI.name_description"
|
||||
e-name = '{{VDI.UUID}}/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
|
||||
span(
|
||||
editable-select="(VDI.$SR | resolve).ref"
|
||||
e-ng-options="SR.ref as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in writable_SRs"
|
||||
e-name = '{{VDI.UUID}}/$SR'
|
||||
)
|
||||
//- Are SR editable? will trigger moving VDI to the new SR
|
||||
a(xo-sref="SRs_view({id: (VDI.$SR | resolve).UUID})")
|
||||
| {{(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.UUID)"
|
||||
)
|
||||
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")
|
||||
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")
|
||||
i.fa.fa-plus(ng-if = '!creatingVdi')
|
||||
i.fa.fa-minus(ng-if = 'creatingVdi')
|
||||
| New Disk
|
||||
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.ref 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
|
||||
|
||||
//- 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.UUID")
|
||||
td VIF \#{{VIF.device}}
|
||||
td
|
||||
| {{VIF.MAC}}
|
||||
td
|
||||
| {{VIF.MTU}}
|
||||
td
|
||||
| {{(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.UUID)")
|
||||
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.UUID)")
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Remove this interface", xo-click="deleteVIF(VIF.UUID)")
|
||||
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-model = 'newInterfaceNetwork', ng-change = 'updateMTU(newInterfaceNetwork)', required, ng-options='network.name_label for network in networks')
|
||||
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' track by snapshot.UUID")
|
||||
td {{snapshot.snapshot_time*1e3 | date:"medium"}}
|
||||
td
|
||||
span(editable-text="snapshot.name_label", e-name="name_label", e-form="vmSnap", onbeforesave="saveSnapshot(snapshot.UUID, $data)")
|
||||
| {{snapshot.name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Export this snapshot", type="button", xo-click="exportVM(snapshot.UUID)")
|
||||
i.fa.fa-upload.fa-lg
|
||||
a(tooltip="Revert VM to this snapshot", xo-click="revertSnapshot(snapshot.UUID)")
|
||||
i.fa.fa-undo.fa-lg
|
||||
a(tooltip="Remove this snapshot", xo-click="deleteSnapshot(snapshot.UUID)")
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
.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.length", tooltip="Remove all logs", xo-click="deleteAllLog()")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="!VM.messages.length") No recent logs
|
||||
table.table.table-hover(ng-if="VM.messages.length")
|
||||
th Date
|
||||
th Name
|
||||
tr(ng-repeat="message in VM.messages | resolve | orderBy:'-time' track by message.UUID")
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="deleteLog(message.UUID)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
|
||||
.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' track by pci.UUID")
|
||||
td {{pci.pci_id}} ({{pci.class_name}})
|
||||
td {{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.UUID)")
|
||||
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.UUID, pci.pci_id)")
|
||||
i.fa.fa-link.fa-lg
|
||||
.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' track by vgpu.UUID")
|
||||
td {{vgpu.device}}
|
||||
td(ng-if="vgu.currentlyAttached")
|
||||
span.label.label-success Attached
|
||||
td(ng-if="!vgu.currentlyAttached")
|
||||
span.label.label-default Not attached
|
||||
102
app/node_modules/angular-no-vnc/index.js
generated
vendored
@@ -1,102 +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.
|
||||
139
app/node_modules/xo-api/index.js
generated
vendored
@@ -1,139 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import indexOf from 'lodash.indexof';
|
||||
let isArray = angular.isArray;
|
||||
|
||||
import 'angular-cookies';
|
||||
|
||||
import xoLib from 'xo-lib';
|
||||
|
||||
export default angular.module('xo-api', [
|
||||
'ngCookies',
|
||||
])
|
||||
.run(function ($rootScope) {
|
||||
// Ensure correct integration with Angular.
|
||||
xoLib.setScheduler(function (fn) {
|
||||
$rootScope.$evalAsync(fn);
|
||||
});
|
||||
})
|
||||
.service('xoApi', function ($cookieStore) {
|
||||
var xo = new xoLib.Xo();
|
||||
|
||||
try {
|
||||
let 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;
|
||||
}
|
||||
}
|
||||
|
||||
let getObject;
|
||||
{
|
||||
let {
|
||||
all: byIds,
|
||||
indexes: {
|
||||
ref: byRefs,
|
||||
UUID: byUuids
|
||||
}
|
||||
} = xo.objects;
|
||||
|
||||
// Look up an object by id, UUID or ref and optionally check its
|
||||
// type.
|
||||
getObject = (id, type) => {
|
||||
let object = byIds[id];
|
||||
|
||||
// If not found by id, try by UUID and ref.
|
||||
if (!object) {
|
||||
object = byUuids[id] || byRefs[id];
|
||||
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
|
||||
// In indexes, the object is wrapped in an array.
|
||||
object = object[0];
|
||||
}
|
||||
|
||||
if (
|
||||
// No type specifier.
|
||||
!type ||
|
||||
|
||||
// A single type.
|
||||
(type === object.type) ||
|
||||
|
||||
// An array of possible types.
|
||||
isArray(type) && (indexOf(type, object.type) === -1)
|
||||
) {
|
||||
return object;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
return xo.call(method, params);
|
||||
},
|
||||
|
||||
//------------------
|
||||
// Objects
|
||||
//------------------
|
||||
|
||||
get(id, types) {
|
||||
if (isArray(id)) {
|
||||
let objects = [];
|
||||
|
||||
angular.forEach(id, id => {
|
||||
let object = getObject(id, types);
|
||||
if (object) {
|
||||
objects.push(object);
|
||||
}
|
||||
});
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
return getObject(id, types);
|
||||
},
|
||||
|
||||
all: xo.objects.all,
|
||||
|
||||
byTypes: xo.objects.indexes.type,
|
||||
};
|
||||
})
|
||||
.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.$apply ->
|
||||
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"
|
||||
]
|
||||
}
|
||||
}
|
||||
110
app/node_modules/xo-filters/index.coffee
generated
vendored
@@ -1,110 +0,0 @@
|
||||
angular = require 'angular'
|
||||
|
||||
# TODO: split into multiple modules.
|
||||
module.exports = angular.module 'xoWebApp.filters', [
|
||||
require 'xo-api'
|
||||
]
|
||||
|
||||
# The bytes filters takes a number and formats it using adapted
|
||||
# units (KB, MB, etc.).
|
||||
.filter 'bytesToSize', ->
|
||||
(bytes, unit, base) ->
|
||||
unit ?= 'B'
|
||||
base ?= 1024
|
||||
powers = ['', 'K', 'M', 'G', 'T', 'P']
|
||||
|
||||
i = 0
|
||||
while bytes >= base
|
||||
bytes /= base
|
||||
++i
|
||||
|
||||
if bytes is -1
|
||||
"-"
|
||||
else
|
||||
# Maximum 1 decimals.
|
||||
bytes = ((bytes * 10)|0) / 10
|
||||
"#{bytes}#{powers[i]}B"
|
||||
|
||||
.filter 'sizeToBytes', ->
|
||||
regex = ///^
|
||||
(\d+(?:\.\d+)?) # digits ('.' digits)?
|
||||
\s* # Optional spaces beetween the digits and the unit.
|
||||
([kmgtp])? # Optional unit modifier K/M/G/T/P.
|
||||
b? # Optional unit (“b”), not meaningful.
|
||||
$///i
|
||||
factors = {
|
||||
k: 1024
|
||||
m: 1048576
|
||||
g: 1073741824
|
||||
t: 1099511627776
|
||||
p: 1125899906842624
|
||||
}
|
||||
(size) ->
|
||||
matches = regex.exec size
|
||||
|
||||
# If the input is invalid, just returns null.
|
||||
return null unless matches
|
||||
|
||||
modifier = matches[2]
|
||||
Math.round if modifier and (factor = factors[modifier.toLowerCase()])
|
||||
factor * matches[1]
|
||||
else
|
||||
matches[1]
|
||||
|
||||
# Simply returns the number of elements in the collection.
|
||||
.filter 'count', ->
|
||||
(collection) ->
|
||||
# Array.
|
||||
if angular.isArray collection
|
||||
return collection.length
|
||||
|
||||
# Object.
|
||||
count = 0
|
||||
for key of collection
|
||||
++count if collection.hasOwnProperty key
|
||||
|
||||
count
|
||||
|
||||
# Resolves links between objects.
|
||||
.filter('resolve', (xoApi) -> xoApi.get)
|
||||
|
||||
# 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.
|
||||
#
|
||||
# Note: This filter behaves nicely if the first argument is not an
|
||||
# array.
|
||||
.filter 'map', ->
|
||||
(items, fn) ->
|
||||
unless angular.isArray items
|
||||
return []
|
||||
|
||||
if angular.isString fn
|
||||
property = fn
|
||||
fn = (item) -> item[property]
|
||||
|
||||
fn item for item in items
|
||||
|
||||
.filter '%', ->
|
||||
(value) ->
|
||||
# If `value` is an array of two values, divide the first by the
|
||||
# second and mutiply by 100.
|
||||
if value.length is 2
|
||||
|
||||
# Special case, if the divider is 0, simply returns "N/A".
|
||||
return 'N/A' if value[1] is 0
|
||||
|
||||
result = 100 * value[0] / value[1]
|
||||
if isNaN result
|
||||
return 'N/A'
|
||||
|
||||
value = result
|
||||
|
||||
# No decimals at most.
|
||||
value = (Math.round value * 1e0) / 1e0
|
||||
|
||||
"#{value}%"
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
8
app/node_modules/xo-filters/package.json
generated
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"coffeeify"
|
||||
]
|
||||
}
|
||||
}
|
||||
264
app/node_modules/xo-services/index.coffee
generated
vendored
@@ -1,264 +0,0 @@
|
||||
angular = require 'angular'
|
||||
|
||||
filter = require 'lodash.filter'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# TODO: split into multiple modules.
|
||||
module.exports = angular.module 'xoWebApp.services', [
|
||||
require 'angular-animate'
|
||||
|
||||
require 'angular-notify-toaster'
|
||||
|
||||
require 'xo-api'
|
||||
]
|
||||
|
||||
.service 'notify', (toaster) ->
|
||||
notifier = (level) ->
|
||||
(options) ->
|
||||
if angular.isString options
|
||||
options = { message: options }
|
||||
else
|
||||
throw new Error 'missing message' unless options.message
|
||||
|
||||
toaster.pop(
|
||||
level
|
||||
options.title ? 'Xen-Orchestra'
|
||||
options.message
|
||||
)
|
||||
|
||||
{
|
||||
warning: notifier 'warning'
|
||||
error: notifier 'error'
|
||||
info: notifier 'info'
|
||||
# TODO: It is probably a bad design to have notification for
|
||||
# successful operations.
|
||||
# success: notifier 'success'
|
||||
}
|
||||
|
||||
.service 'xo', ($timeout, xoApi, notify) ->
|
||||
# FIXME: default mapper should be identity.
|
||||
defaultArgsMapper = (id) -> if id? then {id} else {}
|
||||
|
||||
action = (name, method, options) ->
|
||||
unless method
|
||||
return ->
|
||||
notify.info {
|
||||
title: name
|
||||
message: 'This feature has not been implemented yet.'
|
||||
}
|
||||
|
||||
# TODO: A (broken) promise should be returned for
|
||||
# consistency.
|
||||
|
||||
{argsMapper, notification} = options ? {}
|
||||
argsMapper ?= defaultArgsMapper
|
||||
|
||||
(args...) ->
|
||||
xoApi.call(
|
||||
method
|
||||
argsMapper args...
|
||||
).catch (error) ->
|
||||
unless notification is false
|
||||
code = error?.code
|
||||
message = if code is 2
|
||||
'You don\'t have the permission.'
|
||||
else
|
||||
'The action failed for unknown reason.'
|
||||
|
||||
notify.warning {
|
||||
title: name
|
||||
message
|
||||
}
|
||||
|
||||
console.error error
|
||||
|
||||
# Re-throws the error to make it available in the promise
|
||||
# chain.
|
||||
throw error
|
||||
|
||||
# The interface.
|
||||
xo = {
|
||||
acl:
|
||||
add: action('Adding an ACL entry', 'acl.add', {
|
||||
argsMapper: (subject, object) => {subject, object},
|
||||
})
|
||||
get: action('Getting ACLs', 'acl.get')
|
||||
remove: action('Remove an ACL entry', 'acl.remove', {
|
||||
argsMapper: (subject, object) => {subject, object},
|
||||
})
|
||||
|
||||
|
||||
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 'Atach host'#, 'host.attach'
|
||||
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
|
||||
# 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', {
|
||||
argsMapper: (params) -> angular.copy(params)
|
||||
}
|
||||
remove: action 'Remove server', 'server.remove', argsMapper: (id) -> {id}
|
||||
getAll: action 'Getting server', 'server.getAll'
|
||||
set: action 'Save server', 'server.set', {
|
||||
argsMapper: (params) -> angular.copy(params)
|
||||
}
|
||||
connect: action 'Connect to a server', 'server.connect', notification: false, argsMapper: (id) -> {id}
|
||||
disconnect: action 'Disconnect from a server', 'server.disconnect', argsMapper: (id) -> {id}
|
||||
|
||||
task:
|
||||
cancel: action 'Cancel task', 'task.cancel', argsMapper: (id) -> {id}
|
||||
destroy: action 'Destroy task', 'task.destroy', argsMapper: (id) -> {id}
|
||||
|
||||
user:
|
||||
create: action 'Create user', 'user.create', {
|
||||
argsMapper: (params) -> angular.copy(params)
|
||||
}
|
||||
delete: action 'Delete user', 'user.delete', argsMapper: (id) -> {id: "#{id}"}
|
||||
getAll: action 'Getting users', 'user.getAll'
|
||||
set: action 'Save user', 'user.set', {
|
||||
argsMapper: (params) -> angular.copy(params)
|
||||
}
|
||||
|
||||
vm:
|
||||
convert: action 'Convert VM', 'vm.convert', {
|
||||
argsMapper: (id) -> {id}
|
||||
}
|
||||
clone: action 'Copy VM', 'vm.clone', {
|
||||
argsMapper: (id, name, full_copy) -> {id, name, full_copy} #todo : sr ref to choose target SR
|
||||
}
|
||||
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', {
|
||||
argsMapper: (params) -> angular.copy(params)
|
||||
}
|
||||
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', {
|
||||
argsMapper: (id) -> {id}
|
||||
}
|
||||
# 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'
|
||||
}
|
||||
|
||||
# TODO: should probably be merged in the main collection in xo-lib.
|
||||
currentAcls = Object.create(null)
|
||||
updateCurrentAcls = () ->
|
||||
xoApi.call('acl.getCurrent').then((acls) ->
|
||||
currentAcls = Object.create(null)
|
||||
for acl in acls
|
||||
object = xoApi.get(acl.object)
|
||||
if object
|
||||
currentAcls[object.id] = true
|
||||
|
||||
$timeout(updateCurrentAcls, 1e4)
|
||||
return
|
||||
)
|
||||
updateCurrentAcls()
|
||||
|
||||
# Adds the dynamic properties.
|
||||
Object.defineProperties(xo, {
|
||||
byTypes: { get: ->
|
||||
throw new Error('use xoApi.byTypes instead');
|
||||
},
|
||||
get: { get: ->
|
||||
throw new Error('use xoApi.get() instead');
|
||||
},
|
||||
|
||||
currentAcls: { get: -> currentAcls },
|
||||
})
|
||||
|
||||
xo.canAccess = (id) ->
|
||||
{id} = id if id.id
|
||||
|
||||
return (
|
||||
# Administrators can access everything.
|
||||
xoApi.user and (xoApi.user.permission is 'admin') or
|
||||
|
||||
# Check if the id is in the ACLs table.
|
||||
(id of currentAcls) or
|
||||
|
||||
# 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.
|
||||
(id = xoApi.get(id)?.id) and (id of currentAcls)
|
||||
)
|
||||
|
||||
# Returns the interface.
|
||||
xo
|
||||
|
||||
.filter 'xoHideUnauthorized', (xo) ->
|
||||
{canAccess} = xo
|
||||
return (objects) -> filter objects, xo.canAccess
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
8
app/node_modules/xo-services/package.json
generated
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"coffeeify"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,645 +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 xEditable.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO: do not use CSS import but includes
|
||||
//
|
||||
// This syntax is not yet supported for .css files.
|
||||
//
|
||||
// See https://github.com/sass/node-sass/issues/618
|
||||
//@import "../../dist/bower_components/angular-xeditable/dist/css/xeditable";
|
||||
|
||||
@import "/bower_components/angular-xeditable/dist/css/xeditable.css";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Angular Notify Toaster.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO: do not use CSS import but includes
|
||||
//
|
||||
// This syntax is not yet supported for .css files.
|
||||
//
|
||||
// See https://github.com/sass/node-sass/issues/618
|
||||
//@import "../../dist/bower_components/angular-notify-toaster/toaster";
|
||||
|
||||
@import "/bower_components/angular-notify-toaster/toaster.css";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// UI Select.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
@import "/bower_components/angular-ui-select/dist/select.css";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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-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;
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// 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;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// 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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// 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 {
|
||||
position: fixed;
|
||||
padding-top: 1em;
|
||||
top: 51px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
display: block;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||
background-color: #242628;
|
||||
border-right: 1px solid #eee;
|
||||
width: 212px;
|
||||
}
|
||||
|
||||
.settings-menu li a {
|
||||
font-size: 1em;
|
||||
padding-: 1em;
|
||||
padding-left: 2em;
|
||||
display: block;
|
||||
color: #f8f8f8;
|
||||
&:hover, &:focus {
|
||||
background-color: #2e3133;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
margin-left: 212px;
|
||||
}
|
||||
|
||||
.fa-menu {
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
491
gulpfile.js
@@ -1,344 +1,291 @@
|
||||
// Julien Fontanet gulpfile.js
|
||||
//
|
||||
// https://gist.github.com/julien-f/4af9f3865513efeff6ab
|
||||
'use strict'
|
||||
|
||||
'use strict';
|
||||
// ===================================================================
|
||||
|
||||
//====================================================================
|
||||
|
||||
var gulp = require('gulp');
|
||||
|
||||
// All plugins are loaded (on demand) by gulp-load-plugins.
|
||||
var $ = require('gulp-load-plugins')();
|
||||
|
||||
//====================================================================
|
||||
|
||||
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'
|
||||
var DIST_DIR = __dirname + '/dist'
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Set to 0 to choose a random port at each run.
|
||||
var SERVER_PORT = LIVERELOAD_PORT + 1;
|
||||
var SERVER_PORT = LIVERELOAD_PORT + 1
|
||||
|
||||
// Address the server should bind to.
|
||||
//
|
||||
// - `'localhost'` to make it accessible from this host only
|
||||
// - `null` to make it accessible for the whole network
|
||||
var SERVER_ADDR = 'localhost';
|
||||
var SERVER_ADDR = 'localhost'
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
var PRODUCTION = process.argv.indexOf('--production') !== -1
|
||||
var DEVELOPMENT = !PRODUCTION
|
||||
|
||||
// Create a noop duplex stream.
|
||||
var noop = function () {
|
||||
var PassThrough = require('stream').PassThrough;
|
||||
// ===================================================================
|
||||
|
||||
noop = function () {
|
||||
return new PassThrough({
|
||||
objectMode: true
|
||||
});
|
||||
};
|
||||
var gulp = require('gulp')
|
||||
|
||||
return noop.apply(this, arguments);
|
||||
};
|
||||
// ===================================================================
|
||||
|
||||
// Browserify plugin for gulp.js which uses watchify in development
|
||||
// mode.
|
||||
function browserify(path, opts) {
|
||||
opts || (opts = {});
|
||||
|
||||
var bundler = require('browserify')({
|
||||
basedir: SRC_DIR,
|
||||
debug: !PRODUCTION,
|
||||
entries: [path],
|
||||
extensions: opts.extensions,
|
||||
standalone: opts.standalone,
|
||||
|
||||
// Required by Watchify.
|
||||
cache: {},
|
||||
packageCache: {},
|
||||
fullPaths: !PRODUCTION,
|
||||
});
|
||||
|
||||
if (!PRODUCTION) {
|
||||
bundler = require('watchify')(bundler);
|
||||
bundler.plugin('bundle-collapser/plugin');
|
||||
function lazyFn (factory) {
|
||||
var fn = function () {
|
||||
fn = factory()
|
||||
return fn.apply(this, arguments)
|
||||
}
|
||||
|
||||
// Append the extension if necessary.
|
||||
if (!/\.js$/.test(path)) {
|
||||
path += '.js';
|
||||
return function () {
|
||||
return fn.apply(this, arguments)
|
||||
}
|
||||
|
||||
// Absolute path.
|
||||
path = require('path').resolve(SRC_DIR, path);
|
||||
|
||||
var proxy = noop();
|
||||
|
||||
var write;
|
||||
function bundle() {
|
||||
bundler.bundle(function onBundleComplete(err, buf) {
|
||||
if (err) {
|
||||
proxy.emit('error', err);
|
||||
return;
|
||||
}
|
||||
|
||||
write(new (require('vinyl'))({
|
||||
base: SRC_DIR,
|
||||
path: path,
|
||||
contents: buf,
|
||||
}));
|
||||
});
|
||||
}
|
||||
if (PRODUCTION) {
|
||||
write = proxy.end.bind(proxy);
|
||||
} else {
|
||||
proxy = $.plumber().pipe(proxy);
|
||||
write = proxy.write.bind(proxy);
|
||||
bundler.on('update', bundle);
|
||||
}
|
||||
bundle();
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
// 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);
|
||||
};
|
||||
var livereload = lazyFn(function () {
|
||||
var livereload = require('gulp-livereload')
|
||||
livereload.listen(LIVERELOAD_PORT)
|
||||
|
||||
return livereload
|
||||
})
|
||||
|
||||
var pipe = lazyFn(function () {
|
||||
var pipe = require('nice-pipe')
|
||||
|
||||
return PRODUCTION
|
||||
? pipe
|
||||
: function () {
|
||||
return require('gulp-plumber')().pipe(pipe.apply(this, arguments))
|
||||
}
|
||||
})
|
||||
|
||||
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 = (function () {
|
||||
var resolvePath = require('path').resolve;
|
||||
function resolve(path) {
|
||||
if (path) {
|
||||
return resolvePath(SRC_DIR, path);
|
||||
}
|
||||
return SRC_DIR;
|
||||
var src = lazyFn(function () {
|
||||
function resolve (path) {
|
||||
return path
|
||||
? resolvePath(SRC_DIR, path)
|
||||
: SRC_DIR
|
||||
}
|
||||
|
||||
if (PRODUCTION)
|
||||
{
|
||||
return function src(pattern, base) {
|
||||
base = resolve(base);
|
||||
return PRODUCTION
|
||||
? function src (pattern, base) {
|
||||
base = resolve(base)
|
||||
|
||||
return gulp.src(pattern, {
|
||||
base: base,
|
||||
cwd: base,
|
||||
});
|
||||
};
|
||||
}
|
||||
sourcemaps: true
|
||||
})
|
||||
}
|
||||
: function src (pattern, base) {
|
||||
base = resolve(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())
|
||||
;
|
||||
};
|
||||
})();
|
||||
return pipe(
|
||||
gulp.src(pattern, {
|
||||
base: base,
|
||||
cwd: base,
|
||||
sourcemaps: true
|
||||
}),
|
||||
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 = (function () {
|
||||
var resolvePath = require('path').resolve;
|
||||
function resolve(path) {
|
||||
if (path) {
|
||||
return resolvePath(DIST_DIR, path);
|
||||
var dest = lazyFn(function () {
|
||||
function resolve (path) {
|
||||
return path
|
||||
? resolvePath(DIST_DIR, path)
|
||||
: DIST_DIR
|
||||
}
|
||||
|
||||
var opts = {
|
||||
sourcemaps: {
|
||||
path: '.'
|
||||
}
|
||||
return DIST_DIR;
|
||||
}
|
||||
|
||||
if (PRODUCTION)
|
||||
{
|
||||
return function dest(path) {
|
||||
return gulp.dest(resolve(path));
|
||||
};
|
||||
return PRODUCTION
|
||||
? function dest (path) {
|
||||
return gulp.dest(resolve(path), opts)
|
||||
}
|
||||
: function dest (path) {
|
||||
return pipe(
|
||||
gulp.dest(resolve(path), opts),
|
||||
livereload()
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function browserify (path, opts) {
|
||||
if (opts == null) {
|
||||
opts = {}
|
||||
}
|
||||
|
||||
var livereload = function () {
|
||||
$.livereload.listen(LIVERELOAD_PORT);
|
||||
var bundler = require('browserify')(path, {
|
||||
basedir: SRC_DIR,
|
||||
debug: DEVELOPMENT,
|
||||
extensions: opts.extensions,
|
||||
fullPaths: DEVELOPMENT,
|
||||
standalone: opts.standalone,
|
||||
|
||||
livereload = $.livereload;
|
||||
return livereload();
|
||||
};
|
||||
|
||||
return function dest(path) {
|
||||
return combine(
|
||||
gulp.dest(resolve(path)),
|
||||
livereload()
|
||||
);
|
||||
};
|
||||
})();
|
||||
|
||||
//====================================================================
|
||||
|
||||
gulp.task('buildPages', function buildPages() {
|
||||
return src('[i]ndex.jade')
|
||||
.pipe($.jade())
|
||||
.pipe(PRODUCTION ? noop() : $.embedlr({ port: LIVERELOAD_PORT }))
|
||||
.pipe(dest())
|
||||
;
|
||||
});
|
||||
|
||||
gulp.task('buildScripts', [
|
||||
'installBowerComponents',
|
||||
], function buildScripts() {
|
||||
return browserify('./app', {
|
||||
extensions: '.coffee .jade'.split(' '),
|
||||
// Required by Watchify.
|
||||
cache: {},
|
||||
packageCache: {}
|
||||
})
|
||||
// Annotate the code before minification (for Angular.js)
|
||||
.pipe($.ngAnnotate({
|
||||
add: true,
|
||||
'single_quotes': true,
|
||||
}))
|
||||
.pipe(PRODUCTION ? $.uglify() : noop())
|
||||
.pipe(dest())
|
||||
;
|
||||
});
|
||||
|
||||
gulp.task('buildStyles', [
|
||||
'installBowerComponents',
|
||||
], function buildStyles() {
|
||||
return src('styles/[m]ain.scss')
|
||||
.pipe($.sass())
|
||||
.pipe($.autoprefixer([
|
||||
'last 1 version',
|
||||
'> 1%',
|
||||
]))
|
||||
.pipe(PRODUCTION ? $.csso() : noop())
|
||||
.pipe(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()
|
||||
);
|
||||
bundler.plugin('bundle-collapser/plugin')
|
||||
} else {
|
||||
imgStream = noop();
|
||||
bundler = require('watchify')(bundler)
|
||||
}
|
||||
|
||||
return merge(
|
||||
src([
|
||||
'[f]avicon.ico',
|
||||
'images/**/*',
|
||||
]).pipe(imgStream),
|
||||
// Append the extension if necessary.
|
||||
if (!/\.js$/.test(path)) {
|
||||
path += '.js'
|
||||
}
|
||||
path = resolvePath(SRC_DIR, path)
|
||||
|
||||
var stream = new (require('readable-stream'))({
|
||||
objectMode: true
|
||||
})
|
||||
|
||||
var write
|
||||
function bundle () {
|
||||
bundler.bundle(function onBundle (error, buffer) {
|
||||
if (error) {
|
||||
stream.emit('error', error)
|
||||
return
|
||||
}
|
||||
|
||||
write(new (require('vinyl'))({
|
||||
base: SRC_DIR,
|
||||
contents: buffer,
|
||||
path: path
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
if (PRODUCTION) {
|
||||
write = function (data) {
|
||||
stream.push(data)
|
||||
stream.push(null)
|
||||
}
|
||||
} else {
|
||||
stream = require('gulp-plumber')().pipe(stream)
|
||||
write = function (data) {
|
||||
stream.push(data)
|
||||
}
|
||||
|
||||
bundler.on('update', bundle)
|
||||
}
|
||||
|
||||
stream._read = function () {
|
||||
this._read = function () {}
|
||||
bundle()
|
||||
}
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
gulp.task(function buildPages () {
|
||||
return pipe(
|
||||
src('index.jade'),
|
||||
require('gulp-jade')(),
|
||||
DEVELOPMENT && require('gulp-embedlr')({
|
||||
port: LIVERELOAD_PORT
|
||||
}),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
|
||||
gulp.task(function buildScripts () {
|
||||
return pipe(
|
||||
browserify('./index.js'),
|
||||
PRODUCTION && require('gulp-uglify')({
|
||||
mangle: false // Avoid breaking Angular deps injector.
|
||||
}),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
|
||||
gulp.task(function buildStyles () {
|
||||
return pipe(
|
||||
src('index.scss'),
|
||||
require('gulp-sass')(),
|
||||
require('gulp-autoprefixer')([
|
||||
'last 1 version',
|
||||
'> 1%'
|
||||
]),
|
||||
PRODUCTION && require('gulp-csso')(),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
|
||||
gulp.task(function copyAssets () {
|
||||
return pipe(
|
||||
src(['assets/**/*', 'favicon.*']),
|
||||
src(
|
||||
'fontawesome-webfont.*',
|
||||
__dirname + '/node_modules/font-awesome/fonts/'
|
||||
)
|
||||
).pipe(dest());
|
||||
});
|
||||
__dirname + '/node_modules/font-awesome/fonts'
|
||||
),
|
||||
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',
|
||||
]);
|
||||
'copyAssets'
|
||||
))
|
||||
|
||||
gulp.task('clean', function clean(done) {
|
||||
require('rimraf')(DIST_DIR, done);
|
||||
});
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
gulp.task('distclean', ['clean'], function distclean(done) {
|
||||
require('rimraf')(BOWER_DIR, done);
|
||||
});
|
||||
gulp.task(function clean (done) {
|
||||
require('rimraf')(DIST_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() {
|
||||
var address = this.address();
|
||||
.listen(SERVER_PORT, SERVER_ADDR, function onListen () {
|
||||
var address = this.address()
|
||||
|
||||
var port = address.port;
|
||||
address = address.address;
|
||||
var port = address.port
|
||||
address = address.address
|
||||
|
||||
// Correctly handle IPv6 addresses.
|
||||
if (address.indexOf(':') !== -1) {
|
||||
address = '['+ address +']';
|
||||
address = '[' + address + ']'
|
||||
}
|
||||
|
||||
console.log('Listening on http://'+ address +':'+ port);
|
||||
/* jshint devel: true*/
|
||||
console.log('Listening on http://' + address + ':' + port)
|
||||
})
|
||||
.on('close', function serverOnClose() {
|
||||
done();
|
||||
.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-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');
|
||||
16
node_modules/has.js
generated
vendored
@@ -1,16 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//====================================================================
|
||||
|
||||
module.exports = (function (hasOwnProperty) {
|
||||
/* jshint eqnull: true */
|
||||
|
||||
return hasOwnProperty ?
|
||||
function has(obj, prop) {
|
||||
return (obj != null) && hasOwnProperty.call(obj, prop);
|
||||
} :
|
||||
function has(obj, prop) {
|
||||
return (obj != null) && obj[prop] !== undefined;
|
||||
}
|
||||
;
|
||||
})(Object.prototype.hasOwnProperty);
|
||||
21
node_modules/isempty.js
generated
vendored
@@ -1,21 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//====================================================================
|
||||
|
||||
var has = require('has');
|
||||
|
||||
//====================================================================
|
||||
|
||||
module.exports = function isEmpty(obj) {
|
||||
if (has(obj, 'length')) {
|
||||
return obj.length === 0;
|
||||
}
|
||||
|
||||
var prop;
|
||||
for (prop in obj) {
|
||||
if (has(obj, prop)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
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;
|
||||
141
package.json
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "xo-web",
|
||||
"version": "3.9.0",
|
||||
"license": "AGPL3",
|
||||
"version": "5.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
"xen",
|
||||
@@ -9,52 +10,8 @@
|
||||
"xen-orchestra",
|
||||
"web"
|
||||
],
|
||||
"devDependencies": {
|
||||
"angular": "^1.3.15",
|
||||
"angular-animate": "^1.3.15",
|
||||
"angular-bootstrap": "^0.12.0",
|
||||
"angular-cookies": "^1.3.15",
|
||||
"angular-ui-router": "^0.2.13",
|
||||
"angular-ui-utils": "^0.1.1",
|
||||
"babelify": "^6.0.2",
|
||||
"bluebird": "^2.9.14",
|
||||
"bootstrap-sass": "^3.3.4",
|
||||
"bower": "^1.3.12",
|
||||
"browserify": "^9.0.3",
|
||||
"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-coffee": "^2.3.1",
|
||||
"gulp-csso": "^1.0.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.8.1",
|
||||
"gulp-ng-annotate": "^0.5.2",
|
||||
"gulp-plumber": "^1.0.0",
|
||||
"gulp-sass": "^1.3.3",
|
||||
"gulp-uglify": "^1.1.0",
|
||||
"gulp-watch": "^4.2.0",
|
||||
"in-publish": "^1.1.1",
|
||||
"jquery": "^2.1.3",
|
||||
"lodash.difference": "^3.0.1",
|
||||
"lodash.filter": "^3.0.0",
|
||||
"lodash.includes": "^3.1.1",
|
||||
"lodash.indexof": "^3.0.2",
|
||||
"lodash.sortby": "^3.1.0",
|
||||
"lodash.throttle": "^3.0.1",
|
||||
"novnc-node": "^0.5.1",
|
||||
"rimraf": "^2.3.2",
|
||||
"vinyl": "^0.4.6",
|
||||
"watchify": "^3.1.1",
|
||||
"xo-lib": "^0.6.3"
|
||||
},
|
||||
"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"
|
||||
@@ -63,22 +20,92 @@
|
||||
"name": "Julien Fontanet",
|
||||
"email": "julien.fontanet@vates.fr"
|
||||
},
|
||||
"preferGlobal": false,
|
||||
"main": "dist/",
|
||||
"bin": {},
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^5.0.0-beta6",
|
||||
"babel-plugin-transform-runtime": "^6.4.3",
|
||||
"babel-preset-es2015": "^6.3.13",
|
||||
"babel-preset-react": "^6.3.13",
|
||||
"babel-preset-stage-0": "^6.3.13",
|
||||
"babelify": "^7.2.0",
|
||||
"bootstrap": "github:twbs/bootstrap#v4-dev",
|
||||
"browserify": "^13.0.0",
|
||||
"browserify-plain-jade": "^0.2.2",
|
||||
"bundle-collapser": "^1.2.1",
|
||||
"clarify": "^1.0.5",
|
||||
"connect": "^3.4.0",
|
||||
"dependency-check": "^2.5.1",
|
||||
"font-awesome": "^4.5.0",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-autoprefixer": "^3.1.0",
|
||||
"gulp-csso": "^1.0.1",
|
||||
"gulp-embedlr": "^0.5.2",
|
||||
"gulp-jade": "^1.1.0",
|
||||
"gulp-livereload": "^3.8.1",
|
||||
"gulp-plumber": "^1.0.1",
|
||||
"gulp-sass": "^2.1.1",
|
||||
"gulp-uglify": "^1.5.1",
|
||||
"gulp-watch": "^4.3.5",
|
||||
"history": "^2.0.0-rc2",
|
||||
"jsonrpc-websocket-client": "0.0.1-4",
|
||||
"mocha": "^2.3.4",
|
||||
"must": "^0.13.1",
|
||||
"nice-pipe": "^0.3.4",
|
||||
"nyc": "^5.3.0",
|
||||
"react": "^0.14.6",
|
||||
"react-dom": "^0.14.6",
|
||||
"react-intl": "^1.2.2",
|
||||
"react-redux": "^4.0.6",
|
||||
"react-router": "^2.0.0-rc5",
|
||||
"react-router-redux": "^2.1.0",
|
||||
"readable-stream": "^2.0.5",
|
||||
"redux": "^3.0.5",
|
||||
"redux-devtools": "^3.0.1",
|
||||
"redux-router": "^1.0.0-beta7",
|
||||
"redux-thunk": "^1.0.3",
|
||||
"serve-static": "^1.10.2",
|
||||
"source-map-support": "^0.4.0",
|
||||
"standard": "^5.4.1",
|
||||
"trace": "^2.0.2",
|
||||
"vinyl": "^1.1.1",
|
||||
"watchify": "^3.7.0",
|
||||
"xo-lib": "^0.8.0-1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp distclean && gulp build --production",
|
||||
"dev": "gulp build",
|
||||
"prepublish": "in-publish && npm run build || in-install"
|
||||
"build": "gulp build --production",
|
||||
"dev": "gulp build server",
|
||||
"dev-test": "mocha --opts .mocha.opts --watch --reporter=min \"dist/**/*.spec.js\"",
|
||||
"lint": "standard",
|
||||
"depcheck": "dependency-check ./package.json",
|
||||
"posttest": "npm run lint && npm run depcheck",
|
||||
"prepublish": "npm run build",
|
||||
"test": "nyc mocha --opts .mocha.opts \"dist/**/*.spec.js\""
|
||||
},
|
||||
"browser": {
|
||||
"node_modules/ws/index.js": "./ws.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify",
|
||||
"browserify-plain-jade",
|
||||
"coffeeify"
|
||||
"browserify-plain-jade"
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.3.19",
|
||||
"redux-promise": "^0.5.1"
|
||||
}
|
||||
}
|
||||
|
||||
50
src/index.jade
Normal file
@@ -0,0 +1,50 @@
|
||||
//- HTML 5 Doctype
|
||||
doctype html
|
||||
|
||||
//- The `no-js` class will be automatically removed if JavaScript is
|
||||
//- available.
|
||||
html.no-js(
|
||||
dir = 'ltr'
|
||||
lang = 'en'
|
||||
)
|
||||
|
||||
head
|
||||
|
||||
meta(charset = 'utf-8')
|
||||
|
||||
//- Makes sure IE is using the last engine available.
|
||||
meta(
|
||||
http-equiv = 'x-ua-compatible'
|
||||
content = 'ie=edge,chrome=1'
|
||||
)
|
||||
|
||||
//- .visible-js to display content only when JavaScript is ENABLED.
|
||||
//- .hidden-js to display content only when JavaScript is DISABLED.
|
||||
script !function(d){d.className=d.className.replace(/\bno-js\b/,'js')}(document.documentElement)
|
||||
style .no-js .visible-js,.js .hidden-js{display:none}
|
||||
|
||||
//- (TODO: 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'
|
||||
)
|
||||
|
||||
title Xen Orchestra
|
||||
|
||||
link(
|
||||
rel = 'stylesheet'
|
||||
href = 'index.css'
|
||||
)
|
||||
|
||||
body
|
||||
|
||||
xo-app
|
||||
.container: .row: .col-xs-12
|
||||
h1.hidden-js.text-danger JavaScript is required for Xen Orchestra!
|
||||
h1.visible-js.text-muted
|
||||
i.xo-icon-loading
|
||||
| Xen Orchestra is loading…
|
||||
|
||||
script(src = 'index.js')
|
||||
9
src/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import { render } from 'react-dom'
|
||||
|
||||
import XoApp from './xo-app'
|
||||
|
||||
render(
|
||||
<XoApp/>,
|
||||
document.getElementsByTagName('xo-app')[0]
|
||||
)
|
||||
23
src/index.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
// http://v4-alpha.getbootstrap.com/getting-started/flexbox/#how-it-works
|
||||
$enable-flex: true;
|
||||
|
||||
@import "../node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
$fa-font-path: "./";
|
||||
|
||||
@import "../node_modules/font-awesome/scss/font-awesome";
|
||||
|
||||
// Replace Bootstrap's glyphicons by Font Awesome.
|
||||
.glyphicon {
|
||||
@extend .fa;
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
.xo-icon-loading {
|
||||
@extend .fa;
|
||||
@extend .fa-spinner;
|
||||
@extend .fa-pulse;
|
||||
}
|
||||
88
src/store/actions.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/*import Xo from 'xo-lib'
|
||||
import { createBackoff } from 'jsonrpc-websocket-client'*/
|
||||
|
||||
/* action type */
|
||||
export const VM_CREATE = 'VM_CREATE' // create a VM in store
|
||||
export const VM_EDIT = 'VM_EDIT' // edit a vm in store
|
||||
export const VM_SAVE = 'VM_SAVE' // want to save a vm from store to server
|
||||
export const VM_SAVED = 'VM_SAVED' // VM is saved on server
|
||||
|
||||
export const SIGN_IN = 'SIGN_IN' // ask to signin
|
||||
export const SIGNED_IN = 'SIGNED_IN' // is signed in
|
||||
export const SIGN_OUT = 'SIGN_OUT' // want to sign out
|
||||
export const SIGNED_OUT = 'SIGNED_OUT' // signed out
|
||||
export const SESSION_PATCH = 'SESSION_PATCH' // signed out
|
||||
|
||||
/* action creator
|
||||
* they HAVE TO return an action with the mandatory field type, and an optiona payload
|
||||
* they MAY dispatch ( emit ) other actions, async or not
|
||||
* action will be used by reducers
|
||||
*/
|
||||
export function patchSession (patch) {
|
||||
return {
|
||||
type: SESSION_PATCH,
|
||||
payload: patch
|
||||
}
|
||||
}
|
||||
export function signIn () {
|
||||
// using redux thunk https://github.com/gaearon/redux-thunk
|
||||
// instead of returning one promise, it can dispatch multiple events
|
||||
// that way it become trivial to inform user of progress
|
||||
return dispatch => {
|
||||
setTimeout(() => {
|
||||
// Yay! Can invoke sync or async actions with `dispatch`
|
||||
dispatch(signedIn({userId: Math.floor(Math.random() * 1000)}))
|
||||
}, 10)
|
||||
|
||||
dispatch({type: 'SIGN_IN'}) // immediatly inform the sore that we'll try to signin
|
||||
}
|
||||
}
|
||||
|
||||
// you can also directly return an action
|
||||
export function signedIn (payload) {
|
||||
return {
|
||||
type: 'SIGNED_IN',
|
||||
payload
|
||||
}
|
||||
}
|
||||
|
||||
export function VMCreate (payload) {
|
||||
return {
|
||||
type: VM_CREATE,
|
||||
payload
|
||||
}
|
||||
}
|
||||
|
||||
export function VMEdit( payload){
|
||||
// should check if there s a vm id ?
|
||||
return {
|
||||
type: VM_EDIT,
|
||||
payload
|
||||
}
|
||||
}
|
||||
|
||||
export function VMSave (vmId) {
|
||||
//should call xoApi and save to server
|
||||
return dispatch => {
|
||||
setTimeout(function(){
|
||||
console.log('really save')
|
||||
// Yay! Can invoke sync or async actions with `dispatch`
|
||||
dispatch(VMSaved({id: Math.floor(Math.random() * 1000)}))
|
||||
}, 1000)
|
||||
|
||||
dispatch({
|
||||
type: VM_SAVE,
|
||||
payload: {id: vmId}
|
||||
}) // immediatly inform the sore that we'll try to save
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function VMSaved (vm) {
|
||||
//xoAPi is happy, let's tell everyone this vm is save
|
||||
return {
|
||||
type: VM_SAVED,
|
||||
payload: vm
|
||||
}
|
||||
}
|
||||
35
src/store/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
import {
|
||||
applyMiddleware,
|
||||
combineReducers,
|
||||
compose,
|
||||
createStore
|
||||
} from 'redux'
|
||||
|
||||
import reducer from './reducers'
|
||||
import thunk from 'redux-thunk'
|
||||
import initialState from './initialStoreState'
|
||||
|
||||
|
||||
let createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
|
||||
|
||||
|
||||
const store = createStoreWithMiddleware(reducer)
|
||||
|
||||
export default store
|
||||
|
||||
// ===================================================================
|
||||
/*
|
||||
export default compose(
|
||||
applyMiddleware(reduxPromise),
|
||||
// applyMiddleware(reduxThunk),
|
||||
reduxReactRouter({
|
||||
createHistory: createHashHistory
|
||||
})
|
||||
)(createStore)(combineReducers({
|
||||
...reducers,
|
||||
router: routerStateReducer
|
||||
}))
|
||||
|
||||
export * as actions from './actions'
|
||||
*/
|
||||
11
src/store/initialStoreState.js
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
export default {
|
||||
xoApi: {
|
||||
}, // somehting like xoApi.all
|
||||
components: {}, // UI state for each widget /components
|
||||
session: {
|
||||
isLoggingIn: false,
|
||||
isLoggingOut: false,
|
||||
isLoggued: false
|
||||
} // session specific information
|
||||
}
|
||||
110
src/store/reducers.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import * as actions from './actions'
|
||||
import { combineReducers } from 'redux'
|
||||
|
||||
import initialState from './initialStoreState'
|
||||
/* Reducers are synchronous and repeatable : it's not the place to put api call
|
||||
*/
|
||||
|
||||
function sessionReducer (currentSession = initialState.session, action) {
|
||||
switch (action.type) {
|
||||
case actions.SESSION_PATCH :
|
||||
let payload = action.payload
|
||||
// don't change loggued state from this action
|
||||
delete payload.isLoggingIn
|
||||
delete payload.isLoggingOut
|
||||
delete payload.isLoggued
|
||||
return Object.assign(
|
||||
{},
|
||||
currentSession,
|
||||
payload // login , password
|
||||
)
|
||||
case actions.SIGN_IN :
|
||||
// user tried to sign in
|
||||
return Object.assign(
|
||||
{},
|
||||
currentSession,
|
||||
action.payload, // login , password
|
||||
{isLoggingIn: true}
|
||||
)
|
||||
|
||||
case actions.SIGN_OUT :
|
||||
return Object.assign({}, currentSession, {isLoggingOut: true})
|
||||
|
||||
case actions.SIGNED_OUT:
|
||||
// when user is signed out : reset session
|
||||
return initialState.session
|
||||
|
||||
case actions.SIGNED_IN :
|
||||
// when user is signed in : session informations are received from server
|
||||
return Object.assign(
|
||||
{},
|
||||
currentSession,
|
||||
action.payload,
|
||||
{
|
||||
isLoggued: true,
|
||||
isLoggingIn: false,
|
||||
isLoggingOut: false,
|
||||
password: 'redacted , don t need it '
|
||||
}
|
||||
)
|
||||
|
||||
default :
|
||||
return currentSession // reducer HAVE TO return a state, even when they did nothing
|
||||
}
|
||||
}
|
||||
|
||||
function xoApiReducer (state = initialState.xoApi, action) {
|
||||
let payload = action.payload || {}
|
||||
switch (action.type){
|
||||
case actions.VM_EDIT:
|
||||
payload = Object.assign({},
|
||||
state[action.payload.id],
|
||||
payload,
|
||||
{isSaved: false}
|
||||
)
|
||||
return Object.assign(
|
||||
{},
|
||||
state,
|
||||
{
|
||||
[action.payload.id]: payload
|
||||
}
|
||||
)
|
||||
|
||||
case actions.VM_SAVE:
|
||||
console.log(' SAVE ',action.payload)
|
||||
return Object.assign(
|
||||
{},
|
||||
state,
|
||||
{
|
||||
[action.payload.id]: {isSaving:true}
|
||||
}
|
||||
)
|
||||
return state
|
||||
case actions.VM_SAVED:
|
||||
// put the VM at its real id if it's a creation
|
||||
payload = Object.assign({},
|
||||
state[action.payload.id],
|
||||
payload,
|
||||
{isSaved: true, isSaving: false}
|
||||
)
|
||||
console.log('VM_SAVED',payload)
|
||||
return Object.assign(
|
||||
{},
|
||||
state,
|
||||
{
|
||||
[action.payload.id]: payload
|
||||
}
|
||||
)
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
function componentsReducer (state = initialState.components, action) {
|
||||
return state
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
session: sessionReducer,
|
||||
components: componentsReducer,
|
||||
xoApi: xoApiReducer
|
||||
})
|
||||
3
src/xo-app/about/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import React from 'react'
|
||||
|
||||
export default () => <h1>About</h1>
|
||||
3
src/xo-app/home/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import React from 'react'
|
||||
|
||||
export default () => <h1>Home</h1>
|
||||
118
src/xo-app/index.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import { connect, Provider } from 'react-redux'
|
||||
import store from '../store'
|
||||
import LoginForm from './login/loginForm.js'
|
||||
import VMForm from './vm/form.js'
|
||||
import { Router, Route, Link } from 'react-router'
|
||||
// from https://github.com/rackt/react-router/blob/master/upgrade-guides/v2.0.0.md#no-default-history
|
||||
import { hashHistory } from 'react-router'
|
||||
|
||||
|
||||
const Dashboard = React.createClass({
|
||||
render() {
|
||||
return <div>Welcome to the app!</div>
|
||||
}
|
||||
})
|
||||
|
||||
const App = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<ul>
|
||||
<li><Link to="/vm/Lab1/create">createvm</Link></li>
|
||||
<li><Link to="/inbox">Inbox</Link></li>
|
||||
</ul>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
const Inbox = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h2>Inbox</h2>
|
||||
{this.props.children || "Welcome to your Inbox"}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const Message = React.createClass({
|
||||
render() {
|
||||
return <h3>Message {this.props.params.id}</h3>
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class XoAppUnconnected extends Component {
|
||||
render () {
|
||||
const {isLoggued, login, password, userId} = this.props
|
||||
return (
|
||||
<div>
|
||||
<h2> Xen Orchestra {login} {isLoggued ? 'connecté' : ' non connecté'}</h2>
|
||||
{!isLoggued &&
|
||||
<LoginForm />
|
||||
}
|
||||
{isLoggued &&
|
||||
<Router history={hashHistory}>
|
||||
<Route path="/" component={App}>
|
||||
<Route path="vm/:host/:vmId" component={ VMForm } host='lab1'/>
|
||||
<Route path="inbox" component={Inbox}>
|
||||
<Route path="messages/:id" component={Message} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Router>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
const XoApp = connect(state => state.session)(XoAppUnconnected)
|
||||
|
||||
/*
|
||||
* Provider allow the compontn directly bellow XoApp to have acces to the store
|
||||
* and to the dispatch method
|
||||
*/
|
||||
export default () =>
|
||||
<Provider store={ store }>
|
||||
<XoApp/>
|
||||
</Provider>
|
||||
/*
|
||||
|
||||
import About from './about'
|
||||
import Home from './home'
|
||||
import CreateVm from './create-vm'
|
||||
//import CreatVM from './create-vm'
|
||||
|
||||
class XoApp extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
render () {
|
||||
|
||||
}
|
||||
|
||||
_do (action) {
|
||||
return () => this.props.dispatch(action)
|
||||
}
|
||||
}
|
||||
|
||||
XoApp = connect(state => state)(XoApp)
|
||||
|
||||
export default () => <div>
|
||||
<h1>Xen Orchestra</h1>
|
||||
|
||||
<ReduxRouter>
|
||||
<Route path='/' component={ XoApp }>
|
||||
<IndexRoute component={ Home } />
|
||||
<Route path='/about' component={ About } />
|
||||
<Route path='/create-vm' component={ CreateVm } />
|
||||
</Route>
|
||||
</ReduxRouter>
|
||||
</div>*/
|
||||
61
src/xo-app/login/loginForm.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import {signIn, signedIn, patchSession } from '../../store/actions'
|
||||
|
||||
class LoginForm extends Component {
|
||||
|
||||
handleSigninIn (e) {
|
||||
const { actions } = this.props
|
||||
e.preventDefault()
|
||||
actions.signIn()
|
||||
}
|
||||
|
||||
updateLogin (event) {
|
||||
this.props.actions.patchSession({login: event.target.value})
|
||||
}
|
||||
|
||||
updatePassword (event) {
|
||||
this.props.actions.patchSession({password: event.target.value})
|
||||
}
|
||||
|
||||
render () {
|
||||
/*remeber : this.propos.session is from the connect call*/
|
||||
const { login, password, isLoggingIn } = this.props.session
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={(e) => this.handleSigninIn(e)}>
|
||||
<input type='text' name='login' value={login} onChange={(e) => this.updateLogin(e)/* autobinding is only in ES5*/}/>
|
||||
<input type='password' name='password' value={password} onChange={(e) => this.updatePassword(e)}/>
|
||||
{isLoggingIn &&
|
||||
<p>signing in , waiting for server</p>
|
||||
}
|
||||
{!isLoggingIn &&
|
||||
<input type='submit' value='Sign in'/>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/* Which part of the global app state this component can see ?
|
||||
* make it as small as possible to reduce the rerender */
|
||||
export default connect(
|
||||
(state) => {
|
||||
return {session: state.session}
|
||||
},
|
||||
/* Transmit action and actions creators
|
||||
* It can be usefull to transmit only a few selected actions.
|
||||
* Also bind them to dispatch , so the component can call action creator directly,
|
||||
* without having to manually wrap each
|
||||
*/
|
||||
(dispatch) => {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
signIn, patchSession, signedIn
|
||||
},
|
||||
dispatch) }
|
||||
}
|
||||
)(LoginForm)
|
||||
200
src/xo-app/vm/form.js
Normal file
@@ -0,0 +1,200 @@
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { VMEdit, VMSave } from '../../store/actions'
|
||||
|
||||
// should move to glbal style
|
||||
const fieldsetStyle = {
|
||||
'display': 'flex',
|
||||
'flexDirection': 'row'
|
||||
}
|
||||
|
||||
const inputContainerStyle = {
|
||||
'flex': 1
|
||||
}
|
||||
|
||||
const legendStyle = {
|
||||
'width': '150px'
|
||||
}
|
||||
|
||||
/* I don't use fieldset / legend here since they don't
|
||||
* really like being styled with flex
|
||||
*/
|
||||
|
||||
class VmForm extends Component {
|
||||
static propType = {
|
||||
routeParams: PropTypes.object,
|
||||
vm: PropTypes.object,
|
||||
actions: PropTypes.object
|
||||
};
|
||||
save (e){
|
||||
e.preventDefault()
|
||||
this.props.actions.VMSave(this.props.routeParams.vmId)
|
||||
}
|
||||
patch (field,value){
|
||||
this.props.actions.VMEdit({
|
||||
id:this.props.routeParams.vmId,
|
||||
[field]:value
|
||||
})
|
||||
}
|
||||
|
||||
addSr ( e) {
|
||||
e.preventDefault()
|
||||
const vm = this.props.vm || {}
|
||||
let srs = vm.srs || []
|
||||
srs.push({})
|
||||
this.props.actions.VMEdit({
|
||||
id:this.props.routeParams.vmId,
|
||||
srs:srs
|
||||
})
|
||||
}
|
||||
|
||||
editSr( index, field, value) {
|
||||
const vm = this.props.vm || {}
|
||||
let srs = vm.srs || []
|
||||
srs[index].field = value
|
||||
this.props.actions.VMEdit({
|
||||
id:this.props.routeParams.vmId,
|
||||
srs:srs
|
||||
})
|
||||
}
|
||||
removeSr (e,index) {
|
||||
e.preventDefault()
|
||||
const vm = this.props.vm || {}
|
||||
let srs = vm.srs || []
|
||||
srs.splice(index,1)
|
||||
this.props.actions.VMEdit({
|
||||
id:this.props.routeParams.vmId,
|
||||
srs:srs
|
||||
})
|
||||
}
|
||||
render () {
|
||||
const s = this.state
|
||||
const {name,templateId, isSaving, isSaved, desc, vcpus, ram , ramUnit, srs } = this.props.vm || {}
|
||||
const {host} = this.props.routeParams
|
||||
console.log('VM is now ',this.props.vm)
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => this.save(e)}>
|
||||
<h2>Create VM on {host}</h2>
|
||||
<div style={fieldsetStyle}>
|
||||
<div style={legendStyle}>
|
||||
Infos
|
||||
</div>
|
||||
<div style={inputContainerStyle}>
|
||||
<label htmlFor='vm-edit-name'>Name : </label>
|
||||
<input
|
||||
id ='vm-edit-name'
|
||||
type='text'
|
||||
value={name}
|
||||
onChange={(e) => this.patch('name', e.target.value)}/>
|
||||
<label htmlFor='vm-template-id'>Template : </label>
|
||||
<select
|
||||
id='vm-template-id'
|
||||
value={templateId}
|
||||
onChange={(e) => this.patch('templateId', e.target.value)}>
|
||||
<option></option>
|
||||
<option value='0'>Template0</option>
|
||||
<option value='1'>Template1</option>
|
||||
</select>
|
||||
<br/>
|
||||
<label htmlFor='Description'>Description</label>
|
||||
<input type='text'
|
||||
value={desc}
|
||||
placeholder='Optional description'
|
||||
onChange={(e) => this.patch('desc', e.target.value)}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={fieldsetStyle}>
|
||||
<div style={legendStyle}>
|
||||
Perf
|
||||
</div>
|
||||
<div style={inputContainerStyle}>
|
||||
<label htmlFor='vm-edit-vcpus'>VCPUs : </label>
|
||||
<input
|
||||
id ='vm-edit-vcpus'
|
||||
type='number'
|
||||
value={vcpus}
|
||||
onChange={(e) => this.patch('vcpus', e.target.value)}/>
|
||||
<label htmlFor='vm-template-id'>Ram : </label>
|
||||
<input type='number'
|
||||
value={ram}
|
||||
defaultValue='0'
|
||||
onChange={(e) => this.patch('ram', e.target.value)}/>
|
||||
<select
|
||||
id='vm-edit-ramUnit'
|
||||
value={ramUnit}
|
||||
onChange={(e) => this.patch('ramUnit', e.target.value)}>
|
||||
<option value='MB'>MB</option>
|
||||
<option value='GB'>GB</option>
|
||||
<option value='TB'>TB</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={fieldsetStyle}>
|
||||
<div style={legendStyle}>
|
||||
Disks
|
||||
</div>
|
||||
<div style={inputContainerStyle}>
|
||||
{(srs || []).map((sr, i)=>{
|
||||
return <div key={'srs'+i} /*should be sr.id*/>
|
||||
Name <input type='text' value={sr.name} onChange={(e) => this.editSr(i, 'name',e.target.value)}></input>
|
||||
desc <input type='text' value={sr.mac} placeholder='optionnal desc' onChange={(e) => this.editSr(i, 'desc',e.target.value)}></input>
|
||||
<button onClick={(e) => this.removeSr(e,i)}> X</button>
|
||||
</div>
|
||||
})}
|
||||
<button onClick={(e) => this.addSr(e)}>+ addInterface</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={fieldsetStyle}>
|
||||
<div style={legendStyle}>
|
||||
Summary
|
||||
</div>
|
||||
<div style={inputContainerStyle}>
|
||||
{vcpus} x cpus {ram} {ramUnit || 'MB'}
|
||||
</div>
|
||||
</div>
|
||||
{!isSaving && !isSaved &&
|
||||
<button type='submit'>Save</button>
|
||||
}
|
||||
{isSaving &&
|
||||
<p>Saving to server</p>
|
||||
}
|
||||
{!isSaving && isSaved &&
|
||||
<p>Not modified</p>
|
||||
}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Which part of the global app state this component can see ?
|
||||
* make it as small as possible to reduce the rerender */
|
||||
//ownprop is the prop given to the component
|
||||
function mapStateToProps(state, ownProps) {
|
||||
console.log(ownProps.routeParams.vmId);
|
||||
return {
|
||||
vm :state.xoApi[ownProps.routeParams.vmId]
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
/* Transmit action and actions creators
|
||||
* It can be usefull to transmit only a few selected actions.
|
||||
* Also bind them to dispatch , so the component can call action creator directly,
|
||||
* without having to manually wrap each
|
||||
*/
|
||||
(dispatch) => {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
VMEdit, VMSave
|
||||
},
|
||||
dispatch) }
|
||||
}
|
||||
)(VmForm)
|
||||