Compare commits
1607 Commits
v5.x.all.i
...
v5.2.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
563b4cb1ec | ||
|
|
45bad231cf | ||
|
|
d76bd2484b | ||
|
|
445b60bb63 | ||
|
|
3214e0e41e | ||
|
|
c61230e145 | ||
|
|
fac6a29226 | ||
|
|
7a8f414748 | ||
|
|
9f450d282e | ||
|
|
31787067e3 | ||
|
|
1a769b23e2 | ||
|
|
ae002abafc | ||
|
|
31a25d9c16 | ||
|
|
356295c361 | ||
|
|
d10681b6d1 | ||
|
|
0602410aa8 | ||
|
|
1112768adc | ||
|
|
86b599df89 | ||
|
|
88f7661172 | ||
|
|
29c96c0119 | ||
|
|
d8c6e54c68 | ||
|
|
df053eb016 | ||
|
|
d1715f7711 | ||
|
|
240282c72d | ||
|
|
9e8dd6ea21 | ||
|
|
32806a20c9 | ||
|
|
34dcfbbf49 | ||
|
|
91fec43866 | ||
|
|
aa2d196a79 | ||
|
|
180ca458ad | ||
|
|
aa881c60e7 | ||
|
|
5b6966042d | ||
|
|
dc859da0cd | ||
|
|
151eb6cbd6 | ||
|
|
16db591bbf | ||
|
|
05a55e5eb2 | ||
|
|
dcd84b2b8f | ||
|
|
4a89119f0a | ||
|
|
bc1c30a7bf | ||
|
|
33cffbf28b | ||
|
|
a18b68116c | ||
|
|
d5acf15bca | ||
|
|
84f970af68 | ||
|
|
969f636bb7 | ||
|
|
6939aee20a | ||
|
|
ab2a02a555 | ||
|
|
70038e0764 | ||
|
|
e730ef5e11 | ||
|
|
835ad5aaf1 | ||
|
|
ac645c8617 | ||
|
|
b801fdbab2 | ||
|
|
bf495953e2 | ||
|
|
45b165deec | ||
|
|
09169578e8 | ||
|
|
43b2366927 | ||
|
|
f015a69eec | ||
|
|
99568508dd | ||
|
|
e8515344dd | ||
|
|
edc873a570 | ||
|
|
1a03e96ab2 | ||
|
|
89e0bb4f0a | ||
|
|
7d0fd60908 | ||
|
|
6b20523df4 | ||
|
|
e9a612647e | ||
|
|
28404ef149 | ||
|
|
a5f8230def | ||
|
|
39171de5de | ||
|
|
5aa5a0acbc | ||
|
|
a4518e630a | ||
|
|
94975f5ea6 | ||
|
|
7e98838d96 | ||
|
|
e8c9c196ff | ||
|
|
db314a238f | ||
|
|
2c85a6d4ab | ||
|
|
b683e14e80 | ||
|
|
ba45095fa8 | ||
|
|
b8e5ffa9f7 | ||
|
|
b4bff9e032 | ||
|
|
0c461bc4e2 | ||
|
|
a33b2a5294 | ||
|
|
298e1c4471 | ||
|
|
1c70cdc10b | ||
|
|
160e4bb530 | ||
|
|
e69ba8dd96 | ||
|
|
e55f4c3eb2 | ||
|
|
1a3272b980 | ||
|
|
7bed5e025a | ||
|
|
29d22c0598 | ||
|
|
a38c7c34ac | ||
|
|
8d690ce4ff | ||
|
|
2569568a03 | ||
|
|
2c6ff6b5b8 | ||
|
|
1257f01027 | ||
|
|
fad6830863 | ||
|
|
66262bb20b | ||
|
|
4abb0754c7 | ||
|
|
78c53bf3ad | ||
|
|
810d666d84 | ||
|
|
67699f0bb6 | ||
|
|
46274948c0 | ||
|
|
28e3a842ef | ||
|
|
6d90f1d45d | ||
|
|
09642c347d | ||
|
|
2d0e06f785 | ||
|
|
a5bc8497cf | ||
|
|
4bcb65c518 | ||
|
|
25361fa7eb | ||
|
|
889a265000 | ||
|
|
3122f6dcd5 | ||
|
|
16aa2e8085 | ||
|
|
074d51a670 | ||
|
|
2122a79132 | ||
|
|
26dbc585ba | ||
|
|
4b3cfbd424 | ||
|
|
035191a2cc | ||
|
|
06a40180a1 | ||
|
|
aaf4c5dff7 | ||
|
|
0c83bc2b0e | ||
|
|
2d412fd8db | ||
|
|
443e2bec25 | ||
|
|
d5e1323d82 | ||
|
|
7f0b77cc89 | ||
|
|
0169cff66c | ||
|
|
0fd1424a41 | ||
|
|
6280d56f32 | ||
|
|
9f2a77872f | ||
|
|
b571c18e9a | ||
|
|
49863d6e4d | ||
|
|
48cc7bb647 | ||
|
|
442d42d8dc | ||
|
|
9501ebacfc | ||
|
|
23f9fa46f8 | ||
|
|
1bd0f37fd4 | ||
|
|
ed74ded923 | ||
|
|
b732410b74 | ||
|
|
a51f2b7fcf | ||
|
|
fe12bbb60d | ||
|
|
8882df7939 | ||
|
|
185a554cd9 | ||
|
|
230e0dc2a5 | ||
|
|
f5b69fdfdc | ||
|
|
01dc0d8f1e | ||
|
|
8035886a3c | ||
|
|
0ab5f4b13f | ||
|
|
a1bc98def8 | ||
|
|
868cf6140b | ||
|
|
4b3473f480 | ||
|
|
7bc782cc62 | ||
|
|
e625a53e4a | ||
|
|
b31185d96d | ||
|
|
09d75e972f | ||
|
|
f33568951b | ||
|
|
8d8c442be5 | ||
|
|
f890b8ea7a | ||
|
|
1b80b3929c | ||
|
|
4f946293f6 | ||
|
|
36788cde2b | ||
|
|
1547c99e5a | ||
|
|
5c9606dad8 | ||
|
|
fdcb1dccf5 | ||
|
|
12812b8c23 | ||
|
|
0098497255 | ||
|
|
6562d2de7f | ||
|
|
1f0e88cdb0 | ||
|
|
197da91ef3 | ||
|
|
cbd59789e2 | ||
|
|
190ecf3d74 | ||
|
|
15b8f6bca2 | ||
|
|
5b406d731b | ||
|
|
4be9e67ac4 | ||
|
|
d047421685 | ||
|
|
f6f415a421 | ||
|
|
edfaaebac0 | ||
|
|
67df22a1bf | ||
|
|
7dc59a00f6 | ||
|
|
6214fe4c2e | ||
|
|
21610c3e0a | ||
|
|
87550b0189 | ||
|
|
b7c42d0a08 | ||
|
|
c15ad299ac | ||
|
|
48c56cd602 | ||
|
|
7957f621ef | ||
|
|
38ddbfdc9c | ||
|
|
3d2aae81da | ||
|
|
2227b9d061 | ||
|
|
12aab5fa8c | ||
|
|
7323e6e117 | ||
|
|
6f36869609 | ||
|
|
4a12419162 | ||
|
|
bf91938aa6 | ||
|
|
bd70bd2b45 | ||
|
|
bb26c8e449 | ||
|
|
93c7a01e62 | ||
|
|
9c2359e8ee | ||
|
|
5b9000012e | ||
|
|
bf00b4e8e3 | ||
|
|
ee7787f4ae | ||
|
|
0b88e743c9 | ||
|
|
f07a947580 | ||
|
|
0b8a9eedbc | ||
|
|
8d24e596ac | ||
|
|
c2378a44cd | ||
|
|
023f7fdef1 | ||
|
|
5d7a64bc28 | ||
|
|
8661957a97 | ||
|
|
7a15d265b7 | ||
|
|
2736881975 | ||
|
|
44a85f4e0c | ||
|
|
52a6e42e7e | ||
|
|
3dbe058d4e | ||
|
|
620139efc1 | ||
|
|
71464ac2e3 | ||
|
|
4a65489d39 | ||
|
|
65d7eac590 | ||
|
|
02bbc01dc4 | ||
|
|
3066237c86 | ||
|
|
53f3c0bef1 | ||
|
|
823c91b457 | ||
|
|
3bd7e20411 | ||
|
|
24d4610b04 | ||
|
|
b16097767a | ||
|
|
2ff74ffd39 | ||
|
|
f0bb464136 | ||
|
|
4767830386 | ||
|
|
ce23d4f164 | ||
|
|
c1380d1256 | ||
|
|
ed9a848858 | ||
|
|
5e4e15fc12 | ||
|
|
0dea952a2a | ||
|
|
a1818dd525 | ||
|
|
659e336f66 | ||
|
|
058f7ecd9f | ||
|
|
831d9cb49f | ||
|
|
a5d059b0b1 | ||
|
|
4c3b959869 | ||
|
|
d81a169a39 | ||
|
|
0d47332526 | ||
|
|
539d136936 | ||
|
|
4c28b5775d | ||
|
|
fe6f351f84 | ||
|
|
5dbeccf92f | ||
|
|
56bba1d84b | ||
|
|
af05d362b4 | ||
|
|
268ccf9a36 | ||
|
|
e77d4fafaa | ||
|
|
b88b99e342 | ||
|
|
f862d0df5b | ||
|
|
dac954155c | ||
|
|
cf9deceb15 | ||
|
|
72aed98088 | ||
|
|
ec92eddde8 | ||
|
|
e30b5ab6c3 | ||
|
|
0a5d26b001 | ||
|
|
7e4b881041 | ||
|
|
27a6af414f | ||
|
|
ba6204f811 | ||
|
|
d17b1050ad | ||
|
|
b70bc86f71 | ||
|
|
42b08633e9 | ||
|
|
bc898e1afd | ||
|
|
48d5f34ae6 | ||
|
|
67b8b15cd8 | ||
|
|
09d80afa69 | ||
|
|
c0d95304f6 | ||
|
|
5a0d67a9f6 | ||
|
|
08305b4b93 | ||
|
|
04d5612946 | ||
|
|
3dcb6f1f61 | ||
|
|
4e7684e38b | ||
|
|
a692b7571f | ||
|
|
a098618efa | ||
|
|
71381e75f1 | ||
|
|
05b345db4a | ||
|
|
f85f6eab9e | ||
|
|
b6dc8b507d | ||
|
|
831308ee05 | ||
|
|
eb5bcb759f | ||
|
|
8286570811 | ||
|
|
10b511f0ed | ||
|
|
751e335bc0 | ||
|
|
cb107521f2 | ||
|
|
e56af57b74 | ||
|
|
a2a1cbab6e | ||
|
|
306a021a8d | ||
|
|
d8c414af2f | ||
|
|
ec4c76b2e0 | ||
|
|
e23b8a6891 | ||
|
|
34006bcbf6 | ||
|
|
ed9aeabf6a | ||
|
|
799fc5089f | ||
|
|
683d510aa6 | ||
|
|
ebd7e58f61 | ||
|
|
9a498b54ac | ||
|
|
2687f45e6e | ||
|
|
f79a17fcec | ||
|
|
8fd377d1e2 | ||
|
|
fda06fbd29 | ||
|
|
cee4378e6d | ||
|
|
ab6d342886 | ||
|
|
9954c08993 | ||
|
|
3ae80aeab3 | ||
|
|
2a3534f659 | ||
|
|
fc39de0d5a | ||
|
|
64e4b79d41 | ||
|
|
53887da3da | ||
|
|
7c60d68f56 | ||
|
|
2ac1b991b1 | ||
|
|
8257714cdb | ||
|
|
1b8bacbf5a | ||
|
|
1d5b84389d | ||
|
|
f7dcf52977 | ||
|
|
e26dd5147a | ||
|
|
bb8f96c2e2 | ||
|
|
95d4cc9055 | ||
|
|
cb84a85f8b | ||
|
|
0a8aa2ecf5 | ||
|
|
5941321e84 | ||
|
|
8cf62280f4 | ||
|
|
4cea142b57 | ||
|
|
64d9245bc4 | ||
|
|
2d78c0c4c3 | ||
|
|
aa585e2d25 | ||
|
|
325ab17dcc | ||
|
|
443ea44bcd | ||
|
|
07958d8efa | ||
|
|
f19affe599 | ||
|
|
f7b7c27b6c | ||
|
|
c7af5b384c | ||
|
|
436a9dfc14 | ||
|
|
1d6d8ccb28 | ||
|
|
7d0862ecfd | ||
|
|
7de059919b | ||
|
|
dfd1fb86cb | ||
|
|
847a92433f | ||
|
|
53af4df47b | ||
|
|
09db7c999e | ||
|
|
1b4c958aba | ||
|
|
9368d5df01 | ||
|
|
f3b5026190 | ||
|
|
19dcd81639 | ||
|
|
d38c171151 | ||
|
|
af3049925f | ||
|
|
a79825d18c | ||
|
|
c4b456b470 | ||
|
|
ccdf28767a | ||
|
|
2561f7d793 | ||
|
|
57bd8c1a49 | ||
|
|
8387e4ae04 | ||
|
|
5c02935017 | ||
|
|
726ffb9b1b | ||
|
|
5dcc3f4076 | ||
|
|
4639d7872f | ||
|
|
71cb6af8c4 | ||
|
|
52060301bd | ||
|
|
dfa3e6d8e4 | ||
|
|
f38f3fe5c9 | ||
|
|
24bf031270 | ||
|
|
eeadd72e1f | ||
|
|
e4139bab04 | ||
|
|
7c7205849b | ||
|
|
03b2b13f14 | ||
|
|
8caf9f7fde | ||
|
|
5b8a5ac6b6 | ||
|
|
4429bed1cf | ||
|
|
b9beda3484 | ||
|
|
354c9bc927 | ||
|
|
a2d88f7fbf | ||
|
|
83cad000e7 | ||
|
|
1b78791aa9 | ||
|
|
2b05fbf6a0 | ||
|
|
7b677cddaf | ||
|
|
18a8fcaa70 | ||
|
|
dd1bc757d5 | ||
|
|
27ca0fdfcc | ||
|
|
578de05a40 | ||
|
|
cd3e1d6bd4 | ||
|
|
de160bb51b | ||
|
|
14417e14c0 | ||
|
|
f2d8b4e444 | ||
|
|
b7c41fee28 | ||
|
|
4f0678d6a2 | ||
|
|
880c624935 | ||
|
|
0fa0902262 | ||
|
|
a2ab3ccaee | ||
|
|
77a0d1c2ff | ||
|
|
7fdb022819 | ||
|
|
878a630b69 | ||
|
|
fbcfc69983 | ||
|
|
a1bd327524 | ||
|
|
e62829debd | ||
|
|
d9d669964f | ||
|
|
ced17b632a | ||
|
|
0aada62a5a | ||
|
|
2fece7a8fe | ||
|
|
6680373c76 | ||
|
|
68ae43fd72 | ||
|
|
6b6f452d06 | ||
|
|
7153ff17e8 | ||
|
|
0fde5a1b3d | ||
|
|
b17fbdd19b | ||
|
|
61ae522486 | ||
|
|
bd414ae9f2 | ||
|
|
7579db5876 | ||
|
|
994ce8dab2 | ||
|
|
e8a84dce7d | ||
|
|
fdca9eda90 | ||
|
|
e007009a00 | ||
|
|
1d5cc209dd | ||
|
|
09b18e1563 | ||
|
|
363db0edea | ||
|
|
e500240a35 | ||
|
|
6694977b87 | ||
|
|
b173dc1f28 | ||
|
|
aa91f5649f | ||
|
|
74efd563ab | ||
|
|
b0b389fb4d | ||
|
|
e2d9131a07 | ||
|
|
3b1c8216b9 | ||
|
|
7f259a43cf | ||
|
|
874a504df3 | ||
|
|
6a4c6318e3 | ||
|
|
09bf2b87dc | ||
|
|
9c5c9838ae | ||
|
|
3924033d9a | ||
|
|
d81e45e456 | ||
|
|
631a762b56 | ||
|
|
a9cf79942f | ||
|
|
bd31476933 | ||
|
|
9b9e4c2ffa | ||
|
|
ec93daac7e | ||
|
|
3431b2dfb1 | ||
|
|
4270abaf1c | ||
|
|
0bd288afbd | ||
|
|
b72d5d50a1 | ||
|
|
d51889c233 | ||
|
|
332b093ee9 | ||
|
|
8be332208f | ||
|
|
85d1188628 | ||
|
|
56896996c3 | ||
|
|
896374e069 | ||
|
|
ac36505fb2 | ||
|
|
d36df1a8ae | ||
|
|
edd939c069 | ||
|
|
f80225ba54 | ||
|
|
3ccd87b369 | ||
|
|
59d3dd9255 | ||
|
|
392f08059d | ||
|
|
0d5c9a2bba | ||
|
|
f27de8015b | ||
|
|
3b7bdee814 | ||
|
|
397ed9d581 | ||
|
|
a098880669 | ||
|
|
047d4cb650 | ||
|
|
736904c579 | ||
|
|
e883c668b5 | ||
|
|
14181aa8a7 | ||
|
|
0b1ba99afa | ||
|
|
88a6215939 | ||
|
|
d5ebd33038 | ||
|
|
b2ac214c0f | ||
|
|
3dee41a511 | ||
|
|
934818c07d | ||
|
|
4dc614a58e | ||
|
|
35ea095b75 | ||
|
|
ae1a4c73b3 | ||
|
|
b58dbe89be | ||
|
|
1b4551b622 | ||
|
|
72a8f819d3 | ||
|
|
df8e16379c | ||
|
|
7dbbc7e25c | ||
|
|
1eaae70adb | ||
|
|
d4a61782c4 | ||
|
|
0e39c6f895 | ||
|
|
fcc3ede485 | ||
|
|
f001e7e713 | ||
|
|
3fb8fae821 | ||
|
|
349f3185c5 | ||
|
|
b457b8409f | ||
|
|
c61e5e1ac8 | ||
|
|
9e60f9d9fd | ||
|
|
a95d40078f | ||
|
|
515798bd9f | ||
|
|
20b28135a3 | ||
|
|
33d2b8bbeb | ||
|
|
7c2059af2b | ||
|
|
1e0f57bd1a | ||
|
|
9484f1dbe6 | ||
|
|
658766c9e4 | ||
|
|
9e47d9acf1 | ||
|
|
5a1247c021 | ||
|
|
a8b7972f3c | ||
|
|
836c2127f7 | ||
|
|
6cef200aed | ||
|
|
c9c80b1d62 | ||
|
|
04328bc2d1 | ||
|
|
d909d0eeeb | ||
|
|
80f5e913ec | ||
|
|
57eca31a9c | ||
|
|
c645fc7ad1 | ||
|
|
78b524b2e8 | ||
|
|
1ff1b6931b | ||
|
|
29ac883616 | ||
|
|
467a147603 | ||
|
|
7b49b6304c | ||
|
|
25991027b9 | ||
|
|
fce83dfa66 | ||
|
|
8603d5d468 | ||
|
|
bd17f85140 | ||
|
|
037dddb945 | ||
|
|
dd2151e611 | ||
|
|
a1e0cdadd6 | ||
|
|
8d36efa66c | ||
|
|
b9a12a6dcc | ||
|
|
08e4fe9990 | ||
|
|
2333fec181 | ||
|
|
05676a78e3 | ||
|
|
02aaae240c | ||
|
|
158924fe3c | ||
|
|
0341b926b9 | ||
|
|
69d1f93ea4 | ||
|
|
423fb56ae0 | ||
|
|
c5fc8d437f | ||
|
|
0811addf9c | ||
|
|
e6f8108dc0 | ||
|
|
4aa4a8c75d | ||
|
|
bbe0467d16 | ||
|
|
88ca69138b | ||
|
|
6a0d9c8805 | ||
|
|
1a57f9f134 | ||
|
|
109aedd3ae | ||
|
|
bd9f9344e5 | ||
|
|
5190873e99 | ||
|
|
c5fe7eb0dd | ||
|
|
fef1b14d69 | ||
|
|
472fc02533 | ||
|
|
ed29524cf3 | ||
|
|
69f35436c2 | ||
|
|
a0ca1cddb5 | ||
|
|
be4ffd8308 | ||
|
|
8e246f08ee | ||
|
|
73eda65300 | ||
|
|
be4df02844 | ||
|
|
7de461319f | ||
|
|
970fc16aab | ||
|
|
5db2c5804d | ||
|
|
6c2924a08a | ||
|
|
32511fe6a0 | ||
|
|
94d5b0f083 | ||
|
|
0e957b9566 | ||
|
|
ec93f21f0a | ||
|
|
bbc4f3beb4 | ||
|
|
c271a25a51 | ||
|
|
c986bf0c46 | ||
|
|
6f994b75e5 | ||
|
|
a227039260 | ||
|
|
ee38c07a3f | ||
|
|
9678ebd71e | ||
|
|
82ce0d3461 | ||
|
|
8315c79ef7 | ||
|
|
69cb6d30b5 | ||
|
|
f4beef514e | ||
|
|
f002677134 | ||
|
|
6270d2d3af | ||
|
|
83625e4ba7 | ||
|
|
d039112b5b | ||
|
|
d8481af288 | ||
|
|
ea902c1073 | ||
|
|
db62c18a39 | ||
|
|
d004e2f759 | ||
|
|
1f7e457c64 | ||
|
|
4eae9398d8 | ||
|
|
4766121570 | ||
|
|
3180641e33 | ||
|
|
9273002905 | ||
|
|
3fc9c5ec90 | ||
|
|
3266cea1d6 | ||
|
|
97839c06dc | ||
|
|
304f290e42 | ||
|
|
52a241f300 | ||
|
|
1c1ea0dcc4 | ||
|
|
d998b384e8 | ||
|
|
9184afa6de | ||
|
|
3e1b4d724f | ||
|
|
5b6f50b25b | ||
|
|
b757025359 | ||
|
|
52e97edbd5 | ||
|
|
def88db128 | ||
|
|
d04702e5d4 | ||
|
|
f6407771b5 | ||
|
|
f6a6e125b6 | ||
|
|
2303b8a89f | ||
|
|
93f286b6ac | ||
|
|
75e5f931eb | ||
|
|
b215e89572 | ||
|
|
07a7e8cf0a | ||
|
|
52000edd7d | ||
|
|
3e4c07c86f | ||
|
|
92ce69c603 | ||
|
|
a338e0a3f1 | ||
|
|
143e09b65f | ||
|
|
e5cc5abdc9 | ||
|
|
dfc96ebb99 | ||
|
|
9397d0121d | ||
|
|
d8a1f3c73a | ||
|
|
a07cb425a4 | ||
|
|
a89b33dfdf | ||
|
|
486d33448b | ||
|
|
2299d397cb | ||
|
|
0173c4709f | ||
|
|
42fdf8b61f | ||
|
|
0253723652 | ||
|
|
5ca51d3510 | ||
|
|
466dc0127d | ||
|
|
32f610485c | ||
|
|
429e1b54ee | ||
|
|
268c037487 | ||
|
|
c146f3105e | ||
|
|
81e0c04722 | ||
|
|
5d156695d2 | ||
|
|
f71438347c | ||
|
|
a3081d607f | ||
|
|
ca81f445b9 | ||
|
|
1c22ce6d76 | ||
|
|
a0d482ba88 | ||
|
|
0c050cc053 | ||
|
|
9645d624f2 | ||
|
|
f29cb94d9f | ||
|
|
e239206626 | ||
|
|
35d1065eaf | ||
|
|
8384d6f9d7 | ||
|
|
5b3282ba51 | ||
|
|
bd1043f034 | ||
|
|
c847dcec15 | ||
|
|
f66994f0b5 | ||
|
|
eba27f1823 | ||
|
|
ad1bbb2a00 | ||
|
|
42506ab37d | ||
|
|
6bae33826d | ||
|
|
914c2b89c5 | ||
|
|
e79926cf29 | ||
|
|
1f15d2c736 | ||
|
|
fadd27fd23 | ||
|
|
d5aeb8db55 | ||
|
|
d5dbdd9986 | ||
|
|
352c977dc7 | ||
|
|
bf008eba99 | ||
|
|
76b7777fff | ||
|
|
9292d990da | ||
|
|
87fe715823 | ||
|
|
25e32e0600 | ||
|
|
41c901a05c | ||
|
|
fdaba2faf4 | ||
|
|
0c73ad4f46 | ||
|
|
36c44bc3d4 | ||
|
|
d612598bd0 | ||
|
|
2d75b6086f | ||
|
|
3345674604 | ||
|
|
1eeaeeeca5 | ||
|
|
b0bea8b3ba | ||
|
|
0e3e5edd17 | ||
|
|
ec1287a2f4 | ||
|
|
9d2c857c59 | ||
|
|
077f4f201c | ||
|
|
9a2154a2ce | ||
|
|
f4c111c1c2 | ||
|
|
9483a06e8a | ||
|
|
df2a90dc1d | ||
|
|
246c190ccd | ||
|
|
4640817a14 | ||
|
|
9c7690d39b | ||
|
|
160805af05 | ||
|
|
39e85730f0 | ||
|
|
da692e1a92 | ||
|
|
23bc60f1ac | ||
|
|
ce0f759509 | ||
|
|
1793e5943a | ||
|
|
201b5db155 | ||
|
|
c588ac6777 | ||
|
|
28c01fd4e1 | ||
|
|
0715e7a31f | ||
|
|
b497c38e34 | ||
|
|
8a08dce405 | ||
|
|
a620c348bf | ||
|
|
fe750b7270 | ||
|
|
4aa9d56dfc | ||
|
|
4b2ebf2a3a | ||
|
|
6290446ea5 | ||
|
|
42f5d06960 | ||
|
|
85e8006137 | ||
|
|
7c29d4c644 | ||
|
|
7378bc852d | ||
|
|
67ed137cfa | ||
|
|
02e08e54a2 | ||
|
|
6b2dd24334 | ||
|
|
131d5becad | ||
|
|
157e0a83b1 | ||
|
|
d0d3abce3e | ||
|
|
b965c41a45 | ||
|
|
42f824e034 | ||
|
|
2768d9d49d | ||
|
|
c9a86dcae3 | ||
|
|
e1d307ea2c | ||
|
|
98ece12ae8 | ||
|
|
4cf3db7c2a | ||
|
|
7c2f79d980 | ||
|
|
fe064f8b6a | ||
|
|
2bad2f6b80 | ||
|
|
2ba9c5193f | ||
|
|
331695c10a | ||
|
|
f299193f05 | ||
|
|
de8130abc2 | ||
|
|
1cbde7f2e1 | ||
|
|
d1c796d9a7 | ||
|
|
af43061353 | ||
|
|
1b2ca8e69e | ||
|
|
a9e6679b08 | ||
|
|
9408760122 | ||
|
|
c25e804d61 | ||
|
|
b18b2262eb | ||
|
|
570440dc7d | ||
|
|
aef660fb2f | ||
|
|
17671c7282 | ||
|
|
8216ab44b4 | ||
|
|
5902d43a94 | ||
|
|
a6eb04d3f9 | ||
|
|
a71780e860 | ||
|
|
c2d815ef66 | ||
|
|
bf4679aa9b | ||
|
|
4dd74bbb16 | ||
|
|
10530146ca | ||
|
|
75d49da3d4 | ||
|
|
af026b0c52 | ||
|
|
bcd4f70d0e | ||
|
|
54cc31d1a0 | ||
|
|
3f0553861a | ||
|
|
e1b3c51d2c | ||
|
|
58a0e3fad6 | ||
|
|
6bb235650a | ||
|
|
0bf0bc4c33 | ||
|
|
3085749e92 | ||
|
|
de3abbf6b8 | ||
|
|
925469689f | ||
|
|
631e58a585 | ||
|
|
63571d06bf | ||
|
|
1979758fab | ||
|
|
6ef5a23000 | ||
|
|
ae4aa23d27 | ||
|
|
710d1f13cd | ||
|
|
57a4d366d7 | ||
|
|
8060c66c08 | ||
|
|
d5a58fbec2 | ||
|
|
059256de3e | ||
|
|
2c52d4c867 | ||
|
|
d9bfde2e47 | ||
|
|
757acd8d92 | ||
|
|
3e92252e2e | ||
|
|
ce7aeb1a27 | ||
|
|
236d2ad39a | ||
|
|
63b37714b1 | ||
|
|
dc81fd0622 | ||
|
|
ccec2bf7ee | ||
|
|
c93b93331e | ||
|
|
1f194f1680 | ||
|
|
2251123c1d | ||
|
|
1bfe1c3370 | ||
|
|
0044eeb6d1 | ||
|
|
669302d46b | ||
|
|
fce2f44197 | ||
|
|
64db1df248 | ||
|
|
9109d55019 | ||
|
|
412e13ccd5 | ||
|
|
0c7e0528b6 | ||
|
|
ccb22a2f40 | ||
|
|
2f3e463aca | ||
|
|
c548e08aea | ||
|
|
150e0171f0 | ||
|
|
bfcaca7bc0 | ||
|
|
fcb0482193 | ||
|
|
714ea7c236 | ||
|
|
cc30799f0d | ||
|
|
df71259a10 | ||
|
|
9a0ae5d4b9 | ||
|
|
eea4648ada | ||
|
|
2883398c2a | ||
|
|
84f6e14b89 | ||
|
|
30fb9ed65a | ||
|
|
a809f2d1f2 | ||
|
|
6b5a19983d | ||
|
|
2471f447b3 | ||
|
|
413e944d7a | ||
|
|
3b952819d6 | ||
|
|
2445c10c1c | ||
|
|
7e26593d04 | ||
|
|
73595c683b | ||
|
|
570f56a4cc | ||
|
|
f9e940871e | ||
|
|
c2b724a54a | ||
|
|
f52db472ed | ||
|
|
324fe98a5b | ||
|
|
2fd9833580 | ||
|
|
0ab4827d6f | ||
|
|
f0bd7d7eee | ||
|
|
8cd1209602 | ||
|
|
e2781adc81 | ||
|
|
b91ac2fe89 | ||
|
|
683a7a1851 | ||
|
|
8b05aa7b59 | ||
|
|
883f839bfd | ||
|
|
90dc00ac6b | ||
|
|
9c1cecbb7d | ||
|
|
1fce11bfba | ||
|
|
fba3ebdf49 | ||
|
|
e9585e08a4 | ||
|
|
24a89985fb | ||
|
|
33026e8281 | ||
|
|
50d9b832a9 | ||
|
|
1df82c3380 | ||
|
|
0a8db4ebbf | ||
|
|
928b19aef4 | ||
|
|
a7ec98cef6 | ||
|
|
67326a1859 | ||
|
|
631a8a5edf | ||
|
|
7e5e463ef2 | ||
|
|
add65e41da | ||
|
|
351b4571cd | ||
|
|
793258a91f | ||
|
|
ed3e1933c3 | ||
|
|
2f63b26458 | ||
|
|
f1d14da3dd | ||
|
|
4a3d90bdf3 | ||
|
|
f8f24fbc37 | ||
|
|
8f6c53e111 | ||
|
|
c23f55b1d4 | ||
|
|
88f94f5d6f | ||
|
|
3e99a179b7 | ||
|
|
c7271f94a5 | ||
|
|
5c0ced942c | ||
|
|
d9b07e76f9 | ||
|
|
c75793df20 | ||
|
|
1bee5121f0 | ||
|
|
932e7eb374 | ||
|
|
abdbcfe42b | ||
|
|
a62e888732 | ||
|
|
16b982b953 | ||
|
|
e92b87095b | ||
|
|
e5f1aa689b | ||
|
|
f39a05cd8d | ||
|
|
c5e22b785a | ||
|
|
11f93a125c | ||
|
|
38a9cb002d | ||
|
|
cf1a38a004 | ||
|
|
d6e823d19d | ||
|
|
763a23d9d0 | ||
|
|
f266577f2f | ||
|
|
1bb5e73668 | ||
|
|
b07bc755f6 | ||
|
|
db4b39c54b | ||
|
|
ffd95261c3 | ||
|
|
82f38040c1 | ||
|
|
7bb4f9f8e3 | ||
|
|
c2345df275 | ||
|
|
b0c341da3f | ||
|
|
b1ccc16da7 | ||
|
|
16856a5911 | ||
|
|
d4ee364349 | ||
|
|
db62ca7b4b | ||
|
|
d0b99b854d | ||
|
|
b3b13b3e01 | ||
|
|
5cb738b82b | ||
|
|
cfe3b15cbe | ||
|
|
a1bde80925 | ||
|
|
4c958dd584 | ||
|
|
a05d4d3d18 | ||
|
|
503b6dc914 | ||
|
|
141cbcd1c0 | ||
|
|
e340d2d3f5 | ||
|
|
436d5a3a66 | ||
|
|
3e04fd4790 | ||
|
|
3f6d149f9d | ||
|
|
977fc7832a | ||
|
|
2e2f0e2e3d | ||
|
|
5628beee72 | ||
|
|
a88fea560b | ||
|
|
5832345b96 | ||
|
|
d3d2daa12f | ||
|
|
f6f90982f4 | ||
|
|
98323b08f0 | ||
|
|
8f3112a5e2 | ||
|
|
3408bd41ad | ||
|
|
68e12e86c1 | ||
|
|
890e0b4906 | ||
|
|
3aca7c7ae5 | ||
|
|
e8077ddbc5 | ||
|
|
6e04907357 | ||
|
|
3d8c9a99fe | ||
|
|
730768705b | ||
|
|
13f75a37ab | ||
|
|
f74c69ea6f | ||
|
|
fe7be0f518 | ||
|
|
2f0e656c45 | ||
|
|
de489b799b | ||
|
|
6250ef49b6 | ||
|
|
afdab8dcde | ||
|
|
8e1d39f37f | ||
|
|
2a8c346a65 | ||
|
|
71e431e744 | ||
|
|
265cb75d70 | ||
|
|
4d0470838a | ||
|
|
d4ed3aeac0 | ||
|
|
89fa89fe98 | ||
|
|
2d663f0ac5 | ||
|
|
bc9b3f1c5c | ||
|
|
2f0a46a46d | ||
|
|
9559604d1e | ||
|
|
b779ab9bc5 | ||
|
|
481943051c | ||
|
|
cb49b7a906 | ||
|
|
090e4b3117 | ||
|
|
9a40d5f784 | ||
|
|
1485637c6d | ||
|
|
b0ec8e26e8 | ||
|
|
e4c12e08cb | ||
|
|
a1675745b5 | ||
|
|
40beb5b104 | ||
|
|
e49e2f51c2 | ||
|
|
99669f2678 | ||
|
|
9aa88b9dad | ||
|
|
10de29795a | ||
|
|
c4767e74f4 | ||
|
|
19f8666e1e | ||
|
|
2e2bbdf0d7 | ||
|
|
973bee0ffa | ||
|
|
9470cdf774 | ||
|
|
ab198ea60b | ||
|
|
c78f4bb6d2 | ||
|
|
ff054ca47f | ||
|
|
30cc804022 | ||
|
|
890d733bf8 | ||
|
|
a554a9c4a1 | ||
|
|
9ef212937d | ||
|
|
82d9c53f3e | ||
|
|
d9d91c4953 | ||
|
|
dd5f5282e0 | ||
|
|
cc116defc6 | ||
|
|
90c755e120 | ||
|
|
85d1c80581 | ||
|
|
5bb04d3857 | ||
|
|
dde8404242 | ||
|
|
7940bd2dcc | ||
|
|
da48d117a0 | ||
|
|
ffc74967fc | ||
|
|
c7388d5836 | ||
|
|
ee4b8fc66f | ||
|
|
c73f22ca45 | ||
|
|
bafa053fd1 | ||
|
|
a00406d2b3 | ||
|
|
df91e17dc6 | ||
|
|
7c6eeababc | ||
|
|
911a5067f9 | ||
|
|
bfa0fe9c51 | ||
|
|
7dafe31d51 | ||
|
|
1271ecedb4 | ||
|
|
80be18068f | ||
|
|
ecbf2c0958 | ||
|
|
cfb84b677f | ||
|
|
dfbd7a5d76 | ||
|
|
8e157f8ff7 | ||
|
|
c75580e852 | ||
|
|
e21934fd55 | ||
|
|
844f1609c8 | ||
|
|
6f7de28672 | ||
|
|
247212d768 | ||
|
|
6592b880ea | ||
|
|
e27a4bf119 | ||
|
|
dce5a83093 | ||
|
|
78c70f35a1 | ||
|
|
f4b84a0902 | ||
|
|
b16237b514 | ||
|
|
b0077539fa | ||
|
|
63602216eb | ||
|
|
2f93935009 | ||
|
|
c0a0b653c1 | ||
|
|
24d6354467 | ||
|
|
54a07469fd | ||
|
|
12f37bead1 | ||
|
|
8d76dc2511 | ||
|
|
8ef7d6defc | ||
|
|
2f2cbbe3f0 | ||
|
|
2662ac719b | ||
|
|
1b0fe6e847 | ||
|
|
9fa9e26324 | ||
|
|
cb434df099 | ||
|
|
e3cb3002fe | ||
|
|
8b2a09b522 | ||
|
|
03265d2545 | ||
|
|
9b25e07b5e | ||
|
|
60a21ca58d | ||
|
|
05172492ef | ||
|
|
a9c089a994 | ||
|
|
988d018c8e | ||
|
|
853c611fde | ||
|
|
0886f5335f | ||
|
|
024d481a4d | ||
|
|
b80de1af95 | ||
|
|
da44268f0d | ||
|
|
b921a3ed8e | ||
|
|
d2c9c824f9 | ||
|
|
21c255051c | ||
|
|
dccf09c5dd | ||
|
|
0772a17e4c | ||
|
|
644ee782d2 | ||
|
|
ea180cc415 | ||
|
|
68e3c4e6ed | ||
|
|
47b1c2d680 | ||
|
|
2e513043b7 | ||
|
|
ea942635f7 | ||
|
|
8a5f5cc673 | ||
|
|
6cc673035d | ||
|
|
02d717e5a8 | ||
|
|
77d83b06bd | ||
|
|
e3d8eabb05 | ||
|
|
0596b0106f | ||
|
|
765bbd90fc | ||
|
|
3799902a8a | ||
|
|
ca1dbb4556 | ||
|
|
df551d457c | ||
|
|
278d518d8f | ||
|
|
b06fa191f7 | ||
|
|
13d73f6f27 | ||
|
|
d59af117c0 | ||
|
|
69efd85ad6 | ||
|
|
ead51787a8 | ||
|
|
4bf0fee20d | ||
|
|
209693ee3f | ||
|
|
796d4f5b08 | ||
|
|
96914c9901 | ||
|
|
9e6073bf56 | ||
|
|
f8ad58159c | ||
|
|
ede4a02315 | ||
|
|
e25faba990 | ||
|
|
4ccf272e53 | ||
|
|
5ad0951db3 | ||
|
|
f72fcb76e3 | ||
|
|
8ca00c81b2 | ||
|
|
5c7f87b8ae | ||
|
|
9f4f7ec88c | ||
|
|
ba7676f778 | ||
|
|
ac248c32bb | ||
|
|
dd6a3e8535 | ||
|
|
69b538cfd6 | ||
|
|
e248c22f4b | ||
|
|
eff3c43483 | ||
|
|
1cbfc3ccd4 | ||
|
|
2fa72838f9 | ||
|
|
a559ec1fda | ||
|
|
8ddbc8b1fb | ||
|
|
c650a43d38 | ||
|
|
ab4cc20c8c | ||
|
|
30d613ff04 | ||
|
|
b93da5a281 | ||
|
|
9e2cf67a93 | ||
|
|
e87720ffdd | ||
|
|
5108ed7da5 | ||
|
|
77936d86f2 | ||
|
|
cbe8927d73 | ||
|
|
47a7e98da8 | ||
|
|
d4f27cd2e0 | ||
|
|
c355ad7a86 | ||
|
|
c8c9ec081d | ||
|
|
1289e46401 | ||
|
|
0b54292130 | ||
|
|
d1e2b91116 | ||
|
|
cd96b3e8f6 | ||
|
|
9119f4d06f | ||
|
|
4e6ccf2c81 | ||
|
|
6294e43762 | ||
|
|
feca78e55d | ||
|
|
0c86526ad2 | ||
|
|
9fd1d26067 | ||
|
|
5ec02078d1 | ||
|
|
012c6f3d41 | ||
|
|
8e89d492fc | ||
|
|
69fe2f0443 | ||
|
|
ebd7f4ae1b | ||
|
|
f9ca4f339e | ||
|
|
5919020e1c | ||
|
|
9d09c2356d | ||
|
|
3cfb597fc6 | ||
|
|
03f2da19e5 | ||
|
|
951e62d984 | ||
|
|
146039c4c5 | ||
|
|
98a216fdb9 | ||
|
|
28ea09a0c4 | ||
|
|
a98a772360 | ||
|
|
4f1da8a24b | ||
|
|
cb83e71f2b | ||
|
|
646d174616 | ||
|
|
34cf78fd33 | ||
|
|
457e1bee2f | ||
|
|
6b95c63c1e | ||
|
|
e965f222db | ||
|
|
d1591bc01c | ||
|
|
a72846be7a | ||
|
|
5072661369 | ||
|
|
5417a83662 | ||
|
|
23be006932 | ||
|
|
e28553767e | ||
|
|
c1f64c043d | ||
|
|
9e6d0183d4 | ||
|
|
8dc7f3fb9e | ||
|
|
ac7b3b9b67 | ||
|
|
af0ebba5db | ||
|
|
b454709e5e | ||
|
|
78684607bd | ||
|
|
3790cad5e5 | ||
|
|
77b7c091a8 | ||
|
|
450ad62fa9 | ||
|
|
1d138c33a4 | ||
|
|
fb2a0e4a1e | ||
|
|
da3db0b0f9 | ||
|
|
bc8aaadd90 | ||
|
|
e6054a4971 | ||
|
|
26548e1929 | ||
|
|
4424cf8190 | ||
|
|
67c1aacd54 | ||
|
|
39b046f18b | ||
|
|
d630f04872 | ||
|
|
35403c87bd | ||
|
|
f2b247e042 | ||
|
|
94022bd9f2 | ||
|
|
ccb2abb950 | ||
|
|
cbafc15292 | ||
|
|
e12c52294a | ||
|
|
63b529da00 | ||
|
|
e4be2fd19e | ||
|
|
269bf4feec | ||
|
|
9d4c4a1e2b | ||
|
|
2e41fdcb41 | ||
|
|
2e48218623 | ||
|
|
8576a54056 | ||
|
|
79d924f920 | ||
|
|
305beb3af8 | ||
|
|
06fceded14 | ||
|
|
0eadfd5a58 | ||
|
|
eea34a4f6c | ||
|
|
500ec36522 | ||
|
|
ca525bd08c | ||
|
|
ac2ffc4586 | ||
|
|
5781269557 | ||
|
|
e4422b9fe7 | ||
|
|
269f76d546 | ||
|
|
540e3f0aaa | ||
|
|
5f64ae28e0 | ||
|
|
ece364c823 | ||
|
|
f669f64fcb | ||
|
|
2a53ed93c4 | ||
|
|
2b731fb30c | ||
|
|
be2db2dd8e | ||
|
|
09c08df1b9 | ||
|
|
9ccd3438ad | ||
|
|
393bcbcca5 | ||
|
|
7fac0958b8 | ||
|
|
c6a0874b3b | ||
|
|
9c80470185 | ||
|
|
2034445f5b | ||
|
|
fd8da5ffba | ||
|
|
e987af87f6 | ||
|
|
0074cc3933 | ||
|
|
5f2ce89316 | ||
|
|
60492c48a6 | ||
|
|
eed2d70017 | ||
|
|
b859adaa8c | ||
|
|
89a587f9ae | ||
|
|
fb56bcff80 | ||
|
|
99eb6907dd | ||
|
|
3743fad899 | ||
|
|
c1e59a7e03 | ||
|
|
b34dee1f83 | ||
|
|
6edd65ad8f | ||
|
|
0959ca6a40 | ||
|
|
1287fa2cd0 | ||
|
|
a5a07f250d | ||
|
|
089fb526f5 | ||
|
|
af58b7593a | ||
|
|
d4508b25ce | ||
|
|
9edc218eaa | ||
|
|
3790f753aa | ||
|
|
82b30d8388 | ||
|
|
e9a0dc7826 | ||
|
|
8ce3a4f904 | ||
|
|
1d42e9c348 | ||
|
|
2340a6bc37 | ||
|
|
be0b9c7e53 | ||
|
|
6d75cd9025 | ||
|
|
345d6f369e | ||
|
|
959ea86d85 | ||
|
|
b67a99af3d | ||
|
|
fa3b848d40 | ||
|
|
0f971e9e7d | ||
|
|
c17f76c009 | ||
|
|
bf23b5d295 | ||
|
|
09c7256d42 | ||
|
|
eaee8a2fbb | ||
|
|
3b18dd67be | ||
|
|
c3f87b4248 | ||
|
|
f6b91ad652 | ||
|
|
1c79edc52f | ||
|
|
fe2dfd0e8f | ||
|
|
fa6056c1b1 | ||
|
|
d5762c7ad8 | ||
|
|
d9c9dd2a4f | ||
|
|
3a4d945c68 | ||
|
|
f4a364816b | ||
|
|
931bc03cab | ||
|
|
1abd4937cd | ||
|
|
0df8b51c62 | ||
|
|
e5b7190015 | ||
|
|
279b8aacf6 | ||
|
|
9eebaab2f4 | ||
|
|
16e9d60033 | ||
|
|
335b378e9a | ||
|
|
9c41bc33a3 | ||
|
|
7f7d6b4d5d | ||
|
|
cc0e3bbce0 | ||
|
|
2eead65fef | ||
|
|
e664be451f | ||
|
|
d2d8160096 | ||
|
|
3bd503c28d | ||
|
|
aa1df8eb33 | ||
|
|
c1aace45ae | ||
|
|
217a60aadc | ||
|
|
5654f528ca | ||
|
|
4da036a064 | ||
|
|
2256b3d262 | ||
|
|
d84ecc307d | ||
|
|
237313d5fb | ||
|
|
7caf766bca | ||
|
|
0a3f9f5ef1 | ||
|
|
e890b8f7c1 | ||
|
|
dc4d5f0ecb | ||
|
|
a2f0980731 | ||
|
|
0a5c029f8b | ||
|
|
85bb79e4fb | ||
|
|
f18d1e50f8 | ||
|
|
943b10dd5d | ||
|
|
0a48e17c88 | ||
|
|
da1381e14e | ||
|
|
bdffb0ee10 | ||
|
|
7bdb7d2ca8 | ||
|
|
92567561b8 | ||
|
|
335bdcd89d | ||
|
|
4c2fc13abb | ||
|
|
7f8f29daa2 | ||
|
|
8fac845ecb | ||
|
|
d8076e7630 | ||
|
|
b370bc27c4 | ||
|
|
334c3f4488 | ||
|
|
33822109c0 | ||
|
|
b1f18b0f5b | ||
|
|
c0d6284368 | ||
|
|
16e294f6fc | ||
|
|
5f7925b2b8 | ||
|
|
2e001b0ce4 | ||
|
|
c1ca3ff5b5 | ||
|
|
1de33cd4ca | ||
|
|
77b773388f | ||
|
|
3e668ee439 | ||
|
|
0d3ea9af36 | ||
|
|
0d81bc8056 | ||
|
|
e4b532a34d | ||
|
|
61f86c0ac3 | ||
|
|
66fad37116 | ||
|
|
c7bbd8c823 | ||
|
|
dc6f8baf1e | ||
|
|
0e76e65d65 | ||
|
|
877dbed999 | ||
|
|
668fd05fae | ||
|
|
3e49998f41 | ||
|
|
99b183ac17 | ||
|
|
bb04cddc48 | ||
|
|
ba3f095dd8 | ||
|
|
f8438421c8 | ||
|
|
334361860b | ||
|
|
23bd211758 | ||
|
|
4cc8fb9891 | ||
|
|
ed3cd690fe | ||
|
|
9a40c7cdc6 | ||
|
|
ae4c9ce819 | ||
|
|
2f8bae1356 | ||
|
|
2dfcd5b7ef | ||
|
|
739926f64e | ||
|
|
a18dde07de | ||
|
|
6480017a91 | ||
|
|
4467ec52f7 | ||
|
|
072d82a10e | ||
|
|
b50b759f4f | ||
|
|
c1477ad45f | ||
|
|
679d45399b | ||
|
|
0e15a789ff | ||
|
|
dff5b3f497 | ||
|
|
483e49a6ae | ||
|
|
a689b5b917 | ||
|
|
1363d98280 | ||
|
|
405f3dcbdd | ||
|
|
4b48408bc9 | ||
|
|
d6f1e2d7e2 | ||
|
|
c04c8e3aa4 | ||
|
|
ff5a08d3b0 | ||
|
|
d2049c759e | ||
|
|
f940cb0ace | ||
|
|
507f2f4af4 | ||
|
|
deca7099f3 | ||
|
|
46a741825a | ||
|
|
14fdcd3052 | ||
|
|
c76b01608a | ||
|
|
be709d6601 | ||
|
|
7dc8fac198 | ||
|
|
29ae7d57fd | ||
|
|
1bf9ce872b | ||
|
|
0071c9504f | ||
|
|
594a872c84 | ||
|
|
0d1f78e82e | ||
|
|
a12de51897 | ||
|
|
74fa084dd0 | ||
|
|
a1ee258da5 | ||
|
|
c1af171c5d | ||
|
|
e36c9560fa | ||
|
|
5cd19ddc8d | ||
|
|
ac243e5d11 | ||
|
|
b480d019f6 | ||
|
|
cc13ab97d6 | ||
|
|
fee1d2ed04 | ||
|
|
4144d5faa6 | ||
|
|
38a23c0bee | ||
|
|
8fcfebe170 | ||
|
|
f917fa8138 | ||
|
|
f60b611304 | ||
|
|
a54624e5c8 | ||
|
|
370b14b82e | ||
|
|
88205adeb2 | ||
|
|
351ce995d9 | ||
|
|
a9aa92de90 | ||
|
|
9ea665dea2 | ||
|
|
30b52527e7 | ||
|
|
bb4125153b | ||
|
|
f0d5b2b1da | ||
|
|
30c4048e4a | ||
|
|
93770ca9ce | ||
|
|
e788783d12 | ||
|
|
1314444d7c | ||
|
|
2a14664d34 | ||
|
|
07a03940a0 | ||
|
|
5f2f6fff56 | ||
|
|
353548660c | ||
|
|
912f07225c | ||
|
|
a23b7eeff1 | ||
|
|
574f0d71b2 | ||
|
|
a368312035 | ||
|
|
2f88b1ab65 | ||
|
|
f85f97e061 | ||
|
|
30dec13903 | ||
|
|
3057e5c997 | ||
|
|
6c413eb1ba | ||
|
|
dcdd9132e2 | ||
|
|
d9b1c36055 | ||
|
|
a593a247d7 | ||
|
|
155debc864 | ||
|
|
a5975ac38b | ||
|
|
204f1cfd6b | ||
|
|
2d22e043a0 | ||
|
|
c26cacaf4e | ||
|
|
f0048544e2 | ||
|
|
cf227dbfa2 | ||
|
|
a5f8bdbe61 | ||
|
|
e442553c6f | ||
|
|
7134acfcd6 | ||
|
|
82439f444e | ||
|
|
1a17908488 | ||
|
|
9af30e99f8 | ||
|
|
6f942c3417 | ||
|
|
57083c90cd | ||
|
|
e28bcdd978 | ||
|
|
0b4a5ab2eb | ||
|
|
034704a330 | ||
|
|
5c60eaf6ab | ||
|
|
f5709eac2c | ||
|
|
5a5e714aca | ||
|
|
747d48e4d9 | ||
|
|
07a0200f30 | ||
|
|
1c5313f2d9 | ||
|
|
05e08719fb | ||
|
|
ca0e616f88 | ||
|
|
a8d20caba4 | ||
|
|
0d4bbb0a48 | ||
|
|
b9cc219530 | ||
|
|
e204ab5871 | ||
|
|
16d0c05b4b | ||
|
|
6f8329d191 | ||
|
|
d751463b26 | ||
|
|
d3b66eff59 | ||
|
|
4257d0332a | ||
|
|
b80442c061 | ||
|
|
3a0f6820ad | ||
|
|
1bc92f5363 | ||
|
|
818ddcf01e | ||
|
|
618ba361c7 | ||
|
|
599160a325 | ||
|
|
35fba6f4ed | ||
|
|
a14aad75fd | ||
|
|
3513e85b0b | ||
|
|
66c0390fc7 | ||
|
|
a6549ccb08 | ||
|
|
15d2878014 | ||
|
|
d271be8723 | ||
|
|
6f9d2d99dd | ||
|
|
5d62664ee3 | ||
|
|
7124d9f2f8 | ||
|
|
0459744771 | ||
|
|
417544b781 | ||
|
|
f9028cb366 | ||
|
|
9a264719a9 | ||
|
|
96c213dcc4 | ||
|
|
dec1a8e204 | ||
|
|
a17fd697e2 | ||
|
|
a6ab66e799 | ||
|
|
17095ec3c6 | ||
|
|
82687147b8 | ||
|
|
ba76422c1f | ||
|
|
083b3c4ece | ||
|
|
5ecfdf38a8 | ||
|
|
dd1acf3c2a | ||
|
|
76e9c2d196 | ||
|
|
15f046959d | ||
|
|
bf3ba04624 | ||
|
|
d997894d9a | ||
|
|
c1059db6e5 | ||
|
|
8ad29a2836 | ||
|
|
93a454b835 | ||
|
|
da899386ec | ||
|
|
05d22903ea | ||
|
|
33945520f1 | ||
|
|
40284809cf | ||
|
|
efc18aaaec | ||
|
|
348441b046 | ||
|
|
66601b2e7c | ||
|
|
724c5e4b73 | ||
|
|
7eff29bc65 | ||
|
|
ca002003c2 | ||
|
|
f0675f1f3c | ||
|
|
976186c525 | ||
|
|
89d5777e52 | ||
|
|
8dbb69809c | ||
|
|
7348bd5d15 | ||
|
|
9a46a466f7 | ||
|
|
fafc5c8553 | ||
|
|
4ffdfaa506 | ||
|
|
e3989840ee | ||
|
|
b3e6f531a1 | ||
|
|
4f6ee34592 | ||
|
|
3ae58a323e | ||
|
|
26b958c270 | ||
|
|
12a4af5900 | ||
|
|
69479d538c | ||
|
|
829397dd5a | ||
|
|
2bc89026db | ||
|
|
ebbc44d181 | ||
|
|
2228a1e36b | ||
|
|
a8cbf3e8ff | ||
|
|
fa32e3d734 | ||
|
|
0d17148ff0 | ||
|
|
aa38411cf7 | ||
|
|
4913c8699d | ||
|
|
1035a11487 | ||
|
|
15c2efe706 | ||
|
|
d7fd71bb62 | ||
|
|
b11ee993fa | ||
|
|
614aa7873c | ||
|
|
1adf31fe15 | ||
|
|
824ffd7b5b | ||
|
|
c31c6fdebb | ||
|
|
83f3276429 | ||
|
|
d21f68ce54 | ||
|
|
18b1e1b133 | ||
|
|
0edaa40052 | ||
|
|
627077c8f3 | ||
|
|
a897b1798d | ||
|
|
50e39993bf | ||
|
|
5e397dd01e | ||
|
|
f57ff5d5e0 | ||
|
|
5c3e40917c | ||
|
|
90a2dc4581 | ||
|
|
b64243fdd6 | ||
|
|
42db87d305 | ||
|
|
e7ab1b589a | ||
|
|
e9979c9887 | ||
|
|
3bb9bb56f0 | ||
|
|
5a99474c55 | ||
|
|
182ee6c25f | ||
|
|
4d3f0a06db | ||
|
|
0e182c519b | ||
|
|
b1ee30ce7d | ||
|
|
93ba764e23 | ||
|
|
433e17bb81 | ||
|
|
61c09083ad | ||
|
|
018377e724 | ||
|
|
b76f9513ba | ||
|
|
40ebb7ba75 | ||
|
|
a9e52e8954 | ||
|
|
3c8876cac7 | ||
|
|
b7e005f9c7 | ||
|
|
e6fe0a19fa | ||
|
|
fba11b6a44 | ||
|
|
c270e7f5dd | ||
|
|
9ee00d345e | ||
|
|
0379fbc4eb | ||
|
|
9748a3ae91 | ||
|
|
1881944748 | ||
|
|
3721fa194c | ||
|
|
8c3fcad20b | ||
|
|
decf373d0b | ||
|
|
ff1d50f993 | ||
|
|
ef34204b59 | ||
|
|
270b636d80 | ||
|
|
ac01da2ae9 | ||
|
|
0136310c54 | ||
|
|
ecf4cf852e | ||
|
|
c66384adfb | ||
|
|
98bdda629d | ||
|
|
a8286f9cba | ||
|
|
fa3db4fcf6 | ||
|
|
ddac0cfee1 | ||
|
|
9368673459 | ||
|
|
43dc999ab5 | ||
|
|
3b7333e866 | ||
|
|
bc0ddbaf16 | ||
|
|
45f0ae7e1c | ||
|
|
a521c4ae01 | ||
|
|
5b8238adeb | ||
|
|
ec330474fa | ||
|
|
ece28904a8 | ||
|
|
4f1c495afb | ||
|
|
5fdd27b7e6 | ||
|
|
91f449af9a | ||
|
|
efc0a0dfe3 | ||
|
|
fee47baa66 | ||
|
|
0ad7bfc7e7 | ||
|
|
bd64143ae1 | ||
|
|
ec982ba9a3 | ||
|
|
6280f6ff98 | ||
|
|
35d20390a9 | ||
|
|
c487c5042f | ||
|
|
aaf7927aa2 | ||
|
|
3c677f3d21 | ||
|
|
94eb76b3a6 | ||
|
|
a921cb2d0d | ||
|
|
f3aaa363d8 | ||
|
|
45a79e1920 | ||
|
|
6fd9b2a453 | ||
|
|
01d8e89a71 | ||
|
|
c89fa63910 | ||
|
|
9fc5c49dbf | ||
|
|
7dfc269df9 | ||
|
|
76d0b397db | ||
|
|
5413f887af | ||
|
|
b3d0c61f0e | ||
|
|
4ce0441d68 | ||
|
|
72be34e18d | ||
|
|
d2961b7650 | ||
|
|
fdca1bbf72 | ||
|
|
ab7a2f9dee | ||
|
|
7b72857a3b | ||
|
|
4787146658 | ||
|
|
430f9356c3 | ||
|
|
70a3b3518f | ||
|
|
c0944c17e0 | ||
|
|
da1b2a91e7 | ||
|
|
aa27492713 | ||
|
|
afe589dec3 | ||
|
|
978d140c8f | ||
|
|
2ce213b62c | ||
|
|
7748266078 | ||
|
|
83783d07a1 | ||
|
|
49a1f2c7c5 | ||
|
|
ddfc0151fc | ||
|
|
81c508e13c | ||
|
|
7195cfc3cf | ||
|
|
93fe5e2cf7 | ||
|
|
a2bf795d12 | ||
|
|
c8d78f39e0 | ||
|
|
d9ab8a1c8b | ||
|
|
5125ad4889 | ||
|
|
951e85b04b | ||
|
|
711d922695 | ||
|
|
3692ffcde7 | ||
|
|
b049420c59 | ||
|
|
241103c369 | ||
|
|
2128367113 | ||
|
|
f555c8190d | ||
|
|
d5df633def | ||
|
|
fe7dc859e3 | ||
|
|
569c5046c6 | ||
|
|
e0210ae2d8 | ||
|
|
f85dc3b7e7 | ||
|
|
92d4363120 | ||
|
|
6c69220de2 | ||
|
|
3a1229b072 | ||
|
|
45538c9f62 |
12
.babelrc
12
.babelrc
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"comments": false,
|
||||
"compact": true,
|
||||
"plugins": [
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
"es2015",
|
||||
"stage-0",
|
||||
"react"
|
||||
]
|
||||
}
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,9 +1,7 @@
|
||||
/.nyc_output/
|
||||
/bower_components/
|
||||
/dist/
|
||||
/node_modules/
|
||||
|
||||
npm-debug.log
|
||||
npm-debug.log.*
|
||||
|
||||
!node_modules/*
|
||||
node_modules/*/
|
||||
pnpm-debug.log
|
||||
pnpm-debug.log.*
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
Error.stackTraceLimit = 100
|
||||
|
||||
try { require('trace') } catch (_) {}
|
||||
try { require('clarify') } catch (_) {}
|
||||
try { require('source-map-support/register') } catch (_) {}
|
||||
@@ -1 +0,0 @@
|
||||
--require ./.mocha.js
|
||||
@@ -1,9 +1,11 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 'stable'
|
||||
- '4'
|
||||
- '0.12'
|
||||
- '0.10'
|
||||
#- '4' # Disabled for now because npm 2 cannot properly handled broken peer dependencies.
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
|
||||
580
CHANGELOG.md
580
CHANGELOG.md
@@ -1,5 +1,585 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.2.5** (2016-10-07)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Stats in home/host view when expanded [\#1634](https://github.com/vatesfr/xo-web/issues/1634)
|
||||
- Bar for used and total RAM on home pool view [\#1625](https://github.com/vatesfr/xo-web/issues/1625)
|
||||
- Can't translate some text [\#1624](https://github.com/vatesfr/xo-web/issues/1624)
|
||||
- Dynamic RAM allocation at creation time [\#1603](https://github.com/vatesfr/xo-web/issues/1603)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Do not expose shortcuts while console is focused [\#1614](https://github.com/vatesfr/xo-web/issues/1614)
|
||||
|
||||
## **5.2.4** (2016-10-04)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Display memory bar in home/host view [\#1616](https://github.com/vatesfr/xo-web/issues/1616)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- All users can see VM templates [\#1621](https://github.com/vatesfr/xo-web/issues/1621)
|
||||
|
||||
## **5.2.3** (2016-10-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Improve keyboard navigation [\#1578](https://github.com/vatesfr/xo-web/issues/1578)
|
||||
- Strongly suggest to install the guest tools [\#1575](https://github.com/vatesfr/xo-web/issues/1575)
|
||||
- Missing tooltip [\#1568](https://github.com/vatesfr/xo-web/issues/1568)
|
||||
- Emphasize already used ips in ipPools [\#1566](https://github.com/vatesfr/xo-web/issues/1566)
|
||||
- Change "missing feature message" for non-admins [\#1564](https://github.com/vatesfr/xo-web/issues/1564)
|
||||
- Allow VIF edition [\#1446](https://github.com/vatesfr/xo-web/issues/1446)
|
||||
- Disable browser autocomplete on credentials on the Update page [\#1304](https://github.com/vatesfr/xo-web/issues/1304)
|
||||
- keyboard shortcuts [\#1279](https://github.com/vatesfr/xo-web/issues/1279)
|
||||
- Add network bond creation [\#876](https://github.com/vatesfr/xo-web/issues/876)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Profile page is broken [\#1612](https://github.com/vatesfr/xo-web/issues/1612)
|
||||
- SR delete should redirect to home [\#1611](https://github.com/vatesfr/xo-web/issues/1611)
|
||||
- Delta VHD backup checksum is invalidated by chaining [\#1606](https://github.com/vatesfr/xo-web/issues/1606)
|
||||
- VM with long description break on 2 lines [\#1580](https://github.com/vatesfr/xo-web/issues/1580)
|
||||
- Network status on VM edition [\#1573](https://github.com/vatesfr/xo-web/issues/1573)
|
||||
- VM template deletion fails [\#1571](https://github.com/vatesfr/xo-web/issues/1571)
|
||||
- Template edition - "no such object" [\#1569](https://github.com/vatesfr/xo-web/issues/1569)
|
||||
- missing links / element not displayed as links [\#1567](https://github.com/vatesfr/xo-web/issues/1567)
|
||||
- Backup restore stalled on some SMB shares [\#1412](https://github.com/vatesfr/xo-web/issues/1412)
|
||||
- Wrong bond display [\#1156](https://github.com/vatesfr/xo-web/issues/1156)
|
||||
|
||||
## **5.2.2** (2016-09-21)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- `pool.setDefaultSr\(\)` should not require `pool` param [\#1558](https://github.com/vatesfr/xo-web/issues/1558)
|
||||
- Select default SR [\#1554](https://github.com/vatesfr/xo-web/issues/1554)
|
||||
- No error message when I exceed my resource set quota [\#1541](https://github.com/vatesfr/xo-web/issues/1541)
|
||||
- Hide some buttons for self service VMs [\#1539](https://github.com/vatesfr/xo-web/issues/1539)
|
||||
- Add Job ID to backup schedules [\#1534](https://github.com/vatesfr/xo-web/issues/1534)
|
||||
- Correct name for VM selector with templates [\#1530](https://github.com/vatesfr/xo-web/issues/1530)
|
||||
- Help text when no matches for a filter [\#1517](https://github.com/vatesfr/xo-web/issues/1517)
|
||||
- Icon or tooltip to allow VDI migration in VM disk view [\#1512](https://github.com/vatesfr/xo-web/issues/1512)
|
||||
- Create a snapshot before restoring one [\#1445](https://github.com/vatesfr/xo-web/issues/1445)
|
||||
- Auto power on setting at creation time [\#1444](https://github.com/vatesfr/xo-web/issues/1444)
|
||||
- local remotes should be avoided if possible [\#1441](https://github.com/vatesfr/xo-web/issues/1441)
|
||||
- Self service edition unclear [\#1429](https://github.com/vatesfr/xo-web/issues/1429)
|
||||
- Avoid "\_" char in job tag name [\#1414](https://github.com/vatesfr/xo-web/issues/1414)
|
||||
- Display message if host reboot needed to apply patches [\#1352](https://github.com/vatesfr/xo-web/issues/1352)
|
||||
- Color code on host PIF stats can be misleading [\#1265](https://github.com/vatesfr/xo-web/issues/1265)
|
||||
- Sign in page is not rendered correctly [\#1161](https://github.com/vatesfr/xo-web/issues/1161)
|
||||
- Template management [\#1091](https://github.com/vatesfr/xo-web/issues/1091)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Multiple reboot selection doesn't work [\#1562](https://github.com/vatesfr/xo-web/issues/1562)
|
||||
- Server logs should be displayed in reverse chonological order [\#1547](https://github.com/vatesfr/xo-web/issues/1547)
|
||||
- Cannot create resource sets without limits [\#1537](https://github.com/vatesfr/xo-web/issues/1537)
|
||||
- UI - Weird display when editing long VM desc [\#1528](https://github.com/vatesfr/xo-web/issues/1528)
|
||||
- Useless iso selector in host console [\#1527](https://github.com/vatesfr/xo-web/issues/1527)
|
||||
- Pool and Host dummy welcome message [\#1519](https://github.com/vatesfr/xo-web/issues/1519)
|
||||
- Bug on Network VM tab [\#1518](https://github.com/vatesfr/xo-web/issues/1518)
|
||||
- Link to home with filter in query does not work [\#1513](https://github.com/vatesfr/xo-web/issues/1513)
|
||||
- VHD merge fails with "RangeError: index out of range" on SMB remote [\#1511](https://github.com/vatesfr/xo-web/issues/1511)
|
||||
- DR: previous VDIs are not removed [\#1510](https://github.com/vatesfr/xo-web/issues/1510)
|
||||
- DR: previous copies not removed when same number as depth [\#1509](https://github.com/vatesfr/xo-web/issues/1509)
|
||||
- Empty Saved Search doesn't load when set to default filter [\#1354](https://github.com/vatesfr/xo-web/issues/1354)
|
||||
- Removing a user/group should delete its ACLs [\#899](https://github.com/vatesfr/xo-web/issues/899)
|
||||
- OVA Import - XO stuck during import [\#1551](https://github.com/vatesfr/xo-web/issues/1551)
|
||||
|
||||
## **5.2.1** (2016-09-13)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- On pool view: collapse network list [\#1461](https://github.com/vatesfr/xo-web/issues/1461)
|
||||
- Alert when trying to reboot/halt the pool master XS [\#1458](https://github.com/vatesfr/xo-web/issues/1458)
|
||||
- Adding tooltip on Home page [\#1456](https://github.com/vatesfr/xo-web/issues/1456)
|
||||
- Docker container management functionality missing from v5 [\#1442](https://github.com/vatesfr/xo-web/issues/1442)
|
||||
- bad error message - delete snapshot [\#1433](https://github.com/vatesfr/xo-web/issues/1433)
|
||||
- Create tag during VM creation [\#1431](https://github.com/vatesfr/xo-web/issues/1431)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- SMB remote empty domain fails [\#1499](https://github.com/vatesfr/xo-web/issues/1499)
|
||||
- Can't edit a remote password [\#1498](https://github.com/vatesfr/xo-web/issues/1498)
|
||||
- Issue in VM create with CoreOS [\#1493](https://github.com/vatesfr/xo-web/issues/1493)
|
||||
- Overlapping months in backup view [\#1488](https://github.com/vatesfr/xo-web/issues/1488)
|
||||
- No line break for SSH key in user view [\#1475](https://github.com/vatesfr/xo-web/issues/1475)
|
||||
- Create VIF UI issues [\#1472](https://github.com/vatesfr/xo-web/issues/1472)
|
||||
|
||||
## **5.2.0** (2016-09-09)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- IP management [\#1350](https://github.com/vatesfr/xo-web/issues/1350), [\#988](https://github.com/vatesfr/xo-web/issues/988), [\#1427](https://github.com/vatesfr/xo-web/issues/1427) and [\#240](https://github.com/vatesfr/xo-web/issues/240)
|
||||
- Update reverse proxy example [\#1474](https://github.com/vatesfr/xo-web/issues/1474)
|
||||
- Improve log view [\#1467](https://github.com/vatesfr/xo-web/issues/1467)
|
||||
- Backup Reports: e-mail subject [\#1463](https://github.com/vatesfr/xo-web/issues/1463)
|
||||
- Backup Reports: report the error [\#1462](https://github.com/vatesfr/xo-web/issues/1462)
|
||||
- Vif selector: select management network by default [\#1425](https://github.com/vatesfr/xo-web/issues/1425)
|
||||
- Display when browser disconnected to server [\#1417](https://github.com/vatesfr/xo-web/issues/1417)
|
||||
- Tooltip on OS icon in VM view [\#1416](https://github.com/vatesfr/xo-web/issues/1416)
|
||||
- Display pool master [\#1407](https://github.com/vatesfr/xo-web/issues/1407)
|
||||
- Missing tooltips in VM creation view [\#1402](https://github.com/vatesfr/xo-web/issues/1402)
|
||||
- Handle VDB disconnect and connect [\#1397](https://github.com/vatesfr/xo-web/issues/1397)
|
||||
- Eject host from a pool [\#1395](https://github.com/vatesfr/xo-web/issues/1395)
|
||||
- Improve pool general view [\#1393](https://github.com/vatesfr/xo-web/issues/1393)
|
||||
- Improve patching system [\#1392](https://github.com/vatesfr/xo-web/issues/1392)
|
||||
- Pool name modification [\#1390](https://github.com/vatesfr/xo-web/issues/1390)
|
||||
- Confirmation dialog before destroying VDIs [\#1388](https://github.com/vatesfr/xo-web/issues/1388)
|
||||
- Tooltips for meter object [\#1387](https://github.com/vatesfr/xo-web/issues/1387)
|
||||
- New Host assistant [\#1374](https://github.com/vatesfr/xo-web/issues/1374)
|
||||
- New VM assistant [\#1373](https://github.com/vatesfr/xo-web/issues/1373)
|
||||
- New SR assistant [\#1372](https://github.com/vatesfr/xo-web/issues/1372)
|
||||
- Direct access to VDI listing from dashboard's SR usage breakdown [\#1371](https://github.com/vatesfr/xo-web/issues/1371)
|
||||
- Can't set a network name at pool level [\#1368](https://github.com/vatesfr/xo-web/issues/1368)
|
||||
- Change a few mouse over descriptions [\#1363](https://github.com/vatesfr/xo-web/issues/1363)
|
||||
- Hide network install in VM create if template is HVM [\#1362](https://github.com/vatesfr/xo-web/issues/1362)
|
||||
- SR space left during VM creation [\#1358](https://github.com/vatesfr/xo-web/issues/1358)
|
||||
- Add destination SR on migration modal in VM view [\#1357](https://github.com/vatesfr/xo-web/issues/1357)
|
||||
- Ability to create a new VM from a snapshot [\#1353](https://github.com/vatesfr/xo-web/issues/1353)
|
||||
- Missing explanation/confirmation on Snapshot Page [\#1349](https://github.com/vatesfr/xo-web/issues/1349)
|
||||
- Log view: expose API errors in the web UI [\#1344](https://github.com/vatesfr/xo-web/issues/1344)
|
||||
- Registration on update page [\#1341](https://github.com/vatesfr/xo-web/issues/1341)
|
||||
- Add export snapshot button [\#1336](https://github.com/vatesfr/xo-web/issues/1336)
|
||||
- Use saved SSH keys in VM create CloudConfig [\#1319](https://github.com/vatesfr/xo-web/issues/1319)
|
||||
- Collapse header in console view [\#1268](https://github.com/vatesfr/xo-web/issues/1268)
|
||||
- Two max concurrent jobs in parallel [\#915](https://github.com/vatesfr/xo-web/issues/915)
|
||||
- Handle OVA import via the web UI [\#709](https://github.com/vatesfr/xo-web/issues/709)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Bug on VM console when header is hidden [\#1485](https://github.com/vatesfr/xo-web/issues/1485)
|
||||
- Disks not removed when deleting multiple VMs [\#1484](https://github.com/vatesfr/xo-web/issues/1484)
|
||||
- Do not display VDI disconnect button when a VM is not running [\#1470](https://github.com/vatesfr/xo-web/issues/1470)
|
||||
- Do not display VIF disconnect button when a VM is not running [\#1468](https://github.com/vatesfr/xo-web/issues/1468)
|
||||
- Error on migration if no default SR \(even when not used\) [\#1466](https://github.com/vatesfr/xo-web/issues/1466)
|
||||
- DR issue while rotating old backup [\#1464](https://github.com/vatesfr/xo-web/issues/1464)
|
||||
- Giving resource set to end-user ends with error [\#1448](https://github.com/vatesfr/xo-web/issues/1448)
|
||||
- Error thrown when cancelling out of Delete User confirmation dialog [\#1439](https://github.com/vatesfr/xo-web/issues/1439)
|
||||
- Wrong month label shown in Backup and Job scheduler [\#1438](https://github.com/vatesfr/xo-web/issues/1438)
|
||||
- Bug on Self service creation/edition [\#1428](https://github.com/vatesfr/xo-web/issues/1428)
|
||||
- ISO selection during VM create is not mounted after [\#1415](https://github.com/vatesfr/xo-web/issues/1415)
|
||||
- Hosts general view: bad link for storage [\#1408](https://github.com/vatesfr/xo-web/issues/1408)
|
||||
- Backup Schedule - "Month" and "Day of Week" display error [\#1404](https://github.com/vatesfr/xo-web/issues/1404)
|
||||
- Migrate dialog doesn't present all available VIF's in new UI interface [\#1403](https://github.com/vatesfr/xo-web/issues/1403)
|
||||
- NFS mount issues [\#1396](https://github.com/vatesfr/xo-web/issues/1396)
|
||||
- Select component color [\#1391](https://github.com/vatesfr/xo-web/issues/1391)
|
||||
- SR created with local path shouldn't be shared [\#1389](https://github.com/vatesfr/xo-web/issues/1389)
|
||||
- Disks (VBD) are attached to VM in RO mode instead of RW even if RO is unchecked [\#1386](https://github.com/vatesfr/xo-web/issues/1386)
|
||||
- Re-connection issues between server and XS hosts [\#1384](https://github.com/vatesfr/xo-web/issues/1384)
|
||||
- Meter object style with Chrome 52 [\#1383](https://github.com/vatesfr/xo-web/issues/1383)
|
||||
- Editing a rolling snapshot job seems to fail [\#1376](https://github.com/vatesfr/xo-web/issues/1376)
|
||||
- Dashboard SR usage and total inverted [\#1370](https://github.com/vatesfr/xo-web/issues/1370)
|
||||
- XenServer connection issue with host while using VGPUs [\#1369](https://github.com/vatesfr/xo-web/issues/1369)
|
||||
- Job created with v4 are not correctly displayed in v5 [\#1366](https://github.com/vatesfr/xo-web/issues/1366)
|
||||
- CPU accounting in resource set [\#1365](https://github.com/vatesfr/xo-web/issues/1365)
|
||||
- Tooltip stay displayed when a button change state [\#1360](https://github.com/vatesfr/xo-web/issues/1360)
|
||||
- Failure on host reboot [\#1351](https://github.com/vatesfr/xo-web/issues/1351)
|
||||
- Editing Backup Jobs Without Compression, Slider Always Set To On [\#1339](https://github.com/vatesfr/xo-web/issues/1339)
|
||||
- Month Selection on Backup Screen Wrong [\#1338](https://github.com/vatesfr/xo-web/issues/1338)
|
||||
- Delta backup fail when removed VDIs [\#1333](https://github.com/vatesfr/xo-web/issues/1333)
|
||||
|
||||
## **5.1.0** (2016-07-26)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Improve backups timezone UI [\#1314](https://github.com/vatesfr/xo-web/issues/1314)
|
||||
- HOME view submenus [\#1306](https://github.com/vatesfr/xo-web/issues/1306)
|
||||
- Ability for a user to save SSH keys [\#1299](https://github.com/vatesfr/xo-web/issues/1299)
|
||||
- \[ACLs\] Ability to select all hosts/VMs [\#1296](https://github.com/vatesfr/xo-web/issues/1296)
|
||||
- Improve scheduling UI [\#1295](https://github.com/vatesfr/xo-web/issues/1295)
|
||||
- Plugins: Predefined configurations [\#1289](https://github.com/vatesfr/xo-web/issues/1289)
|
||||
- Button to recompute resource sets limits [\#1287](https://github.com/vatesfr/xo-web/issues/1287)
|
||||
- Credit scheduler CAP and weight configuration [\#1283](https://github.com/vatesfr/xo-web/issues/1283)
|
||||
- Migration form problem on the /v5/vms/\_\_UUID\_\_ page when doing xenmotion inside a pool [\#1254](https://github.com/vatesfr/xo-web/issues/1254)
|
||||
- /v5/\#/pools/\_\_UUID\_\_: patch table improvement [\#1246](https://github.com/vatesfr/xo-web/issues/1246)
|
||||
- /v5/\#/hosts/\_\_UUID\_\_: patch list improvements ? [\#1245](https://github.com/vatesfr/xo-web/issues/1245)
|
||||
- F\*cking patches, how do they work? [\#1236](https://github.com/vatesfr/xo-web/issues/1236)
|
||||
- Change Default Filter [\#1235](https://github.com/vatesfr/xo-web/issues/1235)
|
||||
- Add a property on jobs to know their state [\#1232](https://github.com/vatesfr/xo-web/issues/1232)
|
||||
- Spanish translation [\#1231](https://github.com/vatesfr/xo-web/issues/1231)
|
||||
- Home: "Filter" input and keyboard focus [\#1228](https://github.com/vatesfr/xo-web/issues/1228)
|
||||
- Display xenserver version [\#1225](https://github.com/vatesfr/xo-web/issues/1225)
|
||||
- Plugin config: presets & defaults [\#1222](https://github.com/vatesfr/xo-web/issues/1222)
|
||||
- Allow halted VM migration [\#1216](https://github.com/vatesfr/xo-web/issues/1216)
|
||||
- Missing confirm dialog on critical button [\#1211](https://github.com/vatesfr/xo-web/issues/1211)
|
||||
- Backup logs are not sortable [\#1196](https://github.com/vatesfr/xo-web/issues/1196)
|
||||
- Page title with the name of current object [\#1185](https://github.com/vatesfr/xo-web/issues/1185)
|
||||
- Existing VIF management [\#1176](https://github.com/vatesfr/xo-web/issues/1176)
|
||||
- Do not display fast clone option is there isn't template disks [\#1172](https://github.com/vatesfr/xo-web/issues/1172)
|
||||
- UI issue when adding a user [\#1159](https://github.com/vatesfr/xo-web/issues/1159)
|
||||
- Combined values on stats [\#1158](https://github.com/vatesfr/xo-web/issues/1158)
|
||||
- Parallel coordinates graph [\#1157](https://github.com/vatesfr/xo-web/issues/1157)
|
||||
- VM creation on self-service as user [\#1155](https://github.com/vatesfr/xo-web/issues/1155)
|
||||
- VM copy bulk action on home view [\#1154](https://github.com/vatesfr/xo-web/issues/1154)
|
||||
- Better VDI map [\#1151](https://github.com/vatesfr/xo-web/issues/1151)
|
||||
- Missing tooltips on buttons [\#1150](https://github.com/vatesfr/xo-web/issues/1150)
|
||||
- Patching from pool view [\#1149](https://github.com/vatesfr/xo-web/issues/1149)
|
||||
- Missing patches in dashboard [\#1148](https://github.com/vatesfr/xo-web/issues/1148)
|
||||
- Improve tasks view [\#1147](https://github.com/vatesfr/xo-web/issues/1147)
|
||||
- Home bulk VM migration [\#1146](https://github.com/vatesfr/xo-web/issues/1146)
|
||||
- LDAP plugin clear password field [\#1145](https://github.com/vatesfr/xo-web/issues/1145)
|
||||
- Cron default behavior [\#1144](https://github.com/vatesfr/xo-web/issues/1144)
|
||||
- Modal for migrate on home [\#1143](https://github.com/vatesfr/xo-web/issues/1143)
|
||||
- /v5/\#/srs/\_\_UUID\_\_: UI improvements [\#1142](https://github.com/vatesfr/xo-web/issues/1142)
|
||||
- /v5/\#/pools/: some name should be links [\#1141](https://github.com/vatesfr/xo-web/issues/1141)
|
||||
- create the page /v5/\#/pools/ [\#1140](https://github.com/vatesfr/xo-web/issues/1140)
|
||||
- Dashboard: add links to different part of XOA [\#1139](https://github.com/vatesfr/xo-web/issues/1139)
|
||||
- /v5/\#/dashboard/overview: add link on the "Top 5 SR Usage" graph [\#1135](https://github.com/vatesfr/xo-web/issues/1135)
|
||||
- /v5/\#/backup/overview: display the error when there is one returned by xenserver on failed job. [\#1134](https://github.com/vatesfr/xo-web/issues/1134)
|
||||
- /v5/: add an option to set the number of element displayed in tables [\#1133](https://github.com/vatesfr/xo-web/issues/1133)
|
||||
- Updater refresh page after update [\#1131](https://github.com/vatesfr/xo-web/issues/1131)
|
||||
- /v5/\#/settings/plugins [\#1130](https://github.com/vatesfr/xo-web/issues/1130)
|
||||
- /v5/\#/new/sr: layout issue [\#1129](https://github.com/vatesfr/xo-web/issues/1129)
|
||||
- v5 /v5/\#/vms/new: layout issue [\#1128](https://github.com/vatesfr/xo-web/issues/1128)
|
||||
- v5 user page missing style [\#1127](https://github.com/vatesfr/xo-web/issues/1127)
|
||||
- Remote helper/tester [\#1075](https://github.com/vatesfr/xo-web/issues/1075)
|
||||
- Generate uiSchema from custom schema properties [\#951](https://github.com/vatesfr/xo-web/issues/951)
|
||||
- Customizing VM names generation during batch creation [\#949](https://github.com/vatesfr/xo-web/issues/949)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Plugins: Don't use `default` attributes in presets list [\#1288](https://github.com/vatesfr/xo-web/issues/1288)
|
||||
- CPU weight must be an integer [\#1286](https://github.com/vatesfr/xo-web/issues/1286)
|
||||
- Overview of self service is always empty [\#1282](https://github.com/vatesfr/xo-web/issues/1282)
|
||||
- SR attach/creation issue [\#1281](https://github.com/vatesfr/xo-web/issues/1281)
|
||||
- Self service resources not modified after a VM deletion [\#1276](https://github.com/vatesfr/xo-web/issues/1276)
|
||||
- Scheduled jobs seems use GMT since 5.0 [\#1258](https://github.com/vatesfr/xo-web/issues/1258)
|
||||
- Can't create a VM with disks on 2 different SRs [\#1257](https://github.com/vatesfr/xo-web/issues/1257)
|
||||
- Graph display bug [\#1247](https://github.com/vatesfr/xo-web/issues/1247)
|
||||
- /v5/#/hosts/__UUID__: Patch list not limited to the current pool [\#1244](https://github.com/vatesfr/xo-web/issues/1244)
|
||||
- Replication issues [\#1233](https://github.com/vatesfr/xo-web/issues/1233)
|
||||
- VM creation install method disabled fields [\#1198](https://github.com/vatesfr/xo-web/issues/1198)
|
||||
- Update icon shouldn't be displayed when menu is collapsed [\#1188](https://github.com/vatesfr/xo-web/issues/1188)
|
||||
- /v5/ : Load average graph axis issue [\#1167](https://github.com/vatesfr/xo-web/issues/1167)
|
||||
- Some remote can't be opened [\#1164](https://github.com/vatesfr/xo-web/issues/1164)
|
||||
- Bulk action for hosts in home and pool view [\#1153](https://github.com/vatesfr/xo-web/issues/1153)
|
||||
- New Vif [\#1138](https://github.com/vatesfr/xo-web/issues/1138)
|
||||
- Missing SRs [\#1123](https://github.com/vatesfr/xo-web/issues/1123)
|
||||
- Continuous replication email alert does not obey per job setting [\#1121](https://github.com/vatesfr/xo-web/issues/1121)
|
||||
- Safari XO5 issue [\#1120](https://github.com/vatesfr/xo-web/issues/1120)
|
||||
- ACLs shoud be available in Enterprise Edition [\#1118](https://github.com/vatesfr/xo-web/issues/1118)
|
||||
- SR edit name or description doesn't work [\#1116](https://github.com/vatesfr/xo-web/issues/1116)
|
||||
- Bad RRD parsing for VIFs [\#969](https://github.com/vatesfr/xo-web/issues/969)
|
||||
|
||||
## **5.0.0** (2016-06-24)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Handle failed quiesce in snapshots [\#1088](https://github.com/vatesfr/xo-web/issues/1088)
|
||||
- Sparklines stats [\#1061](https://github.com/vatesfr/xo-web/issues/1061)
|
||||
- Task view [\#1060](https://github.com/vatesfr/xo-web/issues/1060)
|
||||
- Improved import system [\#1048](https://github.com/vatesfr/xo-web/issues/1048)
|
||||
- Backup restore view improvements [\#1021](https://github.com/vatesfr/xo-web/issues/1021)
|
||||
- Restore VM - Wrong VLAN on the VMs interface [\#1016](https://github.com/vatesfr/xo-web/issues/1016)
|
||||
- Fast Disk Cloning [\#960](https://github.com/vatesfr/xo-web/issues/960)
|
||||
- Disaster recovery job should target SRs, not pools [\#955](https://github.com/vatesfr/xo-web/issues/955)
|
||||
- Improve Header/Content interaction in a page [\#926](https://github.com/vatesfr/xo-web/issues/926)
|
||||
- New default view [\#912](https://github.com/vatesfr/xo-web/issues/912)
|
||||
- Xen Patching - Restart Pending [\#883](https://github.com/vatesfr/xo-web/issues/883)
|
||||
- Hide About page for user that are not admin [\#877](https://github.com/vatesfr/xo-web/issues/877)
|
||||
- ACL: Ability to view/sort/group by User/Group, Objects or Role [\#875](https://github.com/vatesfr/xo-web/issues/875)
|
||||
- ACL: Ability to select multiple users & group when creating a rule [\#874](https://github.com/vatesfr/xo-web/issues/874)
|
||||
- Translation [\#839](https://github.com/vatesfr/xo-web/issues/839)
|
||||
- XO offer useless network interfaces for XenMontion [\#833](https://github.com/vatesfr/xo-web/issues/833)
|
||||
- Show HVM, PVM, PVHVM modes in guest details [\#806](https://github.com/vatesfr/xo-web/issues/806)
|
||||
- Tree view: display cpu available/total for each host [\#696](https://github.com/vatesfr/xo-web/issues/696)
|
||||
- Greenkeeper integration [\#667](https://github.com/vatesfr/xo-web/issues/667)
|
||||
- Clarify vCPUs and RAM editor [\#658](https://github.com/vatesfr/xo-web/issues/658)
|
||||
- Backup LZ4 compression [\#647](https://github.com/vatesfr/xo-web/issues/647)
|
||||
- Support enum in plugins configuration [\#638](https://github.com/vatesfr/xo-web/issues/638)
|
||||
- Add configuration option to disable xoa-updater [\#535](https://github.com/vatesfr/xo-web/issues/535)
|
||||
- Use cursors to add more context to actions [\#523](https://github.com/vatesfr/xo-web/issues/523)
|
||||
- Review UI for flat view [\#354](https://github.com/vatesfr/xo-web/issues/354)
|
||||
- Review UI for the tree view [\#353](https://github.com/vatesfr/xo-web/issues/353)
|
||||
- Tag filtering [\#233](https://github.com/vatesfr/xo-web/issues/233)
|
||||
- GUI review [\#230](https://github.com/vatesfr/xo-web/issues/230)
|
||||
- Review UI for VM creation [\#214](https://github.com/vatesfr/xo-web/issues/214)
|
||||
- Ability to collapse pools/hosts in main view [\#173](https://github.com/vatesfr/xo-web/issues/173)
|
||||
- Issue importing .xva VM via xo-web [\#1022](https://github.com/vatesfr/xo-web/issues/1022)
|
||||
- Enhancement Proposal - Cancel In Progress Backups [\#1003](https://github.com/vatesfr/xo-web/issues/1003)
|
||||
- Can't create VM with CloudConfigDrive [\#917](https://github.com/vatesfr/xo-web/issues/917)
|
||||
- Auth: LDAP User causes problems [\#893](https://github.com/vatesfr/xo-web/issues/893)
|
||||
- No tags in Continuous Replication [\#838](https://github.com/vatesfr/xo-web/issues/838)
|
||||
- Delta backup Depth not working [\#802](https://github.com/vatesfr/xo-web/issues/802)
|
||||
- Update Section - Running version info missing - gui enhancement [\#795](https://github.com/vatesfr/xo-web/issues/795)
|
||||
- On reboot, vnc console wrongly scaled [\#722](https://github.com/vatesfr/xo-web/issues/722)
|
||||
- Make the object name \(title\) "sticky" at the top of the page [\#705](https://github.com/vatesfr/xo-web/issues/705)
|
||||
- pool view: display Local SR from hosts in the current pool [\#692](https://github.com/vatesfr/xo-web/issues/692)
|
||||
- tree view: display all IPs [\#689](https://github.com/vatesfr/xo-web/issues/689)
|
||||
- XO5 parallel distribution [\#462](https://github.com/vatesfr/xo-web/issues/462)
|
||||
- Load balancing with XO [\#423](https://github.com/vatesfr/xo-web/issues/423)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- vCPUs number when no tools installed [\#1089](https://github.com/vatesfr/xo-web/issues/1089)
|
||||
- Config Drive textbox disappears when content is deleted [\#1012](https://github.com/vatesfr/xo-web/issues/1012)
|
||||
- storage status not changed in host view page after disconnect/connect [\#1009](https://github.com/vatesfr/xo-web/issues/1009)
|
||||
- Cannot Delete Logs From Backup Overview [\#1004](https://github.com/vatesfr/xo-web/issues/1004)
|
||||
- \[v5.x\] Plugins configuration: optional non-used objects are sent [\#1000](https://github.com/vatesfr/xo-web/issues/1000)
|
||||
- "@" char in remote password break the remote view [\#997](https://github.com/vatesfr/xo-web/issues/997)
|
||||
- Handle MEMORY\_CONSTRAINT\_VIOLATION correctly [\#970](https://github.com/vatesfr/xo-web/issues/970)
|
||||
- VM creation error on XenServer Dundee [\#964](https://github.com/vatesfr/xo-web/issues/964)
|
||||
- Copy VMs doesn't display all SRs [\#945](https://github.com/vatesfr/xo-web/issues/945)
|
||||
- Autopower\_on wrong value [\#937](https://github.com/vatesfr/xo-web/issues/937)
|
||||
- Correctly handle unknown users in group view [\#900](https://github.com/vatesfr/xo-web/issues/900)
|
||||
- Importing into Dundee [\#887](https://github.com/vatesfr/xo-web/issues/887)
|
||||
- update status - gui resize issue [\#803](https://github.com/vatesfr/xo-web/issues/803)
|
||||
- Backup Remote Stores Problem [\#751](https://github.com/vatesfr/xo-web/issues/751)
|
||||
- VM view is broken when changing a disk SR twice [\#670](https://github.com/vatesfr/xo-web/issues/670)
|
||||
- console mouse sync [\#280](https://github.com/vatesfr/xo-web/issues/280)
|
||||
|
||||
## **4.16.0** (2016-04-29)
|
||||
|
||||
Maintenance release
|
||||
|
||||
### Enhancements
|
||||
|
||||
- TOO\_MANY\_PENDING\_TASKS [\#861](https://github.com/vatesfr/xo-web/issues/861)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Incorrect VM target name with continuous replication [\#904](https://github.com/vatesfr/xo-web/issues/904)
|
||||
- Error while deleting users [\#901](https://github.com/vatesfr/xo-web/issues/901)
|
||||
- Use an available path to the SR to create a config drive [\#882](https://github.com/vatesfr/xo-web/issues/882)
|
||||
- VM autoboot don't set the right pool parameter [\#879](https://github.com/vatesfr/xo-web/issues/879)
|
||||
- BUG: ACL with NFS ISO Library not working! [\#870](https://github.com/vatesfr/xo-web/issues/870)
|
||||
- Broken paths in backups in SMB [\#865](https://github.com/vatesfr/xo-web/issues/865)
|
||||
- Plugins page loads users/groups multiple times [\#829](https://github.com/vatesfr/xo-web/issues/829)
|
||||
- "Ghost" VM remains after migration [\#769](https://github.com/vatesfr/xo-web/issues/769)
|
||||
|
||||
## **4.15.0** (2016-03-21)
|
||||
|
||||
Load balancing, SMB delta support, advanced network operations...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add the job name inside the backup email report [\#819](https://github.com/vatesfr/xo-web/issues/819)
|
||||
- Delta backup with quiesce [\#812](https://github.com/vatesfr/xo-web/issues/812)
|
||||
- Hosts: No user feedback when error occurs with SR connect / disconnect [\#810](https://github.com/vatesfr/xo-web/issues/810)
|
||||
- Expose components versions [\#807](https://github.com/vatesfr/xo-web/issues/807)
|
||||
- Rework networks/PIFs management [\#805](https://github.com/vatesfr/xo-web/issues/805)
|
||||
- Displaying all SRs and a list of available hosts for creating VM from a pool [\#790](https://github.com/vatesfr/xo-web/issues/790)
|
||||
- Add "Source network" on "VM migration" screen [\#785](https://github.com/vatesfr/xo-web/issues/785)
|
||||
- Migration queue [\#783](https://github.com/vatesfr/xo-web/issues/783)
|
||||
- Match network names for VM migration [\#782](https://github.com/vatesfr/xo-web/issues/782)
|
||||
- Disk names [\#780](https://github.com/vatesfr/xo-web/issues/780)
|
||||
- Self service: should the user be able to set the CPU weight? [\#767](https://github.com/vatesfr/xo-web/issues/767)
|
||||
- host & pool Citrix license status [\#763](https://github.com/vatesfr/xo-web/issues/763)
|
||||
- pool view: Provide "updates" section [\#762](https://github.com/vatesfr/xo-web/issues/762)
|
||||
- XOA ISO image: ambigious root disk label [\#761](https://github.com/vatesfr/xo-web/issues/761)
|
||||
- Host info: provide system serial number [\#760](https://github.com/vatesfr/xo-web/issues/760)
|
||||
- CIFS ISO SR Creation [\#731](https://github.com/vatesfr/xo-web/issues/731)
|
||||
- MAC address not preserved on VM restore [\#707](https://github.com/vatesfr/xo-web/issues/707)
|
||||
- Failing replication job should send reports [\#659](https://github.com/vatesfr/xo-web/issues/659)
|
||||
- Display networks in the Pool view [\#226](https://github.com/vatesfr/xo-web/issues/226)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Broken link to backup remote [\#821](https://github.com/vatesfr/xo-web/issues/821)
|
||||
- Issue with self-signed cert for email plugin [\#817](https://github.com/vatesfr/xo-web/issues/817)
|
||||
- Plugins view, reset form and errors [\#815](https://github.com/vatesfr/xo-web/issues/815)
|
||||
- HVM recovery mode is broken [\#794](https://github.com/vatesfr/xo-web/issues/794)
|
||||
- Disk bug when creating vm from template [\#778](https://github.com/vatesfr/xo-web/issues/778)
|
||||
- Can't mount NFS shares in remote stores [\#775](https://github.com/vatesfr/xo-web/issues/775)
|
||||
- VM disk name and description not passed during creation [\#774](https://github.com/vatesfr/xo-web/issues/774)
|
||||
- NFS mount problem for Windows share [\#771](https://github.com/vatesfr/xo-web/issues/771)
|
||||
- lodash.pluck not installed [\#757](https://github.com/vatesfr/xo-web/issues/757)
|
||||
- this.\_getAuthenticationTokensForUser is not a function [\#755](https://github.com/vatesfr/xo-web/issues/755)
|
||||
- CentOS 6.x 64bit template creates a VM that won't boot [\#733](https://github.com/vatesfr/xo-web/issues/733)
|
||||
- Lot of xo:perf leading to XO crash [\#575](https://github.com/vatesfr/xo-web/issues/575)
|
||||
- New collection checklist [\#262](https://github.com/vatesfr/xo-web/issues/262)
|
||||
|
||||
## **4.14.0** (2016-02-23)
|
||||
|
||||
Self service, custom CloudInit...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- VM creation self service with quotas [\#285](https://github.com/vatesfr/xo-web/issues/285)
|
||||
- Cloud config custom user data [\#706](https://github.com/vatesfr/xo-web/issues/706)
|
||||
- Patches behind a proxy [\#737](https://github.com/vatesfr/xo-web/issues/737)
|
||||
- Remote store status indicator [\#728](https://github.com/vatesfr/xo-web/issues/728)
|
||||
- Patch list order [\#724](https://github.com/vatesfr/xo-web/issues/724)
|
||||
- Enable reporting on additional backup types [\#717](https://github.com/vatesfr/xo-web/issues/717)
|
||||
- Tooltip name for cancel [\#703](https://github.com/vatesfr/xo-web/issues/703)
|
||||
- Portable VHD merging [\#646](https://github.com/vatesfr/xo-web/issues/646)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid merge between two delta vdi backups [\#702](https://github.com/vatesfr/xo-web/issues/702)
|
||||
- Text in table is not cut anymore [\#713](https://github.com/vatesfr/xo-web/issues/713)
|
||||
- Disk size edition issue with float numbers [\#719](https://github.com/vatesfr/xo-web/issues/719)
|
||||
- Create vm, summary is not refreshed [\#721](https://github.com/vatesfr/xo-web/issues/721)
|
||||
- Boot order problem [\#726](https://github.com/vatesfr/xo-web/issues/726)
|
||||
|
||||
## **4.13.0** (2016-02-05)
|
||||
|
||||
Backup checksum, SMB remotes...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Add SMB mount for remote [\#338](https://github.com/vatesfr/xo-web/issues/338)
|
||||
- Centralize Perm in a lib [\#345](https://github.com/vatesfr/xo-web/issues/345)
|
||||
- Expose interpool migration details [\#567](https://github.com/vatesfr/xo-web/issues/567)
|
||||
- Add checksum for delta backup [\#617](https://github.com/vatesfr/xo-web/issues/617)
|
||||
- Redirect from HTTP to HTTPS [\#626](https://github.com/vatesfr/xo-web/issues/626)
|
||||
- Expose vCPU weight [\#633](https://github.com/vatesfr/xo-web/issues/633)
|
||||
- Avoid metadata in delta backup [\#651](https://github.com/vatesfr/xo-web/issues/651)
|
||||
- Button to clear logs [\#661](https://github.com/vatesfr/xo-web/issues/661)
|
||||
- Units for RAM and disks [\#666](https://github.com/vatesfr/xo-web/issues/666)
|
||||
- Remove multiple VDIs at once [\#676](https://github.com/vatesfr/xo-web/issues/676)
|
||||
- Find orphaned VDI snapshots [\#679](https://github.com/vatesfr/xo-web/issues/679)
|
||||
- New health view in Dashboard [\#680](https://github.com/vatesfr/xo-web/issues/680)
|
||||
- Use physical usage for VDI and SR [\#682](https://github.com/vatesfr/xo-web/issues/682)
|
||||
- TLS configuration [\#685](https://github.com/vatesfr/xo-web/issues/685)
|
||||
- Better VM info on tree view [\#688](https://github.com/vatesfr/xo-web/issues/688)
|
||||
- Absolute values in tooltips for tree view [\#690](https://github.com/vatesfr/xo-web/issues/690)
|
||||
- Absolute values for host memory [\#691](https://github.com/vatesfr/xo-web/issues/691)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Issues on host console screen [\#672](https://github.com/vatesfr/xo-web/issues/672)
|
||||
- NFS remote mount fails in particular case [\#665](https://github.com/vatesfr/xo-web/issues/665)
|
||||
- Unresponsive pages [\#662](https://github.com/vatesfr/xo-web/issues/662)
|
||||
- Live migration fail in the same pool with local SR fails [\#655](https://github.com/vatesfr/xo-web/issues/655)
|
||||
|
||||
## **4.12.0** (2016-01-18)
|
||||
|
||||
Continuous Replication, Continuous Delta backup...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Continuous VM replication [\#582](https://github.com/vatesfr/xo-web/issues/582)
|
||||
- Continuous Delta Backup [\#576](https://github.com/vatesfr/xo-web/issues/576)
|
||||
- Scheduler should not run job again if previous instance is not finished [\#642](https://github.com/vatesfr/xo-web/issues/642)
|
||||
- Boot VM automatically after creation [\#635](https://github.com/vatesfr/xo-web/issues/635)
|
||||
- Manage existing VIFs in templates [\#630](https://github.com/vatesfr/xo-web/issues/630)
|
||||
- Support templates with existing install repository [\#627](https://github.com/vatesfr/xo-web/issues/627)
|
||||
- Remove running VMs [\#616](https://github.com/vatesfr/xo-web/issues/616)
|
||||
- Prevent a VM to start before delta import is finished [\#613](https://github.com/vatesfr/xo-web/issues/613)
|
||||
- Spawn multiple VMs at once [\#606](https://github.com/vatesfr/xo-web/issues/606)
|
||||
- Fixed `suspendVM` in tree view. [\#619](https://github.com/vatesfr/xo-web/pull/619) ([pdonias](https://github.com/pdonias))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- User defined MAC address is not fetch in VM install [\#643](https://github.com/vatesfr/xo-web/issues/643)
|
||||
- CoreOsCloudConfig is not shown with CoreOS [\#639](https://github.com/vatesfr/xo-web/issues/639)
|
||||
- Plugin activation/deactivation in web UI seems broken [\#637](https://github.com/vatesfr/xo-web/issues/637)
|
||||
- Issue when creating CloudConfig drive [\#636](https://github.com/vatesfr/xo-web/issues/636)
|
||||
- CloudConfig hostname shouldn't have space [\#634](https://github.com/vatesfr/xo-web/issues/634)
|
||||
- Cloned VIFs are not properly deleted on VM creation [\#632](https://github.com/vatesfr/xo-web/issues/632)
|
||||
- Default PV args missing during VM creation [\#628](https://github.com/vatesfr/xo-web/issues/628)
|
||||
- VM creation problems from custom templates [\#625](https://github.com/vatesfr/xo-web/issues/625)
|
||||
- Emergency shutdown race condition [\#622](https://github.com/vatesfr/xo-web/issues/622)
|
||||
- `vm.delete\(\)` should not delete VDIs attached to other VMs [\#621](https://github.com/vatesfr/xo-web/issues/621)
|
||||
- VM creation error from template with a disk [\#581](https://github.com/vatesfr/xo-web/issues/581)
|
||||
- Only delete VDI exports when VM backup is successful [\#644](https://github.com/vatesfr/xo-web/issues/644)
|
||||
- Change the name of an imported VM during the import process [\#641](https://github.com/vatesfr/xo-web/issues/641)
|
||||
- Creating a new VIF in view is partially broken [\#652](https://github.com/vatesfr/xo-web/issues/652)
|
||||
- Grey out the "create button" during VM creation [\#654](https://github.com/vatesfr/xo-web/issues/654)
|
||||
|
||||
## **4.11.0** (2015-12-22)
|
||||
|
||||
Delta backup, CloudInit...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Visible list of SR inside a VM [\#601](https://github.com/vatesfr/xo-web/issues/601)
|
||||
- VDI move [\#591](https://github.com/vatesfr/xo-web/issues/591)
|
||||
- Edit pre-existing disk configuration during VM creation [\#589](https://github.com/vatesfr/xo-web/issues/589)
|
||||
- Allow disk size edition [\#587](https://github.com/vatesfr/xo-web/issues/587)
|
||||
- Better VDI resize support [\#585](https://github.com/vatesfr/xo-web/issues/585)
|
||||
- Remove manual VM export metadata in UI [\#580](https://github.com/vatesfr/xo-web/issues/580)
|
||||
- Support import VM metadata [\#579](https://github.com/vatesfr/xo-web/issues/579)
|
||||
- Set a default pool SR [\#572](https://github.com/vatesfr/xo-web/issues/572)
|
||||
- ISOs should be sorted by name [\#565](https://github.com/vatesfr/xo-web/issues/565)
|
||||
- Button to boot a VM from a disc once [\#564](https://github.com/vatesfr/xo-web/issues/564)
|
||||
- Ability to boot a PV VM from a disc [\#563](https://github.com/vatesfr/xo-web/issues/563)
|
||||
- Add an option to manually run backup jobs [\#562](https://github.com/vatesfr/xo-web/issues/562)
|
||||
- backups to unmounted storage [\#561](https://github.com/vatesfr/xo-web/issues/561)
|
||||
- Root integer properties cannot be edited in plugins configuration form [\#550](https://github.com/vatesfr/xo-web/issues/550)
|
||||
- Generic CloudConfig drive [\#549](https://github.com/vatesfr/xo-web/issues/549)
|
||||
- Auto-discovery of installed xo-server plugins [\#546](https://github.com/vatesfr/xo-web/issues/546)
|
||||
- Hide info on flat view [\#545](https://github.com/vatesfr/xo-web/issues/545)
|
||||
- Config plugin boolean properties must have a default value \(undefined prohibited\) [\#543](https://github.com/vatesfr/xo-web/issues/543)
|
||||
- Present detailed errors on plugin configuration failures [\#530](https://github.com/vatesfr/xo-web/issues/530)
|
||||
- Do not reset form on failures in plugins configuration [\#529](https://github.com/vatesfr/xo-web/issues/529)
|
||||
- XMPP alert plugin [\#518](https://github.com/vatesfr/xo-web/issues/518)
|
||||
- Hide tag adders depending on ACLs [\#516](https://github.com/vatesfr/xo-web/issues/516)
|
||||
- Choosing a framework for xo-web 5 [\#514](https://github.com/vatesfr/xo-web/issues/514)
|
||||
- Prevent adding a host in an existing XAPI connection [\#466](https://github.com/vatesfr/xo-web/issues/466)
|
||||
- Read only connection to Xen servers/pools [\#439](https://github.com/vatesfr/xo-web/issues/439)
|
||||
- generic notification system [\#391](https://github.com/vatesfr/xo-web/issues/391)
|
||||
- Data architecture review [\#384](https://github.com/vatesfr/xo-web/issues/384)
|
||||
- Make filtering easier to understand/add some "default" filters [\#207](https://github.com/vatesfr/xo-web/issues/207)
|
||||
- Improve performance [\#148](https://github.com/vatesfr/xo-web/issues/148)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- VM metadata export should not require a snapshot [\#615](https://github.com/vatesfr/xo-web/issues/615)
|
||||
- Missing patch for all hosts is continuously refreshed [\#609](https://github.com/vatesfr/xo-web/issues/609)
|
||||
- Backup import memory issue [\#608](https://github.com/vatesfr/xo-web/issues/608)
|
||||
- Host list missing patch is buggy [\#604](https://github.com/vatesfr/xo-web/issues/604)
|
||||
- Servers infos should not been refreshed while a field is being edited [\#595](https://github.com/vatesfr/xo-web/issues/595)
|
||||
- Servers list should not been re-order while a field is being edited [\#594](https://github.com/vatesfr/xo-web/issues/594)
|
||||
- Correctly display size in interface \(binary scale\) [\#592](https://github.com/vatesfr/xo-web/issues/592)
|
||||
- Display failures on VM boot order modification [\#560](https://github.com/vatesfr/xo-web/issues/560)
|
||||
- `vm.setBootOrder\(\)` should throw errors on failures \(non-HVM VMs\) [\#559](https://github.com/vatesfr/xo-web/issues/559)
|
||||
- Hide boot order form for non-HVM VMs [\#558](https://github.com/vatesfr/xo-web/issues/558)
|
||||
- Allow editing PV args even when empty \(but only for PV VMs\) [\#557](https://github.com/vatesfr/xo-web/issues/557)
|
||||
- Crashes when using legacy event system [\#556](https://github.com/vatesfr/xo-web/issues/556)
|
||||
- XenServer patches check error for 6.1 [\#555](https://github.com/vatesfr/xo-web/issues/555)
|
||||
- activation plugin xo-server-transport-email [\#553](https://github.com/vatesfr/xo-web/issues/553)
|
||||
- Server error with JSON on 32 bits Dom0 [\#552](https://github.com/vatesfr/xo-web/issues/552)
|
||||
- Cloud Config drive shouldn't be created on default SR [\#548](https://github.com/vatesfr/xo-web/issues/548)
|
||||
- Deep properties cannot be edited in plugins configuration form [\#521](https://github.com/vatesfr/xo-web/issues/521)
|
||||
- Aborted VM export should cancel the operation [\#490](https://github.com/vatesfr/xo-web/issues/490)
|
||||
- VM missing with same UUID after an inter-pool migration [\#284](https://github.com/vatesfr/xo-web/issues/284)
|
||||
|
||||
## **4.10.0** (2015-11-27)
|
||||
|
||||
Job management, email notifications, CoreOS/Docker, Quiesce snapshots...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Job management ([xo-web#487](https://github.com/vatesfr/xo-web/issues/487))
|
||||
- Patch upload on all connected servers ([xo-web#168](https://github.com/vatesfr/xo-web/issues/168))
|
||||
- Emergency shutdown ([xo-web#185](https://github.com/vatesfr/xo-web/issues/185))
|
||||
- CoreOS/docker template install ([xo-web#246](https://github.com/vatesfr/xo-web/issues/246))
|
||||
- Email for backups ([xo-web#308](https://github.com/vatesfr/xo-web/issues/308))
|
||||
- Console Clipboard ([xo-web#408](https://github.com/vatesfr/xo-web/issues/408))
|
||||
- Logs from CLI ([xo-web#486](https://github.com/vatesfr/xo-web/issues/486))
|
||||
- Save disconnected servers ([xo-web#489](https://github.com/vatesfr/xo-web/issues/489))
|
||||
- Snapshot with quiesce ([xo-web#491](https://github.com/vatesfr/xo-web/issues/491))
|
||||
- Start VM in reovery mode ([xo-web#495](https://github.com/vatesfr/xo-web/issues/495))
|
||||
- Username in logs ([xo-web#498](https://github.com/vatesfr/xo-web/issues/498))
|
||||
- Delete associated tokens with user ([xo-web#500](https://github.com/vatesfr/xo-web/issues/500))
|
||||
- Validate plugin configuration ([xo-web#503](https://github.com/vatesfr/xo-web/issues/503))
|
||||
- Avoid non configured plugins to be loaded ([xo-web#504](https://github.com/vatesfr/xo-web/issues/504))
|
||||
- Verbose API logs if configured ([xo-web#505](https://github.com/vatesfr/xo-web/issues/505))
|
||||
- Better backup overview ([xo-web#512](https://github.com/vatesfr/xo-web/issues/512))
|
||||
- VM auto power on ([xo-web#519](https://github.com/vatesfr/xo-web/issues/519))
|
||||
- Title property supported in config schema ([xo-web#522](https://github.com/vatesfr/xo-web/issues/522))
|
||||
- Start VM export only when necessary ([xo-web#534](https://github.com/vatesfr/xo-web/issues/534))
|
||||
- Input type should be number ([xo-web#538](https://github.com/vatesfr/xo-web/issues/538))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Numbers/int support in plugins config ([xo-web#531](https://github.com/vatesfr/xo-web/issues/531))
|
||||
- Boolean support in plugins config ([xo-web#528](https://github.com/vatesfr/xo-web/issues/528))
|
||||
- Keyboard unusable outside console ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
|
||||
- UsernameField for SAML ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
|
||||
- Wrong display of "no plugin found" ([xo-web#508](https://github.com/vatesfr/xo-web/issues/508))
|
||||
- Bower build error ([xo-web#488](https://github.com/vatesfr/xo-web/issues/488))
|
||||
- VM cloning should require SR permission ([xo-web#472](https://github.com/vatesfr/xo-web/issues/472))
|
||||
- Xen tools status ([xo-web#471](https://github.com/vatesfr/xo-web/issues/471))
|
||||
- Can't delete ghost user ([xo-web#464](https://github.com/vatesfr/xo-web/issues/464))
|
||||
- Stats with old versions of Node ([xo-web#463](https://github.com/vatesfr/xo-web/issues/463))
|
||||
|
||||
## **4.9.0** (2015-11-13)
|
||||
|
||||
Automated DR, restore backup, VM copy
|
||||
|
||||
28
ISSUE_TEMPLATE.md
Normal file
28
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,28 @@
|
||||
<!--
|
||||
Welcome to the issue section of Xen Orchestra!
|
||||
|
||||
Here you can:
|
||||
- report an issue
|
||||
- propose an enhancement
|
||||
- ask a question
|
||||
|
||||
The template below is only a proposition for your ticket, feel free to
|
||||
change it as appropriate :)
|
||||
-->
|
||||
|
||||
### Context
|
||||
|
||||
- **XO version**: XO appliance / `stable` branch / `next-release` branch
|
||||
|
||||
If from the sources:
|
||||
|
||||
- **Component**: xo-web / xo-server / *unknown*
|
||||
- **Node/npm version**: *just execute `npm version`*
|
||||
|
||||
### Expected behavior
|
||||
|
||||
<!-- What you expect to happen -->
|
||||
|
||||
### Current behavior
|
||||
|
||||
<!-- What is actually happening -->
|
||||
42
README.md
42
README.md
@@ -1,4 +1,4 @@
|
||||
# Xen Orchestra Web
|
||||
# Xen Orchestra Web [](https://travis-ci.org/vatesfr/xo-web)
|
||||
|
||||

|
||||
|
||||
@@ -6,14 +6,11 @@ XO-Web is part of [Xen Orchestra](https://github.com/vatesfr/xo), a web interfac
|
||||
|
||||
It is a web client for [XO-Server](https://github.com/vatesfr/xo-server).
|
||||
|
||||
[](https://david-dm.org/vatesfr/xo-web)
|
||||
[](https://david-dm.org/vatesfr/xo-web#info=devDependencies)
|
||||
|
||||
___
|
||||
|
||||
## Installation
|
||||
|
||||
XOA or manual install procedure is [available here](https://github.com/vatesfr/xo/blob/master/doc/installation/README.md)
|
||||
XOA or manual install procedure is [available here](https://xen-orchestra.com/docs/installation.html)
|
||||
|
||||
## Compilation
|
||||
|
||||
@@ -29,6 +26,31 @@ Development build:
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
### Environment
|
||||
|
||||
#### `NODE_ENV`
|
||||
|
||||
Set to *production* it disables many checks which result in increased
|
||||
performance.
|
||||
|
||||
#### `XOA_PLAN`
|
||||
|
||||
- 1: Free
|
||||
- 2: Starter
|
||||
- 3: Enterprise
|
||||
- 4: Premium
|
||||
- 5: Sources
|
||||
|
||||
```js
|
||||
if (process.env.XOA_PLAN < 5) {
|
||||
console.log('included only in XOA')
|
||||
}
|
||||
|
||||
if (process.env.XOA_PLAN > 3) {
|
||||
console.log('included only in Premium and Sources')
|
||||
}
|
||||
```
|
||||
|
||||
## How to report a bug?
|
||||
|
||||
If you are certain the bug is exclusively related to XO-Web, you may use the [bugtracker of this repository](https://github.com/vatesfr/xo-web/issues).
|
||||
@@ -38,8 +60,8 @@ Otherwise, please consider using the [bugtracker of the general repository](http
|
||||
## Process for new release
|
||||
|
||||
```bash
|
||||
# Switch to the master branch.
|
||||
git checkout master
|
||||
# Switch to the stable branch.
|
||||
git checkout stable
|
||||
|
||||
# Fetches latest changes.
|
||||
git pull --ff-only
|
||||
@@ -53,12 +75,12 @@ npm version minor
|
||||
# Go back to the next-release branch.
|
||||
git checkout next-release
|
||||
|
||||
# Fetches the last changes (the merge and version bump) from master to
|
||||
# Fetches the last changes (the merge and version bump) from stable to
|
||||
# next-release.
|
||||
git merge --ff-only master
|
||||
git merge --ff-only stable
|
||||
|
||||
# Push the changes on git.
|
||||
git push --follow-tags origin master next-release
|
||||
git push --follow-tags origin stable next-release
|
||||
|
||||
# Publish this release to npm.
|
||||
npm publish
|
||||
|
||||
151
gulpfile.js
151
gulpfile.js
@@ -2,8 +2,8 @@
|
||||
|
||||
// ===================================================================
|
||||
|
||||
var SRC_DIR = __dirname + '/src'
|
||||
var DIST_DIR = __dirname + '/dist'
|
||||
var SRC_DIR = __dirname + '/src' // eslint-disable-line no-path-concat
|
||||
var DIST_DIR = __dirname + '/dist' // eslint-disable-line no-path-concat
|
||||
|
||||
// Port to use for the livereload server.
|
||||
//
|
||||
@@ -11,20 +11,13 @@ var DIST_DIR = __dirname + '/dist'
|
||||
// http://www.random.org/integers/?num=1&min=1024&max=65535&col=1&base=10&format=plain&rnd=new
|
||||
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
|
||||
|
||||
// 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 PRODUCTION = process.argv.indexOf('--production') !== -1
|
||||
var PRODUCTION = process.env.NODE_ENV === 'production'
|
||||
var DEVELOPMENT = !PRODUCTION
|
||||
|
||||
if (!process.env.XOA_PLAN) {
|
||||
process.env.XOA_PLAN = '5' // Open Source
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
var gulp = require('gulp')
|
||||
@@ -45,20 +38,47 @@ function lazyFn (factory) {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var livereload = lazyFn(function () {
|
||||
var livereload = require('gulp-livereload')
|
||||
livereload.listen(LIVERELOAD_PORT)
|
||||
var livereload = require('gulp-refresh')
|
||||
livereload.listen({
|
||||
port: 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 current
|
||||
function pipeCore (streams) {
|
||||
var i, n, stream
|
||||
for (i = 0, n = streams.length; i < n; ++i) {
|
||||
stream = streams[i]
|
||||
if (!stream) {
|
||||
// Nothing to do
|
||||
} else if (stream instanceof Array) {
|
||||
pipeCore(stream)
|
||||
} else {
|
||||
current = current
|
||||
? current.pipe(stream)
|
||||
: stream
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var push = Array.prototype.push
|
||||
return function (streams) {
|
||||
try {
|
||||
if (!(streams instanceof Array)) {
|
||||
streams = []
|
||||
push.apply(streams, arguments)
|
||||
}
|
||||
|
||||
pipeCore(streams)
|
||||
|
||||
return current
|
||||
} finally {
|
||||
current = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var resolvePath = lazyFn(function () {
|
||||
@@ -77,23 +97,25 @@ var src = lazyFn(function () {
|
||||
}
|
||||
|
||||
return PRODUCTION
|
||||
? function src (pattern, base) {
|
||||
base = resolve(base)
|
||||
? function src (pattern, opts) {
|
||||
var base = resolve(opts && opts.base)
|
||||
|
||||
return gulp.src(pattern, {
|
||||
base: base,
|
||||
cwd: base,
|
||||
sourcemaps: true
|
||||
passthrough: opts && opts.passthrough,
|
||||
sourcemaps: opts && opts.sourcemaps
|
||||
})
|
||||
}
|
||||
: function src (pattern, base) {
|
||||
base = resolve(base)
|
||||
: function src (pattern, opts) {
|
||||
var base = resolve(opts && opts.base)
|
||||
|
||||
return pipe(
|
||||
gulp.src(pattern, {
|
||||
base: base,
|
||||
cwd: base,
|
||||
sourcemaps: true
|
||||
passthrough: opts && opts.passthrough,
|
||||
sourcemaps: opts && opts.sourcemaps
|
||||
}),
|
||||
require('gulp-watch')(pattern, {
|
||||
base: base,
|
||||
@@ -125,10 +147,9 @@ var dest = lazyFn(function () {
|
||||
return gulp.dest(resolve(path), opts)
|
||||
}
|
||||
: function dest (path) {
|
||||
return pipe(
|
||||
gulp.dest(resolve(path), opts),
|
||||
livereload()
|
||||
)
|
||||
var stream = gulp.dest(resolve(path), opts)
|
||||
stream.pipe(livereload())
|
||||
return stream
|
||||
}
|
||||
})
|
||||
|
||||
@@ -141,9 +162,10 @@ function browserify (path, opts) {
|
||||
|
||||
var bundler = require('browserify')(path, {
|
||||
basedir: SRC_DIR,
|
||||
debug: DEVELOPMENT,
|
||||
debug: DEVELOPMENT, // TODO: enable also in production but need to make it work with gulp-uglify.
|
||||
extensions: opts.extensions,
|
||||
fullPaths: DEVELOPMENT,
|
||||
fullPaths: false,
|
||||
paths: SRC_DIR + '/common',
|
||||
standalone: opts.standalone,
|
||||
|
||||
// Required by Watchify.
|
||||
@@ -151,8 +173,15 @@ function browserify (path, opts) {
|
||||
packageCache: {}
|
||||
})
|
||||
|
||||
var plugins = opts.plugins
|
||||
for (var i = 0, n = plugins && plugins.length; i < n; ++i) {
|
||||
var plugin = plugins[i]
|
||||
bundler.plugin(require(plugin[0]), plugin[1])
|
||||
}
|
||||
|
||||
if (PRODUCTION) {
|
||||
bundler.plugin('bundle-collapser/plugin')
|
||||
// FIXME: does not work with react-intl (?!)
|
||||
// bundler.plugin('bundle-collapser/plugin')
|
||||
} else {
|
||||
bundler = require('watchify')(bundler)
|
||||
}
|
||||
@@ -209,7 +238,7 @@ function browserify (path, opts) {
|
||||
|
||||
gulp.task(function buildPages () {
|
||||
return pipe(
|
||||
src('index.jade'),
|
||||
src('index.jade', { sourcemaps: true }),
|
||||
require('gulp-jade')(),
|
||||
DEVELOPMENT && require('gulp-embedlr')({
|
||||
port: LIVERELOAD_PORT
|
||||
@@ -220,17 +249,22 @@ gulp.task(function buildPages () {
|
||||
|
||||
gulp.task(function buildScripts () {
|
||||
return pipe(
|
||||
browserify('./index.js'),
|
||||
PRODUCTION && require('gulp-uglify')({
|
||||
mangle: false // Avoid breaking Angular deps injector.
|
||||
browserify('./index.js', {
|
||||
plugins: [
|
||||
// ['css-modulesify', {
|
||||
['modular-css/browserify', {
|
||||
css: DIST_DIR + '/modules.css'
|
||||
}]
|
||||
]
|
||||
}),
|
||||
PRODUCTION && require('gulp-uglify')(),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
|
||||
gulp.task(function buildStyles () {
|
||||
return pipe(
|
||||
src('index.scss'),
|
||||
src('index.scss', { sourcemaps: true }),
|
||||
require('gulp-sass')(),
|
||||
require('gulp-autoprefixer')([
|
||||
'last 1 version',
|
||||
@@ -244,10 +278,14 @@ gulp.task(function buildStyles () {
|
||||
gulp.task(function copyAssets () {
|
||||
return pipe(
|
||||
src(['assets/**/*', 'favicon.*']),
|
||||
src(
|
||||
'fontawesome-webfont.*',
|
||||
__dirname + '/node_modules/font-awesome/fonts'
|
||||
),
|
||||
src('fontawesome-webfont.*', {
|
||||
base: __dirname + '/node_modules/font-awesome/fonts', // eslint-disable-line no-path-concat
|
||||
passthrough: true
|
||||
}),
|
||||
src(['!*.css', 'font-mfizz.*'], {
|
||||
base: __dirname + '/node_modules/font-mfizz/dist', // eslint-disable-line no-path-concat
|
||||
passthrough: true
|
||||
}),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
@@ -264,28 +302,3 @@ gulp.task('build', gulp.parallel(
|
||||
gulp.task(function clean (done) {
|
||||
require('rimraf')(DIST_DIR, done)
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
gulp.task(function server (done) {
|
||||
require('connect')()
|
||||
.use(require('serve-static')(DIST_DIR))
|
||||
.listen(SERVER_PORT, SERVER_ADDR, function onListen () {
|
||||
var address = this.address()
|
||||
|
||||
var port = address.port
|
||||
address = address.address
|
||||
|
||||
// Correctly handle IPv6 addresses.
|
||||
if (address.indexOf(':') !== -1) {
|
||||
address = '[' + address + ']'
|
||||
}
|
||||
|
||||
/* jshint devel: true*/
|
||||
console.log('Listening on http://' + address + ':' + port)
|
||||
})
|
||||
.on('error', done)
|
||||
.on('close', function onClose () {
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
181
package.json
181
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.0.0",
|
||||
"version": "5.2.5",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -27,81 +27,164 @@
|
||||
"dist/"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
"node": ">=4",
|
||||
"npm": ">=3"
|
||||
},
|
||||
"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",
|
||||
"ansi_up": "^1.3.0",
|
||||
"asap": "^2.0.4",
|
||||
"ava": "^0.16.0",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-react-constant-elements": "^6.5.0",
|
||||
"babel-plugin-transform-react-inline-elements": "^6.6.5",
|
||||
"babel-plugin-transform-react-jsx-self": "^6.11.0",
|
||||
"babel-plugin-transform-react-jsx-source": "^6.9.0",
|
||||
"babel-plugin-transform-runtime": "^6.6.0",
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"babel-runtime": "^6.6.1",
|
||||
"babelify": "^7.2.0",
|
||||
"benchmark": "^2.1.0",
|
||||
"bootstrap": "github:twbs/bootstrap#v4-dev",
|
||||
"browserify": "^13.0.0",
|
||||
"browserify-plain-jade": "^0.2.2",
|
||||
"bundle-collapser": "^1.2.1",
|
||||
"clarify": "^1.0.5",
|
||||
"connect": "^3.4.0",
|
||||
"chartist-plugin-legend": "^0.5.0",
|
||||
"chartist-plugin-tooltip": "0.0.11",
|
||||
"classnames": "^2.2.3",
|
||||
"cookies-js": "^1.2.2",
|
||||
"d3": "^4.0.0-alpha.50",
|
||||
"dependency-check": "^2.5.1",
|
||||
"event-to-promise": "^0.7.0",
|
||||
"font-awesome": "^4.5.0",
|
||||
"font-mfizz": "github:fizzed/font-mfizz",
|
||||
"get-stream": "^2.3.0",
|
||||
"ghooks": "^1.1.1",
|
||||
"globby": "^6.0.0",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-autoprefixer": "^3.1.0",
|
||||
"gulp-csso": "^1.0.1",
|
||||
"gulp-csso": "^2.0.0",
|
||||
"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-plumber": "^1.1.0",
|
||||
"gulp-refresh": "^1.1.0",
|
||||
"gulp-sass": "^2.2.0",
|
||||
"gulp-uglify": "^2.0.0",
|
||||
"gulp-watch": "^4.3.5",
|
||||
"history": "^2.0.0-rc2",
|
||||
"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",
|
||||
"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",
|
||||
"human-format": "^0.6.0",
|
||||
"index-modules": "0.0.0",
|
||||
"is-ip": "^1.0.0",
|
||||
"jsonrpc-websocket-client": "0.0.1-5",
|
||||
"later": "^1.2.0",
|
||||
"lodash": "^4.6.1",
|
||||
"loose-envify": "^1.1.0",
|
||||
"make-error": "^1.2.1",
|
||||
"marked": "^0.3.5",
|
||||
"modular-css": "^0.27.1",
|
||||
"moment": "^2.13.0",
|
||||
"moment-timezone": "^0.5.4",
|
||||
"notifyjs": "^2.0.1",
|
||||
"novnc-node": "^0.5.3",
|
||||
"promise-toolbox": "^0.5.0",
|
||||
"random-password": "^0.1.2",
|
||||
"react": "^15.0.0",
|
||||
"react-addons-shallow-compare": "^15.1.0",
|
||||
"react-bootstrap-4": "^0.29.1",
|
||||
"react-chartist": "^0.10.1",
|
||||
"react-copy-to-clipboard": "^4.0.2",
|
||||
"react-debounce-input": "^2.4.0",
|
||||
"react-dnd": "^2.1.4",
|
||||
"react-dnd-html5-backend": "^2.1.2",
|
||||
"react-document-title": "^2.0.2",
|
||||
"react-dom": "^15.0.0",
|
||||
"react-dropzone": "^3.5.0",
|
||||
"react-intl": "^2.0.1",
|
||||
"react-key-handler": "^0.3.0",
|
||||
"react-notify": "^2.0.1",
|
||||
"react-redux": "^4.4.0",
|
||||
"react-router": "^3.0.0-alpha.1",
|
||||
"react-select": "^1.0.0-beta13",
|
||||
"react-shortcuts": "^1.0.7",
|
||||
"react-sparklines": "^1.5.0",
|
||||
"react-virtualized": "^8.0.8",
|
||||
"readable-stream": "^2.0.6",
|
||||
"redux": "^3.3.1",
|
||||
"redux-devtools": "^3.1.1",
|
||||
"redux-devtools-dock-monitor": "^1.1.0",
|
||||
"redux-devtools-log-monitor": "^1.0.5",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.2.1",
|
||||
"standard": "^8.2.0",
|
||||
"superagent": "^2.0.0",
|
||||
"tar-stream": "^1.5.2",
|
||||
"vinyl": "^2.0.0",
|
||||
"watchify": "^3.7.0",
|
||||
"xo-lib": "^0.8.0-1"
|
||||
"xml2js": "^0.4.17",
|
||||
"xo-acl-resolver": "^0.2.2",
|
||||
"xo-lib": "^0.8.0",
|
||||
"xo-remote-parser": "^0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp build --production",
|
||||
"dev": "gulp build server",
|
||||
"dev-test": "mocha --opts .mocha.opts --watch --reporter=min \"dist/**/*.spec.js\"",
|
||||
"benchmarks": "./tools/run-benchmarks.js 'src/**/*.bench.js'",
|
||||
"build": "npm run build-indexes && NODE_ENV=production gulp build",
|
||||
"build-indexes": "index-modules --auto src",
|
||||
"dev": "npm run build-indexes && NODE_ENV=development gulp build",
|
||||
"dev-test": "ava --watch",
|
||||
"lint": "standard",
|
||||
"depcheck": "dependency-check ./package.json",
|
||||
"posttest": "npm run lint && npm run depcheck",
|
||||
"posttest": "npm run lint",
|
||||
"prepublish": "npm run build",
|
||||
"test": "nyc mocha --opts .mocha.opts \"dist/**/*.spec.js\""
|
||||
},
|
||||
"browser": {
|
||||
"node_modules/ws/index.js": "./ws.js"
|
||||
"test": "ava"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify",
|
||||
"browserify-plain-jade"
|
||||
"loose-envify"
|
||||
]
|
||||
},
|
||||
"ava": {
|
||||
"babel": "inherit",
|
||||
"files": [
|
||||
"src/**/*.spec.js"
|
||||
],
|
||||
"require": [
|
||||
"babel-register"
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"env": {
|
||||
"development": {
|
||||
"plugins": [
|
||||
"transform-react-jsx-self",
|
||||
"transform-react-jsx-source"
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"plugins": [
|
||||
"transform-react-constant-elements",
|
||||
"transform-react-inline-elements"
|
||||
]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"transform-decorators-legacy",
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
"es2015",
|
||||
"react",
|
||||
"stage-0"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"ghooks": {
|
||||
"commit-msg": "npm test"
|
||||
}
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.3.19",
|
||||
"redux-promise": "^0.5.1"
|
||||
}
|
||||
}
|
||||
|
||||
1
src/assets/loading.svg
Normal file
1
src/assets/loading.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><svg width='62px' height='62px' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="uil-ring-alt"><rect x="0" y="0" width="100" height="100" fill="none" class="bk"></rect><circle cx="50" cy="50" r="40" stroke="#cfcfcf" fill="none" stroke-width="10" stroke-linecap="round"></circle><circle cx="50" cy="50" r="40" stroke="#366e98" fill="none" stroke-width="6" stroke-linecap="round"><animate attributeName="stroke-dashoffset" dur="1s" repeatCount="indefinite" from="0" to="502"></animate><animate attributeName="stroke-dasharray" dur="1s" repeatCount="indefinite" values="150.6 100.4;1 250;150.6 100.4"></animate></circle></svg>
|
||||
|
After Width: | Height: | Size: 707 B |
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
126
src/chartist.scss
Normal file
126
src/chartist.scss
Normal file
@@ -0,0 +1,126 @@
|
||||
|
||||
// CHARTIST ===================================================================
|
||||
|
||||
$ct-series-colors: (
|
||||
$brand-success,
|
||||
$brand-primary,
|
||||
#f17cb0,
|
||||
#86797d,
|
||||
#b276b2,
|
||||
#f15854,
|
||||
#b2912f,
|
||||
#decf3f,
|
||||
#dda458,
|
||||
#60bd68,
|
||||
#4d4d4d,
|
||||
#eacf7d,
|
||||
#b2c326,
|
||||
#6188e2,
|
||||
#a748ca
|
||||
) !default;
|
||||
|
||||
@import "../node_modules/chartist/dist/scss/settings/_chartist-settings";
|
||||
@import "../node_modules/chartist/dist/scss/chartist";
|
||||
|
||||
.ct-chart {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
// Line in charts with only 2px in width
|
||||
.ct-line {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.ct-bar {
|
||||
stroke-width: 10%;
|
||||
}
|
||||
|
||||
.ct-point {
|
||||
stroke-width: 30px;
|
||||
stroke-opacity: 0!important;
|
||||
}
|
||||
|
||||
.ct-point:hover {
|
||||
stroke-opacity: 0.2!important;
|
||||
stroke-width: 20px;
|
||||
}
|
||||
|
||||
.ct-tooltip {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
min-width: 5em;
|
||||
padding: 8px 10px;
|
||||
background: #383838;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
font-weight: 700;
|
||||
|
||||
// Arrow!
|
||||
&:before {
|
||||
bottom: -14px;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
border: solid transparent;
|
||||
content: '';
|
||||
height: 0;
|
||||
width: 0;
|
||||
pointer-events: none;
|
||||
border-color: rgba(251, 249, 228, 0);
|
||||
border-top-color: #383838;
|
||||
border-width: 7px;
|
||||
margin-left: -8px;
|
||||
}
|
||||
|
||||
&.hide {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// CHARTIST LEGEND =============================================================
|
||||
|
||||
.ct-legend {
|
||||
bottom: 0;
|
||||
margin-bottom: -1em;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
padding-left: 0.5em;
|
||||
list-style-type: none;
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
li:before {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
left: 0;
|
||||
content: '';
|
||||
border: 3px solid transparent;
|
||||
border-radius: 2px;
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
li.inactive:before {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.ct-legend-inside {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@for $i from 0 to length($ct-series-colors) {
|
||||
.ct-series-#{$i}:before {
|
||||
background-color: nth($ct-series-colors, $i + 1);
|
||||
border-color: nth($ct-series-colors, $i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/common/action-bar.js
Normal file
46
src/common/action-bar.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import {
|
||||
ButtonGroup
|
||||
} from 'react-bootstrap-4/lib'
|
||||
import {
|
||||
noop
|
||||
} from 'utils'
|
||||
|
||||
const ActionBar = ({ actions, param }) => (
|
||||
<ButtonGroup>
|
||||
{map(actions, (button, index) => {
|
||||
if (!button) {
|
||||
return
|
||||
}
|
||||
|
||||
const { handler, handlerParam = param, label, icon, redirectOnSuccess } = button
|
||||
return <Tooltip key={index} content={_(label)}>
|
||||
<ActionButton
|
||||
key={index}
|
||||
btnStyle='secondary'
|
||||
handler={handler || noop}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
redirectOnSuccess={redirectOnSuccess}
|
||||
size='large'
|
||||
/>
|
||||
</Tooltip>
|
||||
})}
|
||||
</ButtonGroup>
|
||||
)
|
||||
ActionBar.propTypes = {
|
||||
actions: React.PropTypes.arrayOf(
|
||||
React.PropTypes.shape({
|
||||
label: React.PropTypes.string.isRequired,
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
handler: React.PropTypes.func,
|
||||
redirectOnSuccess: React.PropTypes.string
|
||||
})
|
||||
).isRequired,
|
||||
display: React.PropTypes.oneOf(['icon', 'text', 'both'])
|
||||
}
|
||||
export { ActionBar as default }
|
||||
130
src/common/action-button.js
Normal file
130
src/common/action-button.js
Normal file
@@ -0,0 +1,130 @@
|
||||
import Icon from 'icon'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import React from 'react'
|
||||
import { Button } from 'react-bootstrap-4/lib'
|
||||
|
||||
import Component from './base-component'
|
||||
import logError from './log-error'
|
||||
import propTypes from './prop-types'
|
||||
import Tooltip from './tooltip'
|
||||
|
||||
@propTypes({
|
||||
btnStyle: propTypes.string,
|
||||
disabled: propTypes.bool,
|
||||
form: propTypes.string,
|
||||
handler: propTypes.func.isRequired,
|
||||
handlerParam: propTypes.any,
|
||||
icon: propTypes.string.isRequired,
|
||||
redirectOnSuccess: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
]),
|
||||
size: propTypes.oneOf([
|
||||
'large',
|
||||
'small'
|
||||
]),
|
||||
tooltip: propTypes.node
|
||||
})
|
||||
export default class ActionButton extends Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
|
||||
async _execute () {
|
||||
if (this.state.working) {
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
handler,
|
||||
handlerParam
|
||||
} = this.props
|
||||
|
||||
try {
|
||||
this.setState({
|
||||
error: null,
|
||||
working: true
|
||||
})
|
||||
|
||||
const result = await handler(handlerParam)
|
||||
|
||||
let { redirectOnSuccess } = this.props
|
||||
if (redirectOnSuccess) {
|
||||
if (isFunction(redirectOnSuccess)) {
|
||||
redirectOnSuccess = redirectOnSuccess(result)
|
||||
}
|
||||
return this.context.router.push(redirectOnSuccess)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
working: false
|
||||
})
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
error,
|
||||
working: false
|
||||
})
|
||||
|
||||
// ignore when undefined because it usually means that the action has been canceled
|
||||
if (error !== undefined) {
|
||||
logError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
_execute = ::this._execute
|
||||
|
||||
_eventListener = event => {
|
||||
event.preventDefault()
|
||||
this._execute()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { form } = this.props
|
||||
|
||||
if (form) {
|
||||
document.getElementById(form).addEventListener('submit', this._eventListener)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { form } = this.props
|
||||
|
||||
if (form) {
|
||||
document.getElementById(form).removeEventListener('submit', this._eventListener)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
props: {
|
||||
btnStyle,
|
||||
children,
|
||||
className,
|
||||
disabled,
|
||||
form,
|
||||
icon,
|
||||
size: bsSize,
|
||||
style,
|
||||
tooltip
|
||||
},
|
||||
state: { error, working }
|
||||
} = this
|
||||
|
||||
const button = <Button
|
||||
bsStyle={error ? 'warning' : btnStyle}
|
||||
form={form}
|
||||
onClick={!form && this._execute}
|
||||
disabled={working || disabled}
|
||||
type={form ? 'submit' : 'button'}
|
||||
{...{ bsSize, className, style }}
|
||||
>
|
||||
<Icon icon={working ? 'loading' : icon} fixedWidth />
|
||||
{children && ' '}
|
||||
{children}
|
||||
</Button>
|
||||
|
||||
return tooltip
|
||||
? <Tooltip content={tooltip}>{button}</Tooltip>
|
||||
: button
|
||||
}
|
||||
}
|
||||
7
src/common/action-row-button/index.css
Normal file
7
src/common/action-row-button/index.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.button {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
tr:hover .button, tr:focus .button {
|
||||
opacity: 1;
|
||||
}
|
||||
14
src/common/action-row-button/index.js
Normal file
14
src/common/action-row-button/index.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
|
||||
import ActionButton from '../action-button'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const ActionRowButton = props => (
|
||||
<ActionButton
|
||||
{...props}
|
||||
className={styles.button}
|
||||
size='small'
|
||||
/>
|
||||
)
|
||||
export { ActionRowButton as default }
|
||||
15
src/common/action-toggle.js
Normal file
15
src/common/action-toggle.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
const ActionToggle = ({ className, value, ...props }) =>
|
||||
<ActionButton
|
||||
{...props}
|
||||
btnStyle={value ? 'success' : null}
|
||||
icon={value ? 'toggle-on' : 'toggle-off'}
|
||||
/>
|
||||
|
||||
export default propTypes({
|
||||
value: propTypes.bool
|
||||
})(ActionToggle)
|
||||
139
src/common/base-component.js
Normal file
139
src/common/base-component.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import clone from 'lodash/clone'
|
||||
import includes from 'lodash/includes'
|
||||
import isArray from 'lodash/isArray'
|
||||
import forEach from 'lodash/forEach'
|
||||
import map from 'lodash/map'
|
||||
import { Component } from 'react'
|
||||
|
||||
import getEventValue from './get-event-value'
|
||||
import invoke from './invoke'
|
||||
import shallowEqual from './shallow-equal'
|
||||
|
||||
const cowSet = (object, path, value, depth) => {
|
||||
if (depth >= path.length) {
|
||||
return value
|
||||
}
|
||||
|
||||
object = clone(object)
|
||||
const prop = path[depth]
|
||||
object[prop] = cowSet(object[prop], path, value, depth + 1)
|
||||
return object
|
||||
}
|
||||
|
||||
const get = (object, path, depth) => {
|
||||
if (depth >= path.length) {
|
||||
return object
|
||||
}
|
||||
|
||||
const prop = path[depth++]
|
||||
return isArray(object) && prop === '*'
|
||||
? map(object, value => get(value, path, depth))
|
||||
: get(object[prop], path, depth)
|
||||
}
|
||||
|
||||
export default class BaseComponent extends Component {
|
||||
constructor (props, context) {
|
||||
super(props, context)
|
||||
|
||||
// It really should have been done in React.Component!
|
||||
this.state = {}
|
||||
|
||||
this._linkedState = null
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
this.render = invoke(this.render, render => () => {
|
||||
console.log('render', this.constructor.name)
|
||||
|
||||
return render.call(this)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// See https://preactjs.com/guide/linked-state
|
||||
linkState (name, targetPath) {
|
||||
const key = targetPath
|
||||
? `${name}##${targetPath}`
|
||||
: name
|
||||
|
||||
let linkedState = this._linkedState
|
||||
let cb
|
||||
if (!linkedState) {
|
||||
linkedState = this._linkedState = {}
|
||||
} else if ((cb = linkedState[key])) {
|
||||
return cb
|
||||
}
|
||||
|
||||
let getValue
|
||||
if (targetPath) {
|
||||
const path = targetPath.split('.')
|
||||
getValue = event => get(getEventValue(event), path, 0)
|
||||
} else {
|
||||
getValue = getEventValue
|
||||
}
|
||||
|
||||
if (includes(name, '.')) {
|
||||
const path = name.split('.')
|
||||
return (linkedState[key] = event => {
|
||||
this.setState(cowSet(this.state, path, getValue(event), 0))
|
||||
})
|
||||
}
|
||||
|
||||
return (linkedState[key] = event => {
|
||||
this.setState({
|
||||
[name]: getValue(event)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
toggleState (name) {
|
||||
let linkedState = this._linkedState
|
||||
let cb
|
||||
if (!linkedState) {
|
||||
linkedState = this._linkedState = {}
|
||||
} else if ((cb = linkedState[name])) {
|
||||
return cb
|
||||
}
|
||||
|
||||
if (includes(name, '.')) {
|
||||
const path = name.split('.')
|
||||
return (linkedState[path] = event => {
|
||||
this.setState(cowSet(this.state, path, !get(this.state, path, 0), 0))
|
||||
})
|
||||
}
|
||||
|
||||
return (linkedState[name] = () => {
|
||||
this.setState({
|
||||
[name]: !this.state[name]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
shouldComponentUpdate (newProps, newState) {
|
||||
return !(
|
||||
shallowEqual(this.props, newProps) &&
|
||||
shallowEqual(this.state, newState)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const diff = (name, old, cur) => {
|
||||
const keys = []
|
||||
|
||||
forEach(old, (value, key) => {
|
||||
if (cur[key] !== value) {
|
||||
keys.push(key)
|
||||
}
|
||||
})
|
||||
|
||||
if (keys.length) {
|
||||
console.log(name, keys.sort().join())
|
||||
}
|
||||
}
|
||||
|
||||
BaseComponent.prototype.componentDidUpdate = function (oldProps, oldState) {
|
||||
const prefix = `${this.constructor.name} updated because of its`
|
||||
diff(`${prefix} props:`, oldProps, this.props)
|
||||
diff(`${prefix} state:`, oldState, this.state)
|
||||
}
|
||||
}
|
||||
35
src/common/browser-notification.js
Normal file
35
src/common/browser-notification.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { noop } from 'utils'
|
||||
import Notify from 'notifyjs'
|
||||
|
||||
let notify
|
||||
export { notify as default }
|
||||
|
||||
const sendNotification = (title, body) => {
|
||||
new Notify(title, {
|
||||
body,
|
||||
timeout: 5,
|
||||
icon: 'assets/logo.png'
|
||||
}).show()
|
||||
}
|
||||
|
||||
const requestPermission = (...args) => {
|
||||
if (Notify.isSupported()) {
|
||||
Notify.requestPermission(
|
||||
() => {
|
||||
console.log('notifications allowed')
|
||||
|
||||
return (notify = sendNotification)(...args)
|
||||
},
|
||||
() => {
|
||||
console.log('notifications denied')
|
||||
|
||||
notify = noop
|
||||
}
|
||||
)
|
||||
} else {
|
||||
notify = noop
|
||||
console.warn('notifications are not supported')
|
||||
}
|
||||
}
|
||||
|
||||
notify = Notify.needsPermission ? requestPermission : sendNotification
|
||||
51
src/common/card.js
Normal file
51
src/common/card.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
|
||||
const CARD_STYLE = {
|
||||
minHeight: '100%'
|
||||
}
|
||||
|
||||
const CARD_STYLE_WITH_SHADOW = {
|
||||
...CARD_STYLE,
|
||||
boxShadow: '0 10px 6px -6px #777' // https://css-tricks.com/almanac/properties/b/box-shadow/
|
||||
}
|
||||
|
||||
const CARD_HEADER_STYLE = {
|
||||
minHeight: '100%',
|
||||
textAlign: 'center'
|
||||
}
|
||||
|
||||
export const Card = propTypes({
|
||||
disableMaxHeight: propTypes.bool,
|
||||
shadow: propTypes.bool
|
||||
})(({
|
||||
children,
|
||||
shadow
|
||||
}) => (
|
||||
<div className='card' style={shadow ? CARD_STYLE_WITH_SHADOW : CARD_STYLE}>
|
||||
{children}
|
||||
</div>
|
||||
))
|
||||
|
||||
export const CardHeader = propTypes({
|
||||
className: propTypes.string
|
||||
})(({
|
||||
children,
|
||||
className
|
||||
}) => (
|
||||
<h4 className={`card-header ${className || ''}`} style={CARD_HEADER_STYLE}>
|
||||
{children}
|
||||
</h4>
|
||||
))
|
||||
|
||||
export const CardBlock = propTypes({
|
||||
className: propTypes.string
|
||||
})(({
|
||||
children,
|
||||
className
|
||||
}) => (
|
||||
<div className={`card-block ${className || ''}`}>
|
||||
{children}
|
||||
</div>
|
||||
))
|
||||
9
src/common/center-panel/index.css
Normal file
9
src/common/center-panel/index.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.container {
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
12
src/common/center-panel/index.js
Normal file
12
src/common/center-panel/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const CenterPanel = ({ children }) =>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
export { CenterPanel as default }
|
||||
32
src/common/collapse.js
Normal file
32
src/common/collapse.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
@propTypes({
|
||||
children: propTypes.any.isRequired,
|
||||
className: propTypes.string,
|
||||
buttonText: propTypes.any.isRequired
|
||||
})
|
||||
export default class Collapse extends Component {
|
||||
_onClick = () => {
|
||||
this.setState({
|
||||
isOpened: !this.state.isOpened
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { isOpened } = this.state
|
||||
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<button className='btn btn-lg btn-primary btn-block' onClick={this._onClick}>
|
||||
{props.buttonText} <Icon icon={`chevron-${isOpened ? 'up' : 'down'}`} />
|
||||
</button>
|
||||
{isOpened && props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
3
src/common/combobox/index.css
Normal file
3
src/common/combobox/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.button {
|
||||
border-radius: 0px;
|
||||
};
|
||||
101
src/common/combobox/index.js
Normal file
101
src/common/combobox/index.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import size from 'lodash/size'
|
||||
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import { ensureArray } from '../utils'
|
||||
import {
|
||||
DropdownButton,
|
||||
MenuItem
|
||||
} from 'react-bootstrap-4/lib'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@propTypes({
|
||||
defaultValue: propTypes.any,
|
||||
disabled: propTypes.bool,
|
||||
options: propTypes.oneOfType([
|
||||
propTypes.arrayOf(propTypes.string),
|
||||
propTypes.number,
|
||||
propTypes.objectOf(propTypes.string),
|
||||
propTypes.string
|
||||
]),
|
||||
onChange: propTypes.func,
|
||||
placeholder: propTypes.string,
|
||||
required: propTypes.bool,
|
||||
step: propTypes.any,
|
||||
type: propTypes.string,
|
||||
value: propTypes.any
|
||||
})
|
||||
export default class Combobox extends Component {
|
||||
static defaultProps = {
|
||||
type: 'text'
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
_handleChange = event => {
|
||||
const { onChange } = this.props
|
||||
|
||||
if (onChange) {
|
||||
onChange(event.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
_setText (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const options = ensureArray(props.options)
|
||||
|
||||
const Input = (
|
||||
<input
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
options={options}
|
||||
onChange={this._handleChange}
|
||||
placeholder={props.placeholder}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
step={props.step}
|
||||
type={props.type}
|
||||
value={props.value}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!size(options)) {
|
||||
return Input
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='input-group'>
|
||||
<div className='input-group-btn'>
|
||||
<DropdownButton
|
||||
bsStyle='secondary'
|
||||
className={styles.button}
|
||||
disabled={props.disabled}
|
||||
id='selectInput'
|
||||
title=''
|
||||
>
|
||||
{map(options, option => (
|
||||
<MenuItem key={option} onClick={() => this._setText(option)}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownButton>
|
||||
</div>
|
||||
{Input}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
18
src/common/complex-matcher/index.bench.js
Normal file
18
src/common/complex-matcher/index.bench.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
parse,
|
||||
toString
|
||||
} from './'
|
||||
import {
|
||||
ast,
|
||||
pattern
|
||||
} from './index.fixtures'
|
||||
|
||||
export default ({ benchmark }) => {
|
||||
benchmark('parse', () => {
|
||||
parse(pattern)
|
||||
})
|
||||
|
||||
benchmark('toString', () => {
|
||||
ast::toString()
|
||||
})
|
||||
}
|
||||
18
src/common/complex-matcher/index.fixtures.js
Normal file
18
src/common/complex-matcher/index.fixtures.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
createAnd,
|
||||
createOr,
|
||||
createNot,
|
||||
createProperty,
|
||||
createString
|
||||
} from './'
|
||||
|
||||
export const pattern = 'foo !"\\\\ \\"" name:|(wonderwoman batman)'
|
||||
|
||||
export const ast = createAnd([
|
||||
createString('foo'),
|
||||
createNot(createString('\\ "')),
|
||||
createProperty('name', createOr([
|
||||
createString('wonderwoman'),
|
||||
createString('batman')
|
||||
]))
|
||||
])
|
||||
405
src/common/complex-matcher/index.js
Normal file
405
src/common/complex-matcher/index.js
Normal file
@@ -0,0 +1,405 @@
|
||||
import every from 'lodash/every'
|
||||
import filter from 'lodash/filter'
|
||||
import forEach from 'lodash/forEach'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isPlainObject from 'lodash/isPlainObject'
|
||||
import isString from 'lodash/isString'
|
||||
import map from 'lodash/map'
|
||||
import some from 'lodash/some'
|
||||
|
||||
import filterReduce from '../filter-reduce'
|
||||
import invoke from '../invoke'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const RAW_STRING_CHARS = invoke(() => {
|
||||
const chars = { __proto__: null }
|
||||
const add = (a, b = a) => {
|
||||
let i = a.charCodeAt(0)
|
||||
const j = b.charCodeAt(0)
|
||||
while (i <= j) {
|
||||
chars[String.fromCharCode(i++)] = true
|
||||
}
|
||||
}
|
||||
add('$')
|
||||
add('-')
|
||||
add('.')
|
||||
add('0', '9')
|
||||
add('_')
|
||||
add('A', 'Z')
|
||||
add('a', 'z')
|
||||
return chars
|
||||
})
|
||||
const isRawString = string => {
|
||||
const { length } = string
|
||||
for (let i = 0; i < length; ++i) {
|
||||
if (!RAW_STRING_CHARS[string[i]]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const createAnd = children => children.length === 1
|
||||
? children[0]
|
||||
: { type: 'and', children }
|
||||
|
||||
export const createOr = children => children.length === 1
|
||||
? children[0]
|
||||
: { type: 'or', children }
|
||||
|
||||
export const createNot = child => ({ type: 'not', child })
|
||||
|
||||
export const createProperty = (name, child) => ({ type: 'property', name, child })
|
||||
|
||||
export const createString = value => ({ type: 'string', value })
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// *and = terms
|
||||
// terms = term+
|
||||
// term = ws (groupedAnd | or | not | property | string) ws
|
||||
// ws = ' '*
|
||||
// groupedAnd = "(" and ")"
|
||||
// *or = "|" ws "(" terms ")"
|
||||
// *not = "!" term
|
||||
// *property = string ws ":" term
|
||||
// *string = quotedString | rawString
|
||||
// quotedString = "\"" ( /[^"\]/ | "\\\\" | "\\\"" )+
|
||||
// rawString = /[a-z0-9-_.]+/i
|
||||
export const parse = invoke(() => {
|
||||
let i
|
||||
let n
|
||||
let input
|
||||
|
||||
// -----
|
||||
|
||||
const backtrace = parser => () => {
|
||||
const pos = i
|
||||
const node = parser()
|
||||
if (node != null) {
|
||||
return node
|
||||
}
|
||||
i = pos
|
||||
}
|
||||
|
||||
// -----
|
||||
|
||||
const parseAnd = () => parseTerms(createAnd)
|
||||
const parseTerms = fn => {
|
||||
let term = parseTerm()
|
||||
if (!term) {
|
||||
return
|
||||
}
|
||||
|
||||
const terms = [ term ]
|
||||
while ((term = parseTerm())) {
|
||||
terms.push(term)
|
||||
}
|
||||
return fn(terms)
|
||||
}
|
||||
const parseTerm = () => {
|
||||
parseWs()
|
||||
|
||||
const child = (
|
||||
parseGroupedAnd() ||
|
||||
parseOr() ||
|
||||
parseNot() ||
|
||||
parseProperty() ||
|
||||
parseString()
|
||||
)
|
||||
if (child) {
|
||||
parseWs()
|
||||
return child
|
||||
}
|
||||
}
|
||||
const parseWs = () => {
|
||||
while (input[i] === ' ') {
|
||||
++i
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
const parseGroupedAnd = backtrace(() => {
|
||||
let and
|
||||
if (
|
||||
input[i++] === '(' &&
|
||||
(and = parseAnd()) &&
|
||||
input[i++] === ')'
|
||||
) {
|
||||
return and
|
||||
}
|
||||
})
|
||||
const parseOr = backtrace(() => {
|
||||
let or
|
||||
if (
|
||||
input[i++] === '|' &&
|
||||
parseWs() &&
|
||||
input[i++] === '(' &&
|
||||
(or = parseTerms(createOr)) &&
|
||||
input[i++] === ')'
|
||||
) {
|
||||
return or
|
||||
}
|
||||
})
|
||||
const parseNot = backtrace(() => {
|
||||
let child
|
||||
if (
|
||||
input[i++] === '!' &&
|
||||
(child = parseTerm())
|
||||
) {
|
||||
return createNot(child)
|
||||
}
|
||||
})
|
||||
const parseProperty = backtrace(() => {
|
||||
let name, child
|
||||
if (
|
||||
(name = parseString()) &&
|
||||
parseWs() &&
|
||||
(input[i++] === ':') &&
|
||||
(child = parseTerm())
|
||||
) {
|
||||
return createProperty(name.value, child)
|
||||
}
|
||||
})
|
||||
const parseString = () => {
|
||||
let value
|
||||
if (
|
||||
(value = parseQuotedString()) != null ||
|
||||
(value = parseRawString()) != null
|
||||
) {
|
||||
return createString(value)
|
||||
}
|
||||
}
|
||||
const parseQuotedString = backtrace(() => {
|
||||
if (input[i++] !== '"') {
|
||||
return
|
||||
}
|
||||
|
||||
const value = []
|
||||
let char
|
||||
while (i < n && (char = input[i++]) !== '"') {
|
||||
if (char === '\\') {
|
||||
char = input[i++]
|
||||
}
|
||||
value.push(char)
|
||||
}
|
||||
|
||||
return value.join('')
|
||||
})
|
||||
const parseRawString = () => {
|
||||
let value = ''
|
||||
let c
|
||||
while (
|
||||
(c = input[i]) &&
|
||||
RAW_STRING_CHARS[c]
|
||||
) {
|
||||
++i
|
||||
value += c
|
||||
}
|
||||
if (value.length) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return input_ => {
|
||||
if (!input_) {
|
||||
return
|
||||
}
|
||||
|
||||
i = 0
|
||||
input = input_.split('')
|
||||
n = input.length
|
||||
|
||||
try {
|
||||
return parseAnd()
|
||||
} finally {
|
||||
input = null
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const _getPropertyClauseStrings = ({ child }) => {
|
||||
const { type } = child
|
||||
|
||||
if (type === 'or') {
|
||||
const strings = []
|
||||
forEach(child.children, child => {
|
||||
if (child.type === 'string') {
|
||||
strings.push(child.value)
|
||||
}
|
||||
})
|
||||
return strings
|
||||
}
|
||||
|
||||
if (type === 'string') {
|
||||
return [ child.value ]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// Find possible values for property clauses in a and clause.
|
||||
export const getPropertyClausesStrings = function () {
|
||||
if (!this) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const { type } = this
|
||||
|
||||
if (type === 'property') {
|
||||
return {
|
||||
[this.name]: _getPropertyClauseStrings(this)
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'and') {
|
||||
const strings = {}
|
||||
forEach(this.children, node => {
|
||||
if (node.type === 'property') {
|
||||
const { name } = node
|
||||
const values = strings[name]
|
||||
if (values) {
|
||||
values.push.apply(values, _getPropertyClauseStrings(node))
|
||||
} else {
|
||||
strings[name] = _getPropertyClauseStrings(node)
|
||||
}
|
||||
}
|
||||
})
|
||||
return strings
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const removePropertyClause = function (name) {
|
||||
let type
|
||||
if (
|
||||
!this ||
|
||||
(type = this.type) === 'property' && this.name === name
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'and') {
|
||||
return createAnd(filter(this.children, node =>
|
||||
node.type !== 'property' || node.name !== name
|
||||
))
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const _addAndClause = (node, child, predicate, reducer) =>
|
||||
createAnd(filterReduce(
|
||||
node.type === 'and'
|
||||
? node.children
|
||||
: [ node ],
|
||||
predicate,
|
||||
reducer,
|
||||
child
|
||||
))
|
||||
|
||||
export const setPropertyClause = function (name, child) {
|
||||
const property = createProperty(
|
||||
name,
|
||||
isString(child) ? createString(child) : child
|
||||
)
|
||||
|
||||
if (!this) {
|
||||
return property
|
||||
}
|
||||
|
||||
return _addAndClause(
|
||||
this,
|
||||
property,
|
||||
node => node.type === 'property' && node.name === name,
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const execute = invoke(() => {
|
||||
const visitors = {
|
||||
and: ({ children }, value) => (
|
||||
every(children, child => child::execute(value))
|
||||
),
|
||||
not: ({ child }, value) => (
|
||||
!child::execute(value)
|
||||
),
|
||||
or: ({ children }, value) => (
|
||||
some(children, child => child::execute(value))
|
||||
),
|
||||
property: ({ name, child }, value) => (
|
||||
value != null && child::execute(value[name])
|
||||
),
|
||||
string: invoke(() => {
|
||||
const match = (pattern, value) => {
|
||||
if (isString(value)) {
|
||||
return value.toLowerCase().indexOf(pattern) !== -1
|
||||
}
|
||||
|
||||
if (isArray(value) || isPlainObject(value)) {
|
||||
return some(value, value => match(pattern, value))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return ({ value: pattern }, value) => (
|
||||
match(pattern.toLowerCase(), value)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return function (value) {
|
||||
return visitors[this.type](this, value)
|
||||
}
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const toString = invoke(() => {
|
||||
const toStringTerms = terms => map(terms, toString).join(' ')
|
||||
const toStringGroup = terms => `(${toStringTerms(terms)})`
|
||||
|
||||
const visitors = {
|
||||
and: ({ children }) => toStringGroup(children),
|
||||
not: ({ child }) => `!${toString(child)}`,
|
||||
or: ({ children }) => `|${toStringGroup(children)}`,
|
||||
property: ({ name, child }) => `${toString(createString(name))}:${toString(child)}`,
|
||||
string: ({ value }) => isRawString(value)
|
||||
? value
|
||||
: `"${value.replace(/\\|"/g, match => `\\${match}`)}"`
|
||||
}
|
||||
|
||||
const toString = node => visitors[node.type](node)
|
||||
|
||||
// Special case for a root “and”: do not add braces.
|
||||
return function () {
|
||||
return !this
|
||||
? ''
|
||||
: this.type === 'and'
|
||||
? toStringTerms(this.children)
|
||||
: toString(this)
|
||||
}
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const create = pattern => {
|
||||
pattern = parse(pattern)
|
||||
if (!pattern) {
|
||||
return
|
||||
}
|
||||
|
||||
return value => pattern::execute(value)
|
||||
}
|
||||
53
src/common/complex-matcher/index.spec.js
Normal file
53
src/common/complex-matcher/index.spec.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import test from 'ava'
|
||||
|
||||
import {
|
||||
getPropertyClausesStrings,
|
||||
parse,
|
||||
setPropertyClause,
|
||||
toString
|
||||
} from './'
|
||||
import {
|
||||
ast,
|
||||
pattern
|
||||
} from './index.fixtures'
|
||||
|
||||
test('getPropertyClausesStrings', t => {
|
||||
let tmp = parse('foo bar:baz baz:|(foo bar)')::getPropertyClausesStrings()
|
||||
t.deepEqual(
|
||||
tmp,
|
||||
{
|
||||
bar: [ 'baz' ],
|
||||
baz: [ 'foo', 'bar' ]
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test('parse', t => {
|
||||
t.deepEqual(parse(pattern), ast)
|
||||
})
|
||||
|
||||
test('setPropertyClause', t => {
|
||||
t.is(
|
||||
null::setPropertyClause('foo', 'bar')::toString(),
|
||||
'foo:bar'
|
||||
)
|
||||
|
||||
t.is(
|
||||
parse('baz')::setPropertyClause('foo', 'bar')::toString(),
|
||||
'baz foo:bar'
|
||||
)
|
||||
|
||||
t.is(
|
||||
parse('plip foo:baz plop')::setPropertyClause('foo', 'bar')::toString(),
|
||||
'plip plop foo:bar'
|
||||
)
|
||||
|
||||
t.is(
|
||||
parse('foo:|(baz plop)')::setPropertyClause('foo', 'bar')::toString(),
|
||||
'foo:bar'
|
||||
)
|
||||
})
|
||||
|
||||
test('toString', t => {
|
||||
t.is(pattern, ast::toString())
|
||||
})
|
||||
9
src/common/copiable/index.css
Normal file
9
src/common/copiable/index.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.container .button {
|
||||
position: absolute;
|
||||
margin-left: 1ex;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.container:hover .button {
|
||||
visibility: visible;
|
||||
}
|
||||
31
src/common/copiable/index.js
Normal file
31
src/common/copiable/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import _ from 'intl'
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import classNames from 'classnames'
|
||||
import Tooltip from 'tooltip'
|
||||
import React, { createElement } from 'react'
|
||||
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const Copiable = propTypes({
|
||||
data: propTypes.string,
|
||||
tagName: propTypes.string
|
||||
})(({ className, tagName = 'span', ...props }) => createElement(
|
||||
tagName,
|
||||
{
|
||||
...props,
|
||||
className: classNames(styles.container, className)
|
||||
},
|
||||
props.children,
|
||||
' ',
|
||||
<Tooltip content={_('copyToClipboard')}>
|
||||
<CopyToClipboard text={props.data || props.children}>
|
||||
<button className={classNames('btn btn-sm btn-secondary', styles.button)}>
|
||||
<Icon icon='clipboard' />
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
))
|
||||
export { Copiable as default }
|
||||
9
src/common/d3-utils.js
vendored
Normal file
9
src/common/d3-utils.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import forEach from 'lodash/forEach'
|
||||
|
||||
export function setStyles (style) {
|
||||
forEach(style, (value, key) => {
|
||||
this.style(key, value)
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
53
src/common/debug.js
Normal file
53
src/common/debug.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import { isPromise } from 'promise-toolbox'
|
||||
|
||||
const toString = value => JSON.stringify(value, null, 2)
|
||||
|
||||
// This component does not handle changes in its `promise` property.
|
||||
class DebugAsync extends Component {
|
||||
static propTypes = {
|
||||
promise: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
status: 'pending'
|
||||
}
|
||||
|
||||
props.promise.then(
|
||||
value => this.setState({ status: 'resolved', value }),
|
||||
value => this.setState({ status: 'rejected', value })
|
||||
)
|
||||
}
|
||||
|
||||
shouldComponentUpdate (_, newState) {
|
||||
return this.state.status !== newState.status
|
||||
}
|
||||
|
||||
render () {
|
||||
const { status, value } = this.state
|
||||
|
||||
if (status === 'pending') {
|
||||
return <pre>{'Promise { <pending> }'}</pre>
|
||||
}
|
||||
|
||||
return <pre>
|
||||
{'Promise { '}
|
||||
{status === 'rejected' && '<rejected> '}
|
||||
{toString(value)}
|
||||
{' }'}
|
||||
</pre>
|
||||
}
|
||||
}
|
||||
|
||||
const Debug = ({ value }) => isPromise(value)
|
||||
? <DebugAsync promise={value} />
|
||||
: <pre>{toString(value)}</pre>
|
||||
|
||||
Debug.propTypes = {
|
||||
value: PropTypes.any.isRequired
|
||||
}
|
||||
|
||||
export { Debug as default }
|
||||
13
src/common/editable/index.css
Normal file
13
src/common/editable/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.clickToEdit * {
|
||||
cursor: context-menu !important;
|
||||
}
|
||||
.shortClick {
|
||||
border-bottom: 1px dashed #ccc;
|
||||
}
|
||||
|
||||
.select {
|
||||
padding: 0px;
|
||||
}
|
||||
.size {
|
||||
width: 10rem;
|
||||
}
|
||||
476
src/common/editable/index.js
Normal file
476
src/common/editable/index.js
Normal file
@@ -0,0 +1,476 @@
|
||||
import classNames from 'classnames'
|
||||
import findKey from 'lodash/findKey'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import isString from 'lodash/isString'
|
||||
import map from 'lodash/map'
|
||||
import pick from 'lodash/pick'
|
||||
import React from 'react'
|
||||
|
||||
import _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
import Icon from '../icon'
|
||||
import logError from '../log-error'
|
||||
import propTypes from '../prop-types'
|
||||
import Tooltip from '../tooltip'
|
||||
import { formatSize } from '../utils'
|
||||
import { SizeInput } from '../form'
|
||||
import {
|
||||
SelectHost,
|
||||
SelectIp,
|
||||
SelectNetwork,
|
||||
SelectPool,
|
||||
SelectRemote,
|
||||
SelectSr,
|
||||
SelectSubject,
|
||||
SelectTag,
|
||||
SelectVm,
|
||||
SelectVmTemplate
|
||||
} from '../select-objects'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const LONG_CLICK = 400
|
||||
|
||||
@propTypes({
|
||||
alt: propTypes.node.isRequired
|
||||
})
|
||||
class Hover extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
hover: false
|
||||
}
|
||||
|
||||
this._onMouseEnter = () => this.setState({ hover: true })
|
||||
this._onMouseLeave = () => this.setState({ hover: false })
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this.state.hover) {
|
||||
return <span onMouseLeave={this._onMouseLeave}>
|
||||
{this.props.alt}
|
||||
</span>
|
||||
}
|
||||
|
||||
return <span onMouseEnter={this._onMouseEnter}>
|
||||
{this.props.children}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
onChange: propTypes.func.isRequired,
|
||||
onUndo: propTypes.oneOfType([
|
||||
propTypes.bool,
|
||||
propTypes.func
|
||||
]),
|
||||
useLongClick: propTypes.bool,
|
||||
value: propTypes.any.isRequired
|
||||
})
|
||||
class Editable extends Component {
|
||||
get value () {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
|
||||
_onKeyDown = event => {
|
||||
const { keyCode } = event
|
||||
if (keyCode === 27) {
|
||||
return this._closeEdition()
|
||||
}
|
||||
|
||||
if (keyCode === 13) {
|
||||
return this._save()
|
||||
}
|
||||
}
|
||||
|
||||
_closeEdition = () => {
|
||||
this.setState({ editing: false })
|
||||
}
|
||||
|
||||
_openEdition = () => {
|
||||
this.setState({
|
||||
editing: true,
|
||||
error: null,
|
||||
saving: false
|
||||
})
|
||||
}
|
||||
|
||||
_undo = () => {
|
||||
const { props } = this
|
||||
const { onUndo } = props
|
||||
if (onUndo === false) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.__save(
|
||||
() => this.state.previous,
|
||||
isFunction(onUndo) ? onUndo : props.onChange
|
||||
)
|
||||
}
|
||||
|
||||
_save () {
|
||||
return this.__save(
|
||||
() => this.value,
|
||||
this.props.onChange
|
||||
)
|
||||
}
|
||||
|
||||
async __save (getValue, saveValue) {
|
||||
const { props } = this
|
||||
|
||||
try {
|
||||
const value = getValue()
|
||||
const previous = props.value
|
||||
if (value === previous) {
|
||||
return this._closeEdition()
|
||||
}
|
||||
|
||||
this.setState({ saving: true })
|
||||
|
||||
await saveValue(value)
|
||||
|
||||
this.setState({ previous })
|
||||
this._closeEdition()
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
error: isString(error) ? error : error.message,
|
||||
saving: false
|
||||
})
|
||||
logError(error)
|
||||
}
|
||||
}
|
||||
|
||||
__startTimer = event => {
|
||||
event.persist()
|
||||
this._timeout = setTimeout(() => {
|
||||
event.preventDefault()
|
||||
this._openEdition()
|
||||
}, LONG_CLICK)
|
||||
}
|
||||
__stopTimer = () => clearTimeout(this._timeout)
|
||||
|
||||
render () {
|
||||
const { state, props } = this
|
||||
|
||||
if (!state.editing) {
|
||||
const { onUndo, previous } = state
|
||||
const { useLongClick } = props
|
||||
|
||||
const success = <Icon icon='success' />
|
||||
return <span className={classNames(styles.clickToEdit, !useLongClick && styles.shortClick)}>
|
||||
<span
|
||||
onClick={!useLongClick && this._openEdition}
|
||||
onMouseDown={useLongClick && this.__startTimer}
|
||||
onMouseUp={useLongClick && this.__stopTimer}
|
||||
>
|
||||
{this._renderDisplay()}
|
||||
</span>
|
||||
{previous != null && (onUndo !== false
|
||||
? <Hover
|
||||
alt={<a onClick={this._undo}><Icon icon='undo' /></a>}
|
||||
>
|
||||
{success}
|
||||
</Hover>
|
||||
: success
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
|
||||
const { error, saving } = state
|
||||
|
||||
return <span>
|
||||
{this._renderEdition()}
|
||||
{saving && <span>{' '}<Icon icon='loading' /></span>}
|
||||
{error != null && <span>
|
||||
{' '}<Tooltip content={error}><Icon icon='error' /></Tooltip>
|
||||
</span>}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
autoComplete: propTypes.string,
|
||||
maxLength: propTypes.number,
|
||||
minLength: propTypes.number,
|
||||
pattern: propTypes.string,
|
||||
value: propTypes.string.isRequired
|
||||
})
|
||||
export class Text extends Editable {
|
||||
get value () {
|
||||
const { input } = this.refs
|
||||
|
||||
// FIXME: should be properly forwarded to the user.
|
||||
const error = input.validationMessage
|
||||
if (error) {
|
||||
throw new Error(error)
|
||||
}
|
||||
|
||||
return input.value
|
||||
}
|
||||
|
||||
_onInput = ({ target }) => {
|
||||
target.style.width = `${target.value.length + 1}ex`
|
||||
}
|
||||
|
||||
_renderDisplay () {
|
||||
const {
|
||||
children,
|
||||
value
|
||||
} = this.props
|
||||
|
||||
if (children || value) {
|
||||
return <span> {children || value} </span>
|
||||
}
|
||||
|
||||
const {
|
||||
placeholder,
|
||||
useLongClick
|
||||
} = this.props
|
||||
|
||||
return <span className='text-muted'>
|
||||
{placeholder ||
|
||||
(useLongClick ? _('editableLongClickPlaceholder') : _('editableClickPlaceholder'))
|
||||
}
|
||||
</span>
|
||||
}
|
||||
|
||||
_renderEdition () {
|
||||
const { value } = this.props
|
||||
const { saving } = this.state
|
||||
|
||||
// Optional props that the user may set on the input.
|
||||
const extraProps = pick(this.props, [
|
||||
'autoComplete',
|
||||
'maxLength',
|
||||
'minLength',
|
||||
'pattern'
|
||||
])
|
||||
|
||||
return <input
|
||||
{...extraProps}
|
||||
|
||||
autoFocus
|
||||
defaultValue={value}
|
||||
onBlur={this._closeEdition}
|
||||
onInput={this._onInput}
|
||||
onKeyDown={this._onKeyDown}
|
||||
readOnly={saving}
|
||||
ref='input'
|
||||
style={{
|
||||
width: `${value.length + 1}ex`,
|
||||
maxWidth: '50ex'
|
||||
}}
|
||||
type={this._isPassword ? 'password' : 'text'}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
export class Password extends Text {
|
||||
// TODO: this is a hack, this class should probably have a better
|
||||
// implementation.
|
||||
_isPassword = true
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
nullable: propTypes.bool,
|
||||
value: propTypes.number
|
||||
})
|
||||
export class Number extends Component {
|
||||
get value () {
|
||||
return +this.refs.input.value
|
||||
}
|
||||
|
||||
_onChange = value => {
|
||||
if (value === '') {
|
||||
if (this.props.nullable) {
|
||||
value = null
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
value = +value
|
||||
}
|
||||
|
||||
this.props.onChange(value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value } = this.props
|
||||
return <Text
|
||||
{...this.props}
|
||||
onChange={this._onChange}
|
||||
value={value === null ? '' : String(value)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
labelProp: propTypes.string.isRequired,
|
||||
options: propTypes.oneOfType([
|
||||
propTypes.array,
|
||||
propTypes.object
|
||||
]).isRequired
|
||||
})
|
||||
export class Select extends Editable {
|
||||
constructor (props) {
|
||||
super()
|
||||
|
||||
this._defaultValue = findKey(props.options, option => option === props.value)
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.props.options[this._select.value]
|
||||
}
|
||||
|
||||
_onChange = event => {
|
||||
this._save()
|
||||
}
|
||||
_optionToJsx = (option, index) => {
|
||||
const { labelProp } = this.props
|
||||
return <option
|
||||
key={index}
|
||||
value={index}
|
||||
>
|
||||
{labelProp ? option[labelProp] : option}
|
||||
</option>
|
||||
}
|
||||
|
||||
_onEditionMount = ref => {
|
||||
this._select = ref
|
||||
// Seems to work in Google Chrome (not in Firefox)
|
||||
ref && ref.dispatchEvent(new window.MouseEvent('mousedown'))
|
||||
}
|
||||
|
||||
_renderDisplay () {
|
||||
return this.props.children ||
|
||||
<span>{this.props.value[this.props.labelProp]}</span>
|
||||
}
|
||||
|
||||
_renderEdition () {
|
||||
const { saving } = this.state
|
||||
const { options } = this.props
|
||||
|
||||
return <select
|
||||
autoFocus
|
||||
className={classNames('form-control', styles.select)}
|
||||
defaultValue={this._defaultValue}
|
||||
onBlur={this._closeEdition}
|
||||
onChange={this._onChange}
|
||||
onKeyDown={this._onKeyDown}
|
||||
readOnly={saving}
|
||||
ref={this._onEditionMount}
|
||||
>
|
||||
{map(options, this._optionToJsx)}
|
||||
</select>
|
||||
}
|
||||
}
|
||||
|
||||
const MAP_TYPE_SELECT = {
|
||||
host: SelectHost,
|
||||
ip: SelectIp,
|
||||
network: SelectNetwork,
|
||||
pool: SelectPool,
|
||||
remote: SelectRemote,
|
||||
SR: SelectSr,
|
||||
subject: SelectSubject,
|
||||
tag: SelectTag,
|
||||
VM: SelectVm,
|
||||
'VM-template': SelectVmTemplate
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
labelProp: propTypes.string.isRequired,
|
||||
predicate: propTypes.func,
|
||||
value: propTypes.oneOfType([
|
||||
propTypes.string,
|
||||
propTypes.object
|
||||
]).isRequired
|
||||
})
|
||||
export class XoSelect extends Editable {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
_renderDisplay () {
|
||||
return this.props.children ||
|
||||
<span>{this.props.value[this.props.labelProp]}</span>
|
||||
}
|
||||
|
||||
_onChange = object => {
|
||||
object ? this._save() : this._closeEdition()
|
||||
}
|
||||
|
||||
_renderEdition () {
|
||||
const {
|
||||
placeholder,
|
||||
predicate,
|
||||
saving,
|
||||
xoType
|
||||
} = this.props
|
||||
|
||||
const Select = MAP_TYPE_SELECT[xoType]
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!Select) {
|
||||
throw new Error(`${xoType} is not a valid XoSelect type.`)
|
||||
}
|
||||
}
|
||||
|
||||
// Anchor is needed so that the BlockLink does not trigger a redirection
|
||||
// when this element is clicked.
|
||||
return <a onBlur={this._closeEdition}>
|
||||
<Select
|
||||
autoFocus
|
||||
disabled={saving}
|
||||
onChange={this._onChange}
|
||||
placeholder={placeholder}
|
||||
predicate={predicate}
|
||||
ref='select'
|
||||
/>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
value: propTypes.number.isRequired
|
||||
})
|
||||
export class Size extends Editable {
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
_renderDisplay () {
|
||||
return this.props.children || formatSize(this.props.value)
|
||||
}
|
||||
|
||||
_closeEditionIfUnfocused = () => {
|
||||
this._focused = false
|
||||
setTimeout(() => {
|
||||
!this._focused && this._closeEdition()
|
||||
}, 10)
|
||||
}
|
||||
|
||||
_focus = () => { this._focused = true }
|
||||
|
||||
_renderEdition () {
|
||||
const { saving } = this.state
|
||||
const { value } = this.props
|
||||
|
||||
return <span
|
||||
// SizeInput uses `input-group` which makes it behave as a block element (display: table).
|
||||
// `form-inline` to use it as an inline element
|
||||
className='form-inline'
|
||||
onBlur={this._closeEditionIfUnfocused}
|
||||
onFocus={this._focus}
|
||||
onKeyDown={this._onKeyDown}
|
||||
>
|
||||
<SizeInput
|
||||
autoFocus
|
||||
className={styles.size}
|
||||
ref='input'
|
||||
readOnly={saving}
|
||||
defaultValue={value}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
26
src/common/ellipsis.js
Normal file
26
src/common/ellipsis.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
|
||||
const ellipsisStyle = {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
|
||||
const ellipsisContainerStyle = {
|
||||
display: 'flex'
|
||||
}
|
||||
|
||||
const Ellipsis = ({ children }) => (
|
||||
<span style={ellipsisStyle}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
export { Ellipsis as default }
|
||||
|
||||
export const EllipsisContainer = ({ children }) => (
|
||||
<div style={ellipsisContainerStyle}>
|
||||
{React.Children.map(children, child =>
|
||||
child == null || child.type === Ellipsis ? child : <span>{child}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
45
src/common/filter-reduce.js
Normal file
45
src/common/filter-reduce.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import findIndex from 'lodash/findIndex'
|
||||
import identity from 'lodash/identity'
|
||||
|
||||
// Returns a copy of the array containing:
|
||||
// - the elements which did not matches the predicate
|
||||
// - the result of the reduction of the elements matching the
|
||||
// predicates
|
||||
//
|
||||
// As a special case, if the predicate is not provided, it is
|
||||
// considered to have not matched.
|
||||
const filterReduce = (array, predicate, reducer, initial) => {
|
||||
const { length } = array
|
||||
let i
|
||||
if (
|
||||
!length ||
|
||||
!predicate ||
|
||||
(i = findIndex(array, predicate)) === -1
|
||||
) {
|
||||
return initial == null
|
||||
? array.slice(0)
|
||||
: array.concat(initial)
|
||||
}
|
||||
|
||||
if (reducer == null) {
|
||||
reducer = identity
|
||||
}
|
||||
|
||||
const result = array.slice(0, i)
|
||||
let value = initial == null
|
||||
? array[i]
|
||||
: reducer(initial, array[i], i, array)
|
||||
|
||||
for (i = i + 1; i < length; ++i) {
|
||||
const current = array[i]
|
||||
if (predicate(current, i, array)) {
|
||||
value = reducer(value, current, i, array)
|
||||
} else {
|
||||
result.push(current)
|
||||
}
|
||||
}
|
||||
|
||||
result.push(value)
|
||||
return result
|
||||
}
|
||||
export { filterReduce as default }
|
||||
28
src/common/filter-reduce.spec.js
Normal file
28
src/common/filter-reduce.spec.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import test from 'ava'
|
||||
|
||||
import filterReduce from './filter-reduce'
|
||||
|
||||
const add = (a, b) => a + b
|
||||
const data = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
|
||||
const isEven = x => !(x & 1)
|
||||
|
||||
test('filterReduce', t => {
|
||||
// Returns all elements not matching the predicate and the result of
|
||||
// a reduction over those who do.
|
||||
t.deepEqual(
|
||||
filterReduce(data, isEven, add),
|
||||
[ 1, 3, 5, 7, 9, 20 ]
|
||||
)
|
||||
|
||||
// The default reducer is the identity.
|
||||
t.deepEqual(
|
||||
filterReduce(data, isEven),
|
||||
[ 1, 3, 5, 7, 9, 0 ]
|
||||
)
|
||||
|
||||
// If an initial value is passed it is used.
|
||||
t.deepEqual(
|
||||
filterReduce(data, isEven, add, 22),
|
||||
[ 1, 3, 5, 7, 9, 42 ]
|
||||
)
|
||||
})
|
||||
24
src/common/form-grid.js
Normal file
24
src/common/form-grid.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react'
|
||||
|
||||
import * as Grid from './grid'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
export const LabelCol = propTypes({
|
||||
children: propTypes.any.isRequired
|
||||
})(({ children }) => (
|
||||
<label className='col-md-2 form-control-label'>{children}</label>
|
||||
))
|
||||
|
||||
export const InputCol = propTypes({
|
||||
children: propTypes.any.isRequired
|
||||
})(({ children }) => (
|
||||
<Grid.Col mediumSize={10}>{children}</Grid.Col>
|
||||
))
|
||||
|
||||
export const Row = propTypes({
|
||||
children: propTypes.arrayOf(propTypes.element).isRequired
|
||||
})(({ children }) => (
|
||||
<Grid.Row className='form-group'>
|
||||
{children}
|
||||
</Grid.Row>
|
||||
))
|
||||
325
src/common/form/index.js
Normal file
325
src/common/form/index.js
Normal file
@@ -0,0 +1,325 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import classNames from 'classnames'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import randomPassword from 'random-password'
|
||||
import React from 'react'
|
||||
import round from 'lodash/round'
|
||||
import {
|
||||
DropdownButton,
|
||||
MenuItem
|
||||
} from 'react-bootstrap-4/lib'
|
||||
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import {
|
||||
firstDefined,
|
||||
formatSizeRaw,
|
||||
parseSize
|
||||
} from '../utils'
|
||||
|
||||
export Select from './select'
|
||||
export SelectPlainObject from './select-plain-object'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
enableGenerator: propTypes.bool
|
||||
})
|
||||
export class Password extends Component {
|
||||
get value () {
|
||||
return this.refs.field.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.field.value = value
|
||||
}
|
||||
|
||||
_generate = () => {
|
||||
this.refs.field.value = randomPassword(8)
|
||||
this.setState({
|
||||
visible: true
|
||||
})
|
||||
}
|
||||
|
||||
_toggleVisibility = () => {
|
||||
this.setState({
|
||||
visible: !this.state.visible
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
className,
|
||||
enableGenerator = false,
|
||||
...props
|
||||
} = this.props
|
||||
const { visible } = this.state
|
||||
|
||||
return <div className='input-group'>
|
||||
{enableGenerator && <span className='input-group-btn'>
|
||||
<button type='button' className='btn btn-secondary' onClick={this._generate}>
|
||||
<Icon icon='password' />
|
||||
</button>
|
||||
</span>}
|
||||
<input
|
||||
{...props}
|
||||
className={classNames(className, 'form-control')}
|
||||
ref='field'
|
||||
type={visible ? 'text' : 'password'}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<button type='button' className='btn btn-secondary' onClick={this._toggleVisibility}>
|
||||
<Icon icon={visible ? 'shown' : 'hidden'} />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
defaultValue: propTypes.number,
|
||||
max: propTypes.number.isRequired,
|
||||
min: propTypes.number.isRequired,
|
||||
step: propTypes.number,
|
||||
onChange: propTypes.func
|
||||
})
|
||||
export class Range extends Component {
|
||||
constructor (props) {
|
||||
super()
|
||||
this.state = {
|
||||
value: props.defaultValue || props.min
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.state.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.setState({
|
||||
value: +value
|
||||
})
|
||||
}
|
||||
|
||||
_handleChange = event => {
|
||||
const { onChange } = this.props
|
||||
const { value } = event.target
|
||||
|
||||
if (value === this.state.value) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
value
|
||||
}, onChange && (() => onChange(value)))
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
props
|
||||
} = this
|
||||
const step = props.step || 1
|
||||
const { value } = this.state
|
||||
|
||||
return (
|
||||
<div className='form-group row'>
|
||||
<label className='col-sm-2 control-label'>
|
||||
{value}
|
||||
</label>
|
||||
<div className='col-sm-10'>
|
||||
<input
|
||||
className='form-control'
|
||||
type='range'
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export Toggle from './toggle'
|
||||
|
||||
const UNITS = ['kiB', 'MiB', 'GiB']
|
||||
const DEFAULT_UNIT = 'GiB'
|
||||
@propTypes({
|
||||
autoFocus: propTypes.bool,
|
||||
className: propTypes.string,
|
||||
defaultUnit: propTypes.oneOf(UNITS),
|
||||
defaultValue: propTypes.number,
|
||||
onChange: propTypes.func,
|
||||
placeholder: propTypes.string,
|
||||
readOnly: propTypes.bool,
|
||||
required: propTypes.bool,
|
||||
style: propTypes.object,
|
||||
value: propTypes.oneOfType([
|
||||
propTypes.number,
|
||||
propTypes.oneOf([ null ])
|
||||
])
|
||||
})
|
||||
export class SizeInput extends BaseComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = this._createStateFromBytes(firstDefined(props.value, props.defaultValue, null))
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
const { value } = props
|
||||
if (value !== undefined && value !== this.props.value) {
|
||||
this.setState(this._createStateFromBytes(value))
|
||||
}
|
||||
}
|
||||
|
||||
_createStateFromBytes (bytes) {
|
||||
if (bytes === this._bytes) {
|
||||
return {
|
||||
input: this._input,
|
||||
unit: this._unit
|
||||
}
|
||||
}
|
||||
|
||||
if (bytes === null) {
|
||||
return {
|
||||
input: '',
|
||||
unit: this.props.defaultUnit || DEFAULT_UNIT
|
||||
}
|
||||
}
|
||||
|
||||
const { prefix, value } = formatSizeRaw(bytes)
|
||||
return {
|
||||
input: String(round(value, 2)),
|
||||
unit: `${prefix}B`
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { input, unit } = this.state
|
||||
|
||||
if (!input) {
|
||||
return null
|
||||
}
|
||||
|
||||
return parseSize(`${+input} ${unit}`)
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
this.props.value !== undefined
|
||||
) {
|
||||
throw new Error('cannot set value of controlled SizeInput')
|
||||
}
|
||||
this.setState(this._createStateFromBytes(value))
|
||||
}
|
||||
|
||||
_onChange (input, unit) {
|
||||
const { onChange } = this.props
|
||||
|
||||
// Empty input equals null.
|
||||
const bytes = input
|
||||
? parseSize(`${+input} ${unit}`)
|
||||
: null
|
||||
|
||||
const isControlled = this.props.value !== undefined
|
||||
if (isControlled) {
|
||||
// Store input and unit for this change to update correctly on new
|
||||
// props.
|
||||
this._bytes = bytes
|
||||
this._input = input
|
||||
this._unit = unit
|
||||
} else {
|
||||
this.setState({ input, unit })
|
||||
|
||||
// onChange is optional in uncontrolled mode.
|
||||
if (!onChange) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onChange(bytes)
|
||||
}
|
||||
|
||||
_updateNumber = event => {
|
||||
const input = event.target.value
|
||||
|
||||
if (!input) {
|
||||
return this._onChange(input, this.state.unit)
|
||||
}
|
||||
|
||||
const number = +input
|
||||
|
||||
// NaN: do not ack this change.
|
||||
if (number !== number) { // eslint-disable-line no-self-compare
|
||||
return
|
||||
}
|
||||
|
||||
// Same numeric value: simply update the input.
|
||||
const prevInput = this.state.input
|
||||
if (prevInput && +prevInput === number) {
|
||||
return this.setState({ input })
|
||||
}
|
||||
|
||||
this._onChange(input, this.state.unit)
|
||||
}
|
||||
|
||||
_updateUnit = unit => {
|
||||
const { input } = this.state
|
||||
|
||||
// 0 is always 0, no matter the unit.
|
||||
if (+input) {
|
||||
this._onChange(input, unit)
|
||||
} else {
|
||||
this.setState({ unit })
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
autoFocus,
|
||||
className,
|
||||
readOnly,
|
||||
placeholder,
|
||||
required,
|
||||
style
|
||||
} = this.props
|
||||
|
||||
return <span className={classNames('input-group', className)} style={style}>
|
||||
<input
|
||||
autoFocus={autoFocus}
|
||||
className='form-control'
|
||||
disabled={readOnly}
|
||||
onChange={this._updateNumber}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
type='text'
|
||||
value={this.state.input}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<DropdownButton
|
||||
bsStyle='secondary'
|
||||
id='size'
|
||||
pullRight
|
||||
disabled={readOnly}
|
||||
title={this.state.unit}
|
||||
>
|
||||
{map(UNITS, unit =>
|
||||
<MenuItem
|
||||
key={unit}
|
||||
onClick={() => this._updateUnit(unit)}
|
||||
>
|
||||
{unit}
|
||||
</MenuItem>
|
||||
)}
|
||||
</DropdownButton>
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
117
src/common/form/select-plain-object.js
Normal file
117
src/common/form/select-plain-object.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import find from 'lodash/find'
|
||||
import map from 'lodash/map'
|
||||
import React, { Component } from 'react'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
import Select from './select'
|
||||
|
||||
@propTypes({
|
||||
autoFocus: propTypes.bool,
|
||||
defaultValue: propTypes.any,
|
||||
disabled: propTypes.bool,
|
||||
optionRenderer: propTypes.func,
|
||||
multi: propTypes.bool,
|
||||
onChange: propTypes.func,
|
||||
options: propTypes.array,
|
||||
placeholder: propTypes.string,
|
||||
predicate: propTypes.func,
|
||||
required: propTypes.bool
|
||||
})
|
||||
export default class SelectPlainObject extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
value: this._computeValue(props.defaultValue, props)
|
||||
}
|
||||
}
|
||||
|
||||
_computeValue (value, props = this.props) {
|
||||
let { optionKey } = props
|
||||
optionKey || (optionKey = 'id')
|
||||
const reduceValue = value => value != null ? (value[optionKey] || value) : ''
|
||||
if (props.multi) {
|
||||
if (!Array.isArray(value)) {
|
||||
value = [value]
|
||||
}
|
||||
return map(value, reduceValue)
|
||||
}
|
||||
return reduceValue(value)
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { options } = this.props
|
||||
|
||||
this.setState({
|
||||
options: this._computeOptions(options)
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
const { options } = newProps
|
||||
|
||||
this.setState({
|
||||
options: this._computeOptions(options)
|
||||
})
|
||||
}
|
||||
|
||||
_computeOptions (options) {
|
||||
const { optionKey = 'id' } = this.props
|
||||
const { optionRenderer = o => o.label || o[optionKey] || o } = this.props
|
||||
return map(options, option => ({
|
||||
value: option[optionKey] || option,
|
||||
label: optionRenderer(option)
|
||||
}))
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { optionKey = 'id' } = this.props
|
||||
const { value } = this.state
|
||||
const { options } = this.props
|
||||
const pickValue = value => {
|
||||
value = value.value || value
|
||||
return find(options, option => option[optionKey] === value || option === value)
|
||||
}
|
||||
|
||||
if (this.props.multi) {
|
||||
return map(value, pickValue)
|
||||
}
|
||||
|
||||
return pickValue(value)
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.setState({
|
||||
value: this._computeValue(value)
|
||||
})
|
||||
}
|
||||
|
||||
_handleChange = value => {
|
||||
const { onChange } = this.props
|
||||
|
||||
this.setState({
|
||||
value: this._computeValue(value)
|
||||
}, onChange && (() => { onChange(this.value) }))
|
||||
}
|
||||
|
||||
_renderOption = option => option.label
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
|
||||
return (
|
||||
<Select
|
||||
autofocus={props.autoFocus}
|
||||
disabled={props.disabled}
|
||||
multi={props.multi}
|
||||
onChange={this._handleChange}
|
||||
openOnFocus
|
||||
optionRenderer={this._renderOption}
|
||||
options={state.options}
|
||||
placeholder={props.placeholder}
|
||||
required={props.required}
|
||||
value={state.value}
|
||||
valueRenderer={this._renderOption} />
|
||||
)
|
||||
}
|
||||
}
|
||||
117
src/common/form/select.js
Normal file
117
src/common/form/select.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import React, { Component } from 'react'
|
||||
import ReactSelect from 'react-select'
|
||||
import {
|
||||
AutoSizer,
|
||||
List
|
||||
} from 'react-virtualized'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
const SELECT_MENU_STYLE = {
|
||||
overflow: 'hidden'
|
||||
}
|
||||
|
||||
const SELECT_STYLE = {
|
||||
minWidth: '10em'
|
||||
}
|
||||
|
||||
// See: https://github.com/bvaughn/react-virtualized-select/blob/master/source/VirtualizedSelect/VirtualizedSelect.js
|
||||
@propTypes({
|
||||
maxHeight: propTypes.number,
|
||||
optionHeight: propTypes.number
|
||||
})
|
||||
export default class Select extends Component {
|
||||
static defaultProps = {
|
||||
maxHeight: 200,
|
||||
optionHeight: 40,
|
||||
optionRenderer: (option, labelKey) => option[labelKey]
|
||||
}
|
||||
|
||||
_renderMenu = ({
|
||||
focusedOption,
|
||||
options,
|
||||
...otherOptions
|
||||
}) => {
|
||||
const {
|
||||
maxHeight,
|
||||
optionHeight
|
||||
} = this.props
|
||||
|
||||
const focusedOptionIndex = options.indexOf(focusedOption)
|
||||
const height = Math.min(maxHeight, options.length * optionHeight)
|
||||
|
||||
const wrappedRowRenderer = ({ index, key, style }) =>
|
||||
this._optionRenderer({
|
||||
...otherOptions,
|
||||
focusedOption,
|
||||
focusedOptionIndex,
|
||||
key,
|
||||
option: options[index],
|
||||
options,
|
||||
style
|
||||
})
|
||||
|
||||
return (
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<List
|
||||
height={height}
|
||||
rowCount={options.length}
|
||||
rowHeight={optionHeight}
|
||||
rowRenderer={wrappedRowRenderer}
|
||||
scrollToIndex={focusedOptionIndex}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
)
|
||||
}
|
||||
|
||||
_optionRenderer = ({
|
||||
focusedOption,
|
||||
focusOption,
|
||||
key,
|
||||
labelKey,
|
||||
option,
|
||||
style,
|
||||
selectValue
|
||||
}) => {
|
||||
let className = 'Select-option'
|
||||
|
||||
if (option === focusedOption) {
|
||||
className += ' is-focused'
|
||||
}
|
||||
|
||||
const { disabled } = option
|
||||
|
||||
if (disabled) {
|
||||
className += ' is-disabled'
|
||||
}
|
||||
|
||||
const { props } = this
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={!disabled && (() => selectValue(option))}
|
||||
onMouseOver={!disabled && (() => focusOption(option))}
|
||||
style={{ ...style, height: props.optionHeight }}
|
||||
key={key}
|
||||
>
|
||||
{props.optionRenderer(option, labelKey)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<ReactSelect
|
||||
{...this.props}
|
||||
backspaceToRemoveMessage=''
|
||||
menuRenderer={this._renderMenu}
|
||||
menuStyle={SELECT_MENU_STYLE}
|
||||
style={SELECT_STYLE}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
3
src/common/form/toggle/index.css
Normal file
3
src/common/form/toggle/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.checkbox {
|
||||
display: none;
|
||||
}
|
||||
90
src/common/form/toggle/index.js
Normal file
90
src/common/form/toggle/index.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import Component from '../../base-component'
|
||||
import Icon from '../../icon'
|
||||
import propTypes from '../../prop-types'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@propTypes({
|
||||
className: propTypes.string,
|
||||
defaultValue: propTypes.bool,
|
||||
onChange: propTypes.func,
|
||||
icon: propTypes.string,
|
||||
iconOn: propTypes.string,
|
||||
iconOff: propTypes.string,
|
||||
iconSize: propTypes.number,
|
||||
value: propTypes.bool
|
||||
})
|
||||
export default class Toggle extends Component {
|
||||
static defaultProps = {
|
||||
iconOn: 'toggle-on',
|
||||
iconOff: 'toggle-off',
|
||||
iconSize: 2
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { props } = this
|
||||
|
||||
const { value } = props
|
||||
if (value != null) {
|
||||
return value
|
||||
}
|
||||
|
||||
const { input } = this.refs
|
||||
if (input) {
|
||||
return input.checked
|
||||
}
|
||||
|
||||
return props.defaultValue || false
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
this.props.value != null
|
||||
) {
|
||||
throw new Error('cannot set value of controlled Toggle')
|
||||
}
|
||||
|
||||
this.refs.input.checked = Boolean(value)
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
_onChange = event => {
|
||||
if (this.props.value == null) {
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
const { onChange } = this.props
|
||||
onChange && onChange(event.target.checked)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props, value } = this
|
||||
|
||||
return (
|
||||
<label
|
||||
className={classNames(
|
||||
props.disabled ? 'text-muted' : value ? 'text-success' : null,
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
icon={props.icon || (value ? props.iconOn : props.iconOff)}
|
||||
size={props.iconSize}
|
||||
/>
|
||||
<input
|
||||
checked={props.value}
|
||||
className={styles.checkbox}
|
||||
defaultChecked={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
type='checkbox'
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
}
|
||||
17
src/common/get-event-value.js
Normal file
17
src/common/get-event-value.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// If the param is an event, returns the value of it's target,
|
||||
// otherwise returns the param.
|
||||
const getEventValue = event => {
|
||||
let target
|
||||
if (!event || !(target = event.target)) {
|
||||
return event
|
||||
}
|
||||
|
||||
return (
|
||||
target.nodeName.toLowerCase() === 'input' &&
|
||||
target.type.toLowerCase() === 'checkbox'
|
||||
)
|
||||
? target.checked
|
||||
: target.value
|
||||
}
|
||||
|
||||
export { getEventValue as default }
|
||||
58
src/common/grid.js
Normal file
58
src/common/grid.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
|
||||
export const Col = propTypes({
|
||||
className: propTypes.string,
|
||||
size: propTypes.number,
|
||||
smallSize: propTypes.number,
|
||||
mediumSize: propTypes.number,
|
||||
largeSize: propTypes.number,
|
||||
offset: propTypes.number,
|
||||
smallOffset: propTypes.number,
|
||||
mediumOffset: propTypes.number,
|
||||
largeOffset: propTypes.number
|
||||
})(({
|
||||
children,
|
||||
className,
|
||||
size = 12,
|
||||
smallSize = size,
|
||||
mediumSize,
|
||||
largeSize,
|
||||
offset,
|
||||
smallOffset = offset,
|
||||
mediumOffset,
|
||||
largeOffset,
|
||||
style
|
||||
}) => <div className={classNames(
|
||||
className,
|
||||
smallSize && `col-xs-${smallSize}`,
|
||||
mediumSize && `col-md-${mediumSize}`,
|
||||
largeSize && `col-lg-${largeSize}`,
|
||||
smallOffset && `offset-xs-${smallOffset}`,
|
||||
mediumOffset && `offset-md-${mediumOffset}`,
|
||||
largeOffset && `offset-lg-${largeOffset}`
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</div>)
|
||||
|
||||
export const Container = propTypes({
|
||||
className: propTypes.string
|
||||
})(({
|
||||
children,
|
||||
className
|
||||
}) => <div className={classNames(className, 'container-fluid')}>
|
||||
{children}
|
||||
</div>)
|
||||
|
||||
export const Row = propTypes({
|
||||
className: propTypes.string
|
||||
})(({
|
||||
children,
|
||||
className
|
||||
}) => <div className={`${className || ''} row`}>
|
||||
{children}
|
||||
</div>)
|
||||
20
src/common/home-filters.js
Normal file
20
src/common/home-filters.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export const VM = {
|
||||
homeFilterPendingVms: 'current_operations:"" ',
|
||||
homeFilterNonRunningVms: '!power_state:running ',
|
||||
homeFilterHvmGuests: 'virtualizationMode:hvm ',
|
||||
homeFilterRunningVms: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const host = {
|
||||
homeFilterRunningHosts: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const pool = {
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const vmTemplate = {
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
226
src/common/hosts-patches-table.js
Normal file
226
src/common/hosts-patches-table.js
Normal file
@@ -0,0 +1,226 @@
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import { Portal } from 'react-overlays'
|
||||
|
||||
import _ from './intl'
|
||||
import ActionButton from './action-button'
|
||||
import Component from './base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Link from './link'
|
||||
import propTypes from './prop-types'
|
||||
import SortedTable from './sorted-table'
|
||||
import TabButton from './tab-button'
|
||||
import { connectStore } from './utils'
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
createFilter,
|
||||
createSelector
|
||||
} from './selectors'
|
||||
import {
|
||||
getHostMissingPatches,
|
||||
installAllHostPatches,
|
||||
installAllPatchesOnPool
|
||||
} from './xo'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const MISSING_PATCHES_COLUMNS = [
|
||||
{
|
||||
name: _('srHost'),
|
||||
itemRenderer: host => <Link to={`/hosts/${host.id}`}>{host.name_label}</Link>,
|
||||
sortCriteria: host => host.name_label
|
||||
},
|
||||
{
|
||||
name: _('hostDescription'),
|
||||
itemRenderer: host => host.name_description,
|
||||
sortCriteria: host => host.name_description
|
||||
},
|
||||
{
|
||||
name: _('hostMissingPatches'),
|
||||
itemRenderer: (host, { missingPatches }) => <Link to={`/hosts/${host.id}/patches`}>{missingPatches[host.id]}</Link>,
|
||||
sortCriteria: (host, { missingPatches }) => missingPatches[host.id]
|
||||
},
|
||||
{
|
||||
name: _('patchUpdateButton'),
|
||||
itemRenderer: (host, { installAllHostPatches }) => (
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
handler={installAllHostPatches}
|
||||
handlerParam={host}
|
||||
icon='host-patch-update'
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
const POOLS_MISSING_PATCHES_COLUMNS = [{
|
||||
name: _('srPool'),
|
||||
itemRenderer: (host, { pools }) => {
|
||||
const pool = pools[host.$pool]
|
||||
return <Link to={`/pools/${pool.id}`}>{pool.name_label}</Link>
|
||||
},
|
||||
sortCriteria: (host, { pools }) => pools[host.$pool].name_label
|
||||
}].concat(MISSING_PATCHES_COLUMNS)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class HostsPatchesTable extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state.missingPatches = {}
|
||||
}
|
||||
|
||||
_getHosts = createFilter(
|
||||
() => this.props.hosts,
|
||||
createSelector(
|
||||
() => this.state.missingPatches,
|
||||
missingPatches => host => missingPatches[host.id]
|
||||
)
|
||||
)
|
||||
|
||||
_refreshMissingPatches = () => (
|
||||
Promise.all(
|
||||
map(this.props.hosts, this._refreshHostMissingPatches)
|
||||
)
|
||||
)
|
||||
|
||||
_installAllMissingPatches = () => {
|
||||
const pools = {}
|
||||
forEach(this._getHosts(), host => {
|
||||
pools[host.$pool] = true
|
||||
})
|
||||
|
||||
return Promise.all(map(
|
||||
keys(pools),
|
||||
installAllPatchesOnPool
|
||||
)).then(this._refreshMissingPatches)
|
||||
}
|
||||
|
||||
_refreshHostMissingPatches = host => (
|
||||
getHostMissingPatches(host).then(patches => {
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[host.id]: patches.length
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
_installAllHostPatches = host => (
|
||||
installAllHostPatches(host).then(() =>
|
||||
this._refreshHostMissingPatches(host)
|
||||
)
|
||||
)
|
||||
|
||||
componentWillMount () {
|
||||
this._refreshMissingPatches()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
// Force one Portal refresh.
|
||||
// Because Portal cannot see the container reference at first rendering.
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
forEach(nextProps.hosts, host => {
|
||||
const { id } = host
|
||||
|
||||
if (this.state.missingPatches[id] !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[id]: 0
|
||||
}
|
||||
})
|
||||
|
||||
this._refreshHostMissingPatches(host)
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const hosts = this._getHosts()
|
||||
const noPatches = isEmpty(hosts)
|
||||
const { props } = this
|
||||
|
||||
const Container = props.container || 'div'
|
||||
const Button = props.useTabButton ? TabButton : ActionButton
|
||||
|
||||
const Buttons = (
|
||||
<Container>
|
||||
<Button
|
||||
btnStyle='secondary'
|
||||
handler={this._refreshMissingPatches}
|
||||
icon='refresh'
|
||||
labelId='refreshPatches'
|
||||
/>
|
||||
<Button
|
||||
btnStyle='primary'
|
||||
disabled={noPatches}
|
||||
handler={this._installAllMissingPatches}
|
||||
icon='host-patch-update'
|
||||
labelId='installPoolPatches'
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!noPatches
|
||||
? (
|
||||
<SortedTable
|
||||
collection={hosts}
|
||||
columns={props.displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
|
||||
userData={{
|
||||
installAllHostPatches: this._installAllHostPatches,
|
||||
missingPatches: this.state.missingPatches,
|
||||
pools: props.pools
|
||||
}}
|
||||
/>
|
||||
) : <p>{_('patchNothing')}</p>
|
||||
}
|
||||
<Portal container={() => props.buttonsGroupContainer()}>
|
||||
{Buttons}
|
||||
</Portal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@connectStore(() => {
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
|
||||
return {
|
||||
pools: getPools
|
||||
}
|
||||
})
|
||||
class HostsPatchesTableByPool extends Component {
|
||||
render () {
|
||||
const { props } = this
|
||||
return <HostsPatchesTable {...props} pools={props.pools} />
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default propTypes({
|
||||
buttonsGroupContainer: propTypes.func.isRequired,
|
||||
container: propTypes.any,
|
||||
displayPools: propTypes.bool,
|
||||
hosts: propTypes.oneOfType([
|
||||
propTypes.arrayOf(propTypes.object),
|
||||
propTypes.objectOf(propTypes.object)
|
||||
]).isRequired,
|
||||
useTabButton: propTypes.bool
|
||||
})(props => props.displayPools
|
||||
? <HostsPatchesTableByPool {...props} />
|
||||
: <HostsPatchesTable {...props} />
|
||||
)
|
||||
21
src/common/icon.js
Normal file
21
src/common/icon.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import classNames from 'classnames'
|
||||
import isInteger from 'lodash/isInteger'
|
||||
import React, { PropTypes } from 'react'
|
||||
|
||||
const Icon = ({ className, icon, size = 1, fixedWidth }) => (
|
||||
<i className={classNames(
|
||||
className,
|
||||
icon ? `xo-icon-${icon}` : 'fa', // Without icon prop, is a placeholder.
|
||||
isInteger(size) ? `fa-${size}x` : `fa-${size}`,
|
||||
fixedWidth && 'fa-fw'
|
||||
)} />
|
||||
)
|
||||
Icon.propTypes = {
|
||||
fixedWidth: PropTypes.bool,
|
||||
icon: PropTypes.string,
|
||||
size: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number
|
||||
])
|
||||
}
|
||||
export default Icon
|
||||
83
src/common/intl/index.js
Normal file
83
src/common/intl/index.js
Normal file
@@ -0,0 +1,83 @@
|
||||
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import isString from 'lodash/isString'
|
||||
import moment from 'moment'
|
||||
import React, {
|
||||
Component,
|
||||
PropTypes
|
||||
} from 'react'
|
||||
import {
|
||||
connect
|
||||
} from 'react-redux'
|
||||
import {
|
||||
FormattedMessage,
|
||||
IntlProvider as IntlProvider_
|
||||
} from 'react-intl'
|
||||
|
||||
import messages from './messages'
|
||||
import locales from './locales'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Params:
|
||||
//
|
||||
// - props (optional): properties to add to the FormattedMessage
|
||||
// - messageId: identifier of the message to format/translate
|
||||
// - values (optional): values to pass to the message
|
||||
// - render (optional): a function receiving the React nodes of the
|
||||
// translated message and returning the React node to render
|
||||
const getMessage = (props, messageId, values, render) => {
|
||||
if (isString(props)) {
|
||||
render = values
|
||||
values = messageId
|
||||
messageId = props
|
||||
props = undefined
|
||||
}
|
||||
|
||||
const message = messages[messageId]
|
||||
if (process.env.NODE_ENV !== 'production' && !message) {
|
||||
throw new Error(`no message defined for ${messageId}`)
|
||||
}
|
||||
|
||||
if (isFunction(values)) {
|
||||
render = values
|
||||
values = undefined
|
||||
}
|
||||
|
||||
return <FormattedMessage {...props} {...message} values={values}>
|
||||
{render}
|
||||
</FormattedMessage>
|
||||
}
|
||||
|
||||
export { getMessage as default }
|
||||
|
||||
export { messages }
|
||||
|
||||
@connect(({ lang }) => ({ lang }))
|
||||
export class IntlProvider extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
lang: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
render () {
|
||||
const { lang, children } = this.props
|
||||
return <IntlProvider_
|
||||
locale={lang}
|
||||
messages={locales[lang]}
|
||||
>
|
||||
{children}
|
||||
</IntlProvider_>
|
||||
}
|
||||
}
|
||||
|
||||
@connect(({ lang }) => ({ lang }))
|
||||
export class FormattedDuration extends Component {
|
||||
render () {
|
||||
const {
|
||||
duration,
|
||||
lang
|
||||
} = this.props
|
||||
return <span>{moment.duration(duration).locale(lang).humanize()}</span>
|
||||
}
|
||||
}
|
||||
1
src/common/intl/locales/.gitignore
vendored
Normal file
1
src/common/intl/locales/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/index.js
|
||||
0
src/common/intl/locales/.index-modules
Normal file
0
src/common/intl/locales/.index-modules
Normal file
2059
src/common/intl/locales/es.js
Normal file
2059
src/common/intl/locales/es.js
Normal file
File diff suppressed because it is too large
Load Diff
1139
src/common/intl/locales/fr.js
Normal file
1139
src/common/intl/locales/fr.js
Normal file
File diff suppressed because it is too large
Load Diff
2020
src/common/intl/locales/he.js
Normal file
2020
src/common/intl/locales/he.js
Normal file
File diff suppressed because it is too large
Load Diff
2026
src/common/intl/locales/pt.js
Normal file
2026
src/common/intl/locales/pt.js
Normal file
File diff suppressed because it is too large
Load Diff
2278
src/common/intl/locales/zh.js
Normal file
2278
src/common/intl/locales/zh.js
Normal file
File diff suppressed because it is too large
Load Diff
1152
src/common/intl/messages.js
Normal file
1152
src/common/intl/messages.js
Normal file
File diff suppressed because it is too large
Load Diff
33
src/common/invoke.js
Normal file
33
src/common/invoke.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// Invoke a function and returns it result.
|
||||
// All parameters are forwarded.
|
||||
//
|
||||
// Why using `invoke()`?
|
||||
// - avoid tedious IIFE syntax
|
||||
// - avoid declaring variables in the common scope
|
||||
// - monkey-patching
|
||||
//
|
||||
// ```js
|
||||
// const sum = invoke(1, 2, (a, b) => a + b)
|
||||
//
|
||||
// eventEmitter.emit = invoke(eventEmitter.emit, emit => function (event) {
|
||||
// if (event === 'foo') {
|
||||
// throw new Error('event foo is disabled')
|
||||
// }
|
||||
//
|
||||
// return emit.apply(this, arguments)
|
||||
// })
|
||||
// ```
|
||||
export default function invoke (fn) {
|
||||
const n = arguments.length - 1
|
||||
if (!n) {
|
||||
return fn()
|
||||
}
|
||||
|
||||
fn = arguments[n]
|
||||
const args = new Array(n)
|
||||
for (let i = 0; i < n; ++i) {
|
||||
args[i] = arguments[i]
|
||||
}
|
||||
|
||||
return fn.apply(undefined, args)
|
||||
}
|
||||
126
src/common/ip.js
Normal file
126
src/common/ip.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import forEachRight from 'lodash/forEachRight'
|
||||
import forEach from 'lodash/forEach'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isIp from 'is-ip'
|
||||
import some from 'lodash/some'
|
||||
|
||||
export { isIp }
|
||||
export const isIpV4 = isIp.v4
|
||||
export const isIpV6 = isIp.v6
|
||||
|
||||
// Source: https://github.com/ezpaarse-project/ip-range-generator/blob/master/index.js
|
||||
|
||||
const ipv4 = /^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(?:\.(?!$)|$)){4}$/
|
||||
|
||||
function ip2hex (ip) {
|
||||
let parts = ip.split('.').map(str => parseInt(str, 10))
|
||||
let n = 0
|
||||
|
||||
n += parts[3]
|
||||
n += parts[2] * 256 // 2^8
|
||||
n += parts[1] * 65536 // 2^16
|
||||
n += parts[0] * 16777216 // 2^24
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
function assertIpv4 (str, msg) {
|
||||
if (!ipv4.test(str)) { throw new Error(msg) }
|
||||
}
|
||||
|
||||
function *range (ip1, ip2) {
|
||||
assertIpv4(ip1, 'argument "ip1" must be a valid IPv4 address')
|
||||
assertIpv4(ip2, 'argument "ip2" must be a valid IPv4 address')
|
||||
|
||||
let hex = ip2hex(ip1)
|
||||
let hex2 = ip2hex(ip2)
|
||||
|
||||
if (hex > hex2) {
|
||||
let tmp = hex
|
||||
hex = hex2
|
||||
hex2 = tmp
|
||||
}
|
||||
|
||||
for (let i = hex; i <= hex2; i++) {
|
||||
yield `${(i >> 24) & 0xff}.${(i >> 16) & 0xff}.${(i >> 8) & 0xff}.${i & 0xff}`
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const getNextIpV4 = ip => {
|
||||
const splitIp = ip.split('.')
|
||||
if (splitIp.length !== 4 || some(splitIp, value => value < 0 || value > 255)) {
|
||||
return
|
||||
}
|
||||
let index
|
||||
forEachRight(splitIp, (value, i) => {
|
||||
if (value < 255) {
|
||||
index = i
|
||||
return false
|
||||
}
|
||||
splitIp[i] = 1
|
||||
})
|
||||
if (index === 0 && +splitIp[0] === 255) {
|
||||
return 0
|
||||
}
|
||||
splitIp[index]++
|
||||
|
||||
return splitIp.join('.')
|
||||
}
|
||||
|
||||
export const formatIps = ips => {
|
||||
if (!isArray(ips)) {
|
||||
throw new Error('ips must be an array')
|
||||
}
|
||||
if (ips.length === 0) {
|
||||
return []
|
||||
}
|
||||
const sortedIps = ips.sort((ip1, ip2) => {
|
||||
const splitIp1 = ip1.split('.')
|
||||
const splitIp2 = ip2.split('.')
|
||||
if (splitIp1.length !== 4) {
|
||||
return 1
|
||||
}
|
||||
if (splitIp2.length !== 4) {
|
||||
return -1
|
||||
}
|
||||
return splitIp1[3] - splitIp2[3] +
|
||||
(splitIp1[2] - splitIp2[2]) * 256 +
|
||||
(splitIp1[1] - splitIp2[1]) * 256 * 256 +
|
||||
(splitIp1[0] - splitIp2[0]) * 256 * 256 * 256
|
||||
})
|
||||
const range = { first: '', last: '' }
|
||||
const formattedIps = []
|
||||
let index = 0
|
||||
forEach(sortedIps, ip => {
|
||||
if (ip !== getNextIpV4(range.last)) {
|
||||
if (range.first) {
|
||||
formattedIps[index] = range.first === range.last ? range.first : { ...range }
|
||||
index++
|
||||
}
|
||||
range.first = range.last = ip
|
||||
} else {
|
||||
range.last = ip
|
||||
}
|
||||
})
|
||||
formattedIps[index] = range.first === range.last ? range.first : range
|
||||
|
||||
return formattedIps
|
||||
}
|
||||
|
||||
export const parseIpPattern = pattern => {
|
||||
const ips = []
|
||||
forEach(pattern.split(';'), rawIpRange => {
|
||||
const ipRange = rawIpRange.split('-')
|
||||
if (ipRange.length < 2) {
|
||||
ips.push(ipRange[0])
|
||||
} else if (!isIpV4(ipRange[0]) || !isIpV4(ipRange[1])) {
|
||||
ips.push(rawIpRange)
|
||||
} else {
|
||||
ips.push(...range(ipRange[0], ipRange[1]))
|
||||
}
|
||||
})
|
||||
|
||||
return ips
|
||||
}
|
||||
84
src/common/iso-device.js
Normal file
84
src/common/iso-device.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from 'react'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import { connectStore } from './utils'
|
||||
import { SelectVdi } from './select-objects'
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
createFinder,
|
||||
createGetObject,
|
||||
createSelector
|
||||
} from './selectors'
|
||||
import {
|
||||
ejectCd,
|
||||
insertCd
|
||||
} from './xo'
|
||||
|
||||
@propTypes({
|
||||
vm: propTypes.object.isRequired
|
||||
})
|
||||
@connectStore(() => {
|
||||
const getCdDrive = createFinder(
|
||||
createGetObjectsOfType('VBD').pick(
|
||||
(_, { vm }) => vm.$VBDs
|
||||
),
|
||||
[ vbd => vbd.is_cd_drive ]
|
||||
)
|
||||
|
||||
const getMountedIso = createGetObject(
|
||||
(state, props) => {
|
||||
const cdDrive = getCdDrive(state, props)
|
||||
if (cdDrive) {
|
||||
return cdDrive.VDI
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
cdDrive: getCdDrive,
|
||||
mountedIso: getMountedIso
|
||||
}
|
||||
})
|
||||
export default class IsoDevice extends Component {
|
||||
_getPredicate = createSelector(
|
||||
() => this.props.vm.$pool,
|
||||
poolId => sr => sr.$pool === poolId && sr.SR_type === 'iso'
|
||||
)
|
||||
|
||||
_handleInsert = iso => {
|
||||
const { vm } = this.props
|
||||
|
||||
if (iso) {
|
||||
insertCd(vm, iso.id, true)
|
||||
} else {
|
||||
ejectCd(vm)
|
||||
}
|
||||
}
|
||||
|
||||
_handleEject = () => ejectCd(this.props.vm)
|
||||
|
||||
render () {
|
||||
const { mountedIso } = this.props
|
||||
|
||||
return (
|
||||
<div className='input-group'>
|
||||
<SelectVdi
|
||||
srPredicate={this._getPredicate()}
|
||||
onChange={this._handleInsert}
|
||||
ref='selectIso'
|
||||
value={mountedIso}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<ActionButton
|
||||
btnStyle='secondary'
|
||||
disabled={!mountedIso}
|
||||
handler={this._handleEject}
|
||||
icon='vm-eject'
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
26
src/common/json-schema-input/abstract-input.js
Normal file
26
src/common/json-schema-input/abstract-input.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Component } from 'react'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
onChange: propTypes.func,
|
||||
placeholder: propTypes.string,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.any
|
||||
})
|
||||
export default class AbstractInput extends Component {
|
||||
set value (value) {
|
||||
this.refs.input.value = value === undefined ? '' : String(value)
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { value } = this.refs.input
|
||||
return !value ? undefined : value
|
||||
}
|
||||
}
|
||||
186
src/common/json-schema-input/array-input.js
Normal file
186
src/common/json-schema-input/array-input.js
Normal file
@@ -0,0 +1,186 @@
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
import map from 'lodash/map'
|
||||
import filter from 'lodash/filter'
|
||||
|
||||
import _ from '../intl'
|
||||
import propTypes from '../prop-types'
|
||||
import { propsEqual } from '../utils'
|
||||
|
||||
import GenericInput from './generic-input'
|
||||
import {
|
||||
descriptionRender,
|
||||
forceDisplayOptionalAttr
|
||||
} from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class ArrayItem extends Component {
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children } = this.props
|
||||
|
||||
return (
|
||||
<li className='list-group-item clearfix'>
|
||||
{cloneElement(children, {
|
||||
ref: 'input'
|
||||
})}
|
||||
<button disabled={children.props.disabled} className='btn btn-danger pull-xs-right' type='button' onClick={this.props.onDelete}>
|
||||
{_('remove')}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.array
|
||||
})
|
||||
export default class ArrayInput extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
use: props.required || forceDisplayOptionalAttr(props),
|
||||
children: this._makeChildren(props)
|
||||
}
|
||||
this._nextChildKey = 0
|
||||
}
|
||||
|
||||
get value () {
|
||||
if (this.state.use) {
|
||||
return map(this.refs, 'value')
|
||||
}
|
||||
}
|
||||
|
||||
set value (value = []) {
|
||||
this.setState({
|
||||
children: this._makeChildren({ ...this.props, value })
|
||||
})
|
||||
}
|
||||
|
||||
_handleOptionalChange = event => {
|
||||
this.setState({
|
||||
use: event.target.checked
|
||||
})
|
||||
}
|
||||
|
||||
_handleAdd = () => {
|
||||
const { children } = this.state
|
||||
this.setState({
|
||||
children: children.concat(this._makeChild(this.props))
|
||||
})
|
||||
}
|
||||
|
||||
_remove (key) {
|
||||
this.setState({
|
||||
children: filter(this.state.children, child => child.key !== key)
|
||||
})
|
||||
}
|
||||
|
||||
_makeChild (props) {
|
||||
const key = String(this._nextChildKey++)
|
||||
const {
|
||||
schema: {
|
||||
items
|
||||
}
|
||||
} = props
|
||||
|
||||
return (
|
||||
<ArrayItem key={key} onDelete={() => { this._remove(key) }}>
|
||||
<GenericInput
|
||||
depth={props.depth}
|
||||
disabled={props.disabled}
|
||||
label={items.title || _('item')}
|
||||
required
|
||||
schema={items}
|
||||
uiSchema={props.uiSchema.items}
|
||||
defaultValue={props.defaultValue}
|
||||
/>
|
||||
</ArrayItem>
|
||||
)
|
||||
}
|
||||
|
||||
_makeChildren ({ defaultValue, ...props }) {
|
||||
return map(defaultValue, defaultValue => {
|
||||
return (
|
||||
this._makeChild({
|
||||
...props,
|
||||
defaultValue
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (
|
||||
!propsEqual(
|
||||
this.props,
|
||||
props,
|
||||
[ 'depth', 'disabled', 'label', 'required', 'schema', 'uiSchema' ]
|
||||
)
|
||||
) {
|
||||
this.setState({
|
||||
children: this._makeChildren(props)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
props,
|
||||
state
|
||||
} = this
|
||||
const {
|
||||
disabled,
|
||||
schema
|
||||
} = props
|
||||
const { use } = state
|
||||
const depth = props.depth || 0
|
||||
|
||||
return (
|
||||
<div style={{'paddingLeft': `${depth}em`}}>
|
||||
<legend>{props.label}</legend>
|
||||
{descriptionRender(schema.description)}
|
||||
<hr />
|
||||
{!props.required &&
|
||||
<div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={disabled}
|
||||
onChange={this._handleOptionalChange}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
{use &&
|
||||
<div className={'card-block'}>
|
||||
<ul style={{'paddingLeft': 0}} >
|
||||
{map(this.state.children, (child, index) =>
|
||||
cloneElement(child, { ref: index })
|
||||
)}
|
||||
</ul>
|
||||
<button disabled={disabled} className='btn btn-primary pull-xs-right m-t-1 m-r-1' type='button' onClick={this._handleAdd}>
|
||||
{_('add')}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
35
src/common/json-schema-input/boolean-input.js
Normal file
35
src/common/json-schema-input/boolean-input.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
import { Toggle } from 'form'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class BooleanInput extends AbstractInput {
|
||||
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<div className='checkbox form-control'>
|
||||
<Toggle
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
/>
|
||||
</div>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
36
src/common/json-schema-input/enum-input.js
Normal file
36
src/common/json-schema-input/enum-input.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import _ from 'intl'
|
||||
import map from 'lodash/map'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class EnumInput extends AbstractInput {
|
||||
render () {
|
||||
const { props } = this
|
||||
const {
|
||||
onChange,
|
||||
required
|
||||
} = props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<select
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue || ''}
|
||||
disabled={props.disabled}
|
||||
onChange={onChange && (event => onChange(event.target.value))}
|
||||
ref='input'
|
||||
required={required}
|
||||
>
|
||||
{_('noSelectedValue', message => <option value=''>{message}</option>)}
|
||||
{map(props.schema.enum, (value, index) =>
|
||||
<option value={value} key={index}>{value}</option>
|
||||
)}
|
||||
</select>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
78
src/common/json-schema-input/generic-input.js
Normal file
78
src/common/json-schema-input/generic-input.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import React, { Component } from 'react'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
import { EMPTY_OBJECT } from '../utils'
|
||||
|
||||
import ArrayInput from './array-input'
|
||||
import BooleanInput from './boolean-input'
|
||||
import EnumInput from './enum-input'
|
||||
import IntegerInput from './integer-input'
|
||||
import NumberInput from './number-input'
|
||||
import ObjectInput from './object-input'
|
||||
import StringInput from './string-input'
|
||||
|
||||
import { getType } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const InputByType = {
|
||||
array: ArrayInput,
|
||||
boolean: BooleanInput,
|
||||
integer: IntegerInput,
|
||||
number: NumberInput,
|
||||
object: ObjectInput,
|
||||
string: StringInput
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
onChange: propTypes.func,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.any
|
||||
})
|
||||
export default class GenericInput extends Component {
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
schema,
|
||||
defaultValue = schema.default,
|
||||
uiSchema = EMPTY_OBJECT,
|
||||
...opts
|
||||
} = this.props
|
||||
|
||||
const props = {
|
||||
...opts,
|
||||
defaultValue,
|
||||
schema,
|
||||
uiSchema,
|
||||
ref: 'input'
|
||||
}
|
||||
|
||||
// Enum, special case.
|
||||
if (schema.enum) {
|
||||
return <EnumInput {...props} />
|
||||
}
|
||||
|
||||
const type = getType(schema)
|
||||
const Input = uiSchema.widget || InputByType[type.toLowerCase()]
|
||||
|
||||
if (!Input) {
|
||||
throw new Error(`Unsupported type: ${type}.`)
|
||||
}
|
||||
|
||||
return <Input {...props} {...uiSchema.config} />
|
||||
}
|
||||
}
|
||||
83
src/common/json-schema-input/helpers.js
Normal file
83
src/common/json-schema-input/helpers.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from 'react'
|
||||
import includes from 'lodash/includes'
|
||||
import isArray from 'lodash/isArray'
|
||||
import marked from 'marked'
|
||||
|
||||
import { Col, Row } from 'grid'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const getType = schema => {
|
||||
if (!schema) {
|
||||
return
|
||||
}
|
||||
|
||||
const type = schema.type
|
||||
|
||||
if (isArray(type)) {
|
||||
if (includes(type, 'integer')) {
|
||||
return 'integer'
|
||||
}
|
||||
if (includes(type, 'number')) {
|
||||
return 'number'
|
||||
}
|
||||
|
||||
return 'string'
|
||||
}
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
export const getXoType = schema => {
|
||||
const type = schema && (schema['xo:type'] || schema.$type)
|
||||
|
||||
if (type) {
|
||||
return type.toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const descriptionRender = description =>
|
||||
<span className='text-muted' dangerouslySetInnerHTML={{__html: marked(description || '')}} />
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const PrimitiveInputWrapper = ({ label, required = false, schema, children }) => (
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<div className='input-group'>
|
||||
<span className='input-group-addon'>
|
||||
{label}
|
||||
{required && <span className='text-warning'>*</span>}
|
||||
</span>
|
||||
{children}
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
{descriptionRender(schema.description)}
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const forceDisplayOptionalAttr = ({ schema, defaultValue }) => {
|
||||
if (!schema || !defaultValue) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Array
|
||||
if (schema.items && Array.isArray(defaultValue)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Object
|
||||
for (const key in schema.properties) {
|
||||
if (defaultValue[key]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
1
src/common/json-schema-input/index.js
Normal file
1
src/common/json-schema-input/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export default from './generic-input'
|
||||
42
src/common/json-schema-input/integer-input.js
Normal file
42
src/common/json-schema-input/integer-input.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import Combobox from '../combobox'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class IntegerInput extends AbstractInput {
|
||||
get value () {
|
||||
const { value } = this.refs.input
|
||||
return !value ? undefined : +value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
// Getter/Setter are always inherited together.
|
||||
// `get value` is defined in the subclass, so `set value`
|
||||
// must be defined too.
|
||||
super.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { schema } = props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
step={1}
|
||||
type='number'
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
42
src/common/json-schema-input/number-input.js
Normal file
42
src/common/json-schema-input/number-input.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import Combobox from '../combobox'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class NumberInput extends AbstractInput {
|
||||
get value () {
|
||||
const { value } = this.refs.input
|
||||
return !value ? undefined : +value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
// Getter/Setter are always inherited together.
|
||||
// `get value` is defined in the subclass, so `set value`
|
||||
// must be defined too.
|
||||
super.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { schema } = props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
step='any'
|
||||
type='number'
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
164
src/common/json-schema-input/object-input.js
Normal file
164
src/common/json-schema-input/object-input.js
Normal file
@@ -0,0 +1,164 @@
|
||||
import _ from 'intl'
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
import forEach from 'lodash/forEach'
|
||||
import includes from 'lodash/includes'
|
||||
import map from 'lodash/map'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
import { propsEqual } from '../utils'
|
||||
|
||||
import GenericInput from './generic-input'
|
||||
|
||||
import {
|
||||
descriptionRender,
|
||||
forceDisplayOptionalAttr
|
||||
} from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class ObjectItem extends Component {
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
|
||||
return (
|
||||
<div className='p-b-1'>
|
||||
{cloneElement(props.children, {
|
||||
ref: 'input'
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.object
|
||||
})
|
||||
export default class ObjectInput extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
use: Boolean(props.required) || forceDisplayOptionalAttr(props),
|
||||
children: this._makeChildren(props)
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
if (!this.state.use) {
|
||||
return
|
||||
}
|
||||
|
||||
const obj = {}
|
||||
|
||||
forEach(this.refs, (instance, key) => {
|
||||
obj[key] = instance.value
|
||||
})
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
set value (value = {}) {
|
||||
forEach(this.refs, (instance, id) => {
|
||||
instance.value = value[id]
|
||||
})
|
||||
}
|
||||
|
||||
_handleOptionalChange = event => {
|
||||
const { checked } = event.target
|
||||
|
||||
this.setState({
|
||||
use: checked
|
||||
})
|
||||
}
|
||||
|
||||
_makeChildren (props) {
|
||||
const {
|
||||
depth = 0,
|
||||
schema,
|
||||
uiSchema = {},
|
||||
defaultValue = {}
|
||||
} = props
|
||||
const obj = {}
|
||||
const { properties } = uiSchema
|
||||
|
||||
forEach(schema.properties, (childSchema, key) => {
|
||||
obj[key] = (
|
||||
<ObjectItem key={key}>
|
||||
<GenericInput
|
||||
depth={depth + 2}
|
||||
disabled={props.disabled}
|
||||
label={childSchema.title || key}
|
||||
required={includes(schema.required, key)}
|
||||
schema={childSchema}
|
||||
uiSchema={properties && properties[key]}
|
||||
defaultValue={defaultValue[key]}
|
||||
/>
|
||||
</ObjectItem>
|
||||
)
|
||||
})
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (
|
||||
!propsEqual(
|
||||
this.props,
|
||||
props,
|
||||
[ 'depth', 'disabled', 'label', 'required', 'schema', 'uiSchema' ]
|
||||
)
|
||||
) {
|
||||
this.setState({
|
||||
children: this._makeChildren(props)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const { use } = state
|
||||
const depth = props.depth || 0
|
||||
|
||||
return (
|
||||
<div style={{'paddingLeft': `${depth}em`}}>
|
||||
<legend>{props.label}</legend>
|
||||
{descriptionRender(props.schema.description)}
|
||||
<hr />
|
||||
{!props.required &&
|
||||
<div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={props.disabled}
|
||||
onChange={this._handleOptionalChange}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
{use &&
|
||||
<div className='card-block'>
|
||||
{map(state.children, (child, index) =>
|
||||
cloneElement(child, { ref: index })
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
33
src/common/json-schema-input/string-input.js
Normal file
33
src/common/json-schema-input/string-input.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import Combobox from '../combobox'
|
||||
import propTypes from '../prop-types'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
password: propTypes.bool
|
||||
})
|
||||
export default class StringInput extends AbstractInput {
|
||||
render () {
|
||||
const { props } = this
|
||||
const { schema } = props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
type={props.password && 'password'}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
59
src/common/link.js
Normal file
59
src/common/link.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import Link from 'react-router/lib/Link'
|
||||
import React from 'react'
|
||||
import { routerShape } from 'react-router/lib/PropTypes'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export { Link as default }
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const _IGNORED_TAGNAMES = {
|
||||
A: true,
|
||||
BUTTON: true,
|
||||
INPUT: true,
|
||||
SELECT: true
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
tagName: propTypes.string
|
||||
})
|
||||
export class BlockLink extends Component {
|
||||
static contextTypes = {
|
||||
router: routerShape
|
||||
}
|
||||
|
||||
_style = { cursor: 'pointer' }
|
||||
_onClickCapture = event => {
|
||||
const { currentTarget } = event
|
||||
let element = event.target
|
||||
while (element !== currentTarget) {
|
||||
if (_IGNORED_TAGNAMES[element.tagName]) {
|
||||
return
|
||||
}
|
||||
element = element.parentNode
|
||||
}
|
||||
event.stopPropagation()
|
||||
if (event.ctrlKey || event.button === 1) {
|
||||
window.open(this.context.router.createHref(this.props.to))
|
||||
} else {
|
||||
this.context.router.push(this.props.to)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, tagName = 'div' } = this.props
|
||||
const Component = tagName
|
||||
return (
|
||||
<Component
|
||||
style={this._style}
|
||||
onClickCapture={this._onClickCapture}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
}
|
||||
13
src/common/log-error.js
Normal file
13
src/common/log-error.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// Logs an error properly, correctly use the source map for the stack.
|
||||
//
|
||||
// This is achieved by throwing the error asynchronously.
|
||||
const logError = (error, ...args) => {
|
||||
setTimeout(() => {
|
||||
if (args.length) {
|
||||
console.error(...args)
|
||||
}
|
||||
|
||||
throw error
|
||||
}, 0)
|
||||
}
|
||||
export { logError as default }
|
||||
171
src/common/modal.js
Normal file
171
src/common/modal.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import _ from 'intl'
|
||||
import Icon from 'icon'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isString from 'lodash/isString'
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
import { Button, Modal as ReactModal } from 'react-bootstrap-4/lib'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
|
||||
let instance
|
||||
|
||||
const modal = (content, onClose) => {
|
||||
if (!instance) {
|
||||
throw new Error('No modal instance.')
|
||||
} else if (instance.state.showModal) {
|
||||
throw new Error('Other modal still open.')
|
||||
}
|
||||
instance.setState({ content, onClose, showModal: true })
|
||||
}
|
||||
|
||||
export const alert = (title, body) => {
|
||||
return new Promise(resolve => {
|
||||
const { Body, Footer, Header, Title } = ReactModal
|
||||
modal(
|
||||
<div>
|
||||
<Header closeButton>
|
||||
<Title>{title}</Title>
|
||||
</Header>
|
||||
<Body>{body}</Body>
|
||||
<Footer>
|
||||
<Button bsStyle='primary' onClick={() => {
|
||||
resolve()
|
||||
instance.close()
|
||||
}}>
|
||||
{_('alertOk')}
|
||||
</Button>
|
||||
</Footer>
|
||||
</div>,
|
||||
resolve
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const _addRef = (component, ref) => {
|
||||
if (isString(component) || isArray(component)) {
|
||||
return component
|
||||
}
|
||||
|
||||
try {
|
||||
return cloneElement(component, { ref })
|
||||
} catch (_) {} // Stateless component.
|
||||
return component
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
children: propTypes.node.isRequired,
|
||||
title: propTypes.node.isRequired,
|
||||
icon: propTypes.string
|
||||
})
|
||||
class Confirm extends Component {
|
||||
_resolve = () => {
|
||||
const { body } = this.refs
|
||||
this.props.resolve(body && (body.getWrappedInstance
|
||||
? body.getWrappedInstance().value
|
||||
: body.value
|
||||
))
|
||||
instance.close()
|
||||
}
|
||||
_reject = () => {
|
||||
this.props.reject()
|
||||
instance.close()
|
||||
}
|
||||
|
||||
_style = { marginRight: '0.5em' }
|
||||
|
||||
render () {
|
||||
const { Body, Footer, Header, Title } = ReactModal
|
||||
const { title, icon } = this.props
|
||||
|
||||
const body = _addRef(this.props.children, 'body')
|
||||
|
||||
return <div>
|
||||
<Header closeButton>
|
||||
<Title>
|
||||
{icon
|
||||
? <span><Icon icon={icon} /> {title}</span>
|
||||
: title
|
||||
}
|
||||
</Title>
|
||||
</Header>
|
||||
<Body>
|
||||
{body}
|
||||
</Body>
|
||||
<Footer>
|
||||
<Button
|
||||
bsStyle='primary'
|
||||
onClick={this._resolve}
|
||||
style={this._style}
|
||||
>
|
||||
{_('confirmOk')}
|
||||
</Button>
|
||||
<Button
|
||||
bsStyle='secondary'
|
||||
onClick={this._reject}
|
||||
>
|
||||
{_('confirmCancel')}
|
||||
</Button>
|
||||
</Footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export const confirm = ({
|
||||
body,
|
||||
title,
|
||||
icon = 'alarm'
|
||||
}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
modal(
|
||||
<Confirm
|
||||
title={title}
|
||||
resolve={resolve}
|
||||
reject={reject}
|
||||
icon={icon}
|
||||
>
|
||||
{body}
|
||||
</Confirm>,
|
||||
reject
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export default class Modal extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
if (instance) {
|
||||
throw new Error('Modal is a singleton!')
|
||||
}
|
||||
instance = this
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.setState({ showModal: false })
|
||||
}
|
||||
|
||||
close () {
|
||||
this.setState({ showModal: false })
|
||||
}
|
||||
|
||||
_onHide = () => {
|
||||
this.close()
|
||||
|
||||
const { onClose } = this.state
|
||||
onClose && onClose()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { showModal } = this.state
|
||||
/* TODO: remove this work-around and use
|
||||
* ReactModal.Body, ReactModal.Header, ...
|
||||
* after this issue has been fixed:
|
||||
* https://phabricator.babeljs.io/T6976
|
||||
*/
|
||||
return (
|
||||
<ReactModal show={showModal} onHide={this._onHide}>
|
||||
{this.state.content}
|
||||
</ReactModal>
|
||||
)
|
||||
}
|
||||
}
|
||||
18
src/common/nav.js
Normal file
18
src/common/nav.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import Link from './link'
|
||||
|
||||
export const NavLink = ({ children, to }) => (
|
||||
<li className='nav-item' role='tab'>
|
||||
<Link className='nav-link' activeClassName='active' to={to}>
|
||||
{children}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
|
||||
export const NavTabs = ({ children, className }) => (
|
||||
<ul className={classNames(className, 'nav nav-tabs')} role='tablist'>
|
||||
{children}
|
||||
</ul>
|
||||
)
|
||||
55
src/common/notification.js
Normal file
55
src/common/notification.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { Component } from 'react'
|
||||
import ReactNotify from 'react-notify'
|
||||
|
||||
let instance
|
||||
|
||||
export let error
|
||||
export let info
|
||||
export let success
|
||||
|
||||
export class Notification extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
if (instance) {
|
||||
throw new Error('Notification is a singleton!')
|
||||
}
|
||||
instance = this
|
||||
}
|
||||
|
||||
// This special component never have to rerender!
|
||||
shouldComponentUpdate () {
|
||||
return false
|
||||
}
|
||||
|
||||
render () {
|
||||
return <ReactNotify ref={notification => {
|
||||
if (!notification) {
|
||||
return
|
||||
}
|
||||
|
||||
error = (title, body) => notification.error(title, body, 3e3)
|
||||
info = (title, body) => notification.info(title, body, 3e3)
|
||||
success = (title, body) => notification.success(title, body, 3e3)
|
||||
}} />
|
||||
}
|
||||
}
|
||||
|
||||
export { info as default }
|
||||
|
||||
/* Example:
|
||||
|
||||
import info, { success, error } from 'notification'
|
||||
|
||||
<button onClick={() => info('Info', 'This is an info notification')}>
|
||||
Info notification
|
||||
</button>
|
||||
|
||||
<button onClick={() => success('Success', 'This is a success notification')}>
|
||||
Success notification
|
||||
</button>
|
||||
|
||||
<button onClick={() => error('Error', 'This is an error notification')}>
|
||||
Error notification
|
||||
</button>
|
||||
*/
|
||||
15
src/common/object-name.js
Normal file
15
src/common/object-name.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/** EXPERIMENT: this is here to avoid a littel code dupplication, but is not admitted as a highly recommendable component */
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObject } from 'selectors'
|
||||
import React, { Component } from 'react'
|
||||
|
||||
@connectStore(() => {
|
||||
const object = createGetObject()
|
||||
return (state, props) => ({object: object(state, props)})
|
||||
})
|
||||
export default class ObjectName extends Component {
|
||||
render () {
|
||||
const { object } = this.props
|
||||
return <span>{object && object.name_label}</span>
|
||||
}
|
||||
}
|
||||
22
src/common/prop-types.js
Normal file
22
src/common/prop-types.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import assign from 'lodash/assign'
|
||||
import { PropTypes } from 'react'
|
||||
|
||||
// Decorators to help declaring on React components without using the
|
||||
// tedious static properties syntax.
|
||||
//
|
||||
// ```js
|
||||
// @propTypes({
|
||||
// children: propTypes.node.isRequired
|
||||
// })
|
||||
// class MyComponent extends React.Component {}
|
||||
// ```
|
||||
const propTypes = types => target => {
|
||||
target.propTypes = {
|
||||
...target.propTypes,
|
||||
...types
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
assign(propTypes, PropTypes)
|
||||
export { propTypes as default }
|
||||
151
src/common/react-novnc.js
vendored
Normal file
151
src/common/react-novnc.js
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
import React, { Component } from 'react'
|
||||
import { createBackoff } from 'jsonrpc-websocket-client'
|
||||
import { RFB } from 'novnc-node'
|
||||
import {
|
||||
format as formatUrl,
|
||||
parse as parseUrl,
|
||||
resolve as resolveUrl
|
||||
} from 'url'
|
||||
import { enable as enableShortcuts, disable as disableShortcuts } from 'shortcuts'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
|
||||
const parseRelativeUrl = url => parseUrl(resolveUrl(String(window.location), url))
|
||||
|
||||
const PROTOCOL_ALIASES = {
|
||||
'http:': 'ws:',
|
||||
'https:': 'wss:'
|
||||
}
|
||||
const fixProtocol = url => {
|
||||
const protocol = PROTOCOL_ALIASES[url.protocol]
|
||||
if (protocol) {
|
||||
url.protocol = protocol
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
onClipboardChange: propTypes.func,
|
||||
url: propTypes.string.isRequired
|
||||
})
|
||||
export default class NoVnc extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this._rfb = null
|
||||
this._retryGen = createBackoff(Infinity)
|
||||
|
||||
this._onUpdateState = (rfb, state) => {
|
||||
if (state === 'normal') {
|
||||
if (this._retryTimeout) {
|
||||
clearTimeout(this._retryTimeout)
|
||||
this._retryTimeout = undefined
|
||||
this._retryGen = createBackoff(Infinity)
|
||||
}
|
||||
}
|
||||
|
||||
if (state !== 'disconnected') {
|
||||
return
|
||||
}
|
||||
|
||||
clearTimeout(this._retryTimeout)
|
||||
this._retryTimeout = setTimeout(this._connect, this._retryGen.next().value)
|
||||
}
|
||||
}
|
||||
|
||||
sendCtrlAltDel () {
|
||||
const rfb = this._rfb
|
||||
if (rfb) {
|
||||
rfb.sendCtrlAltDel()
|
||||
}
|
||||
}
|
||||
|
||||
setClipboard (text) {
|
||||
const rfb = this._rfb
|
||||
if (rfb) {
|
||||
rfb.clipboardPasteFrom(text)
|
||||
}
|
||||
}
|
||||
|
||||
_clean () {
|
||||
const rfb = this._rfb
|
||||
if (rfb) {
|
||||
this._rfb = null
|
||||
rfb.disconnect()
|
||||
}
|
||||
enableShortcuts()
|
||||
}
|
||||
|
||||
_connect = () => {
|
||||
this._clean()
|
||||
|
||||
const url = parseRelativeUrl(this.props.url)
|
||||
fixProtocol(url)
|
||||
|
||||
const isSecure = url.protocol === 'wss:'
|
||||
|
||||
const { onClipboardChange } = this.props
|
||||
const rfb = this._rfb = new RFB({
|
||||
encrypt: isSecure,
|
||||
target: this.refs.canvas,
|
||||
wsProtocols: [ 'chat' ],
|
||||
onClipboard: onClipboardChange && ((_, text) => {
|
||||
onClipboardChange(text)
|
||||
}),
|
||||
onUpdateState: this._onUpdateState
|
||||
})
|
||||
|
||||
rfb.connect(formatUrl(url))
|
||||
disableShortcuts()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._connect()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this._clean()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
const rfb = this._rfb
|
||||
if (rfb && this.props.scale !== props.scale) {
|
||||
rfb.get_display().set_scale(props.scale || 1)
|
||||
rfb.get_mouse().set_scale(props.scale || 1)
|
||||
}
|
||||
}
|
||||
|
||||
_focus = () => {
|
||||
const rfb = this._rfb
|
||||
if (rfb) {
|
||||
const { activeElement } = document
|
||||
if (activeElement) {
|
||||
activeElement.blur()
|
||||
}
|
||||
|
||||
rfb.get_keyboard().grab()
|
||||
rfb.get_mouse().grab()
|
||||
|
||||
disableShortcuts()
|
||||
}
|
||||
}
|
||||
|
||||
_unfocus = () => {
|
||||
const rfb = this._rfb
|
||||
if (rfb) {
|
||||
rfb.get_keyboard().ungrab()
|
||||
rfb.get_mouse().ungrab()
|
||||
|
||||
enableShortcuts()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return <canvas
|
||||
className='center-block'
|
||||
height='480'
|
||||
onMouseEnter={this._focus}
|
||||
onMouseLeave={this._unfocus}
|
||||
ref='canvas'
|
||||
width='640'
|
||||
/>
|
||||
}
|
||||
}
|
||||
223
src/common/render-xo-item.js
Normal file
223
src/common/render-xo-item.js
Normal file
@@ -0,0 +1,223 @@
|
||||
import React from 'react'
|
||||
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
import { createGetObject } from './selectors'
|
||||
import { isSrWritable } from './xo'
|
||||
import {
|
||||
connectStore,
|
||||
formatSize
|
||||
} from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const OBJECT_TYPE_TO_ICON = {
|
||||
'VM-template': 'vm',
|
||||
host: 'host',
|
||||
network: 'network'
|
||||
}
|
||||
|
||||
// Host, Network, VM-template.
|
||||
export const PoolObjectItem = propTypes({
|
||||
object: propTypes.object.isRequired
|
||||
})(connectStore(() => {
|
||||
const getPool = createGetObject(
|
||||
(_, props) => props.object.$pool
|
||||
)
|
||||
|
||||
return (state, props) => ({
|
||||
pool: getPool(state, props)
|
||||
})
|
||||
})(({ object, pool }) => {
|
||||
const icon = OBJECT_TYPE_TO_ICON[object.type]
|
||||
const { id } = object
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Icon icon={icon} /> {`${object.name_label || id} `}
|
||||
{pool && `(${pool.name_label || pool.id})`}
|
||||
</span>
|
||||
)
|
||||
}))
|
||||
|
||||
// SR.
|
||||
export const SrItem = propTypes({
|
||||
sr: propTypes.object.isRequired
|
||||
})(connectStore(() => {
|
||||
const getContainer = createGetObject(
|
||||
(_, props) => props.sr.$container
|
||||
)
|
||||
|
||||
return (state, props) => ({
|
||||
container: getContainer(state, props)
|
||||
})
|
||||
})(({ sr, container }) => {
|
||||
let label = `${sr.name_label || sr.id}`
|
||||
|
||||
if (isSrWritable(sr)) {
|
||||
label += ` (${formatSize(sr.size - sr.physical_usage)} free)`
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Icon icon='sr' /> {label}
|
||||
</span>
|
||||
)
|
||||
}))
|
||||
|
||||
// VM.
|
||||
export const VmItem = propTypes({
|
||||
vm: propTypes.object.isRequired
|
||||
})(connectStore(() => {
|
||||
const getContainer = createGetObject(
|
||||
(_, props) => props.vm.$container
|
||||
)
|
||||
|
||||
return (state, props) => ({
|
||||
container: getContainer(state, props)
|
||||
})
|
||||
})(({ vm, container }) => (
|
||||
<span>
|
||||
<Icon icon={`vm-${vm.power_state.toLowerCase()}`} /> {vm.name_label || vm.id}
|
||||
{container && ` (${container.name_label || container.id})`}
|
||||
</span>
|
||||
)))
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const xoItemToRender = {
|
||||
// Subscription objects.
|
||||
group: group => (
|
||||
<span>
|
||||
<Icon icon='group' /> {group.name}
|
||||
</span>
|
||||
),
|
||||
remote: remote => (
|
||||
<span>
|
||||
<Icon icon='remote' /> {remote.value.name}
|
||||
</span>
|
||||
),
|
||||
role: role => (
|
||||
<span>
|
||||
{role.name}
|
||||
</span>
|
||||
),
|
||||
user: user => (
|
||||
<span>
|
||||
<Icon icon='user' /> {user.email}
|
||||
</span>
|
||||
),
|
||||
resourceSet: resourceSet => (
|
||||
<span>
|
||||
<Icon icon='resource-set' /> {resourceSet.name}
|
||||
</span>
|
||||
),
|
||||
sshKey: key => (
|
||||
<span>
|
||||
<Icon icon='ssh-key' /> {key.label}
|
||||
</span>
|
||||
),
|
||||
ipPool: ipPool => (
|
||||
<span>
|
||||
<Icon icon='ip' /> {ipPool.name}
|
||||
</span>
|
||||
),
|
||||
ipAddress: ({label, used}) => {
|
||||
if (used) {
|
||||
return <strong className='text-warning'>{label}</strong>
|
||||
}
|
||||
return <span>{label}</span>
|
||||
},
|
||||
|
||||
// XO objects.
|
||||
pool: pool => (
|
||||
<span>
|
||||
<Icon icon='pool' /> {pool.name_label || pool.id}
|
||||
</span>
|
||||
),
|
||||
|
||||
VDI: vdi => (
|
||||
<span>
|
||||
<Icon icon='disk' /> {vdi.name_label} {vdi.name_description && <span> ({vdi.name_description})</span>}
|
||||
</span>
|
||||
),
|
||||
|
||||
// Pool objects.
|
||||
'VM-template': vmTemplate => <PoolObjectItem object={vmTemplate} />,
|
||||
host: host => <PoolObjectItem object={host} />,
|
||||
network: network => <PoolObjectItem object={network} />,
|
||||
|
||||
// SR.
|
||||
SR: sr => <SrItem sr={sr} />,
|
||||
|
||||
// VM.
|
||||
VM: vm => <VmItem vm={vm} />,
|
||||
'VM-snapshot': vm => <VmItem vm={vm} />,
|
||||
'VM-controller': vm => (
|
||||
<span>
|
||||
<Icon icon='host' />
|
||||
{' '}
|
||||
<VmItem vm={vm} />
|
||||
</span>
|
||||
),
|
||||
|
||||
// PIF.
|
||||
PIF: pif => (
|
||||
<span>
|
||||
<Icon icon='network' /> {pif.device}
|
||||
</span>
|
||||
),
|
||||
|
||||
// Tags.
|
||||
tag: tag => (
|
||||
<span>
|
||||
<Icon icon='tag' /> {tag.value}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const renderXoItem = (item, {
|
||||
className
|
||||
} = {}) => {
|
||||
const { id, type, label } = item
|
||||
|
||||
if (!type) {
|
||||
if (process.env.NODE_ENV !== 'production' && !label) {
|
||||
throw new Error(`an item must have at least either a type or a label`)
|
||||
}
|
||||
return (
|
||||
<span key={id} className={className}>
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const Component = xoItemToRender[type]
|
||||
|
||||
if (process.env.NODE_ENV !== 'production' && !Component) {
|
||||
throw new Error(`no available component for type ${type}`)
|
||||
}
|
||||
|
||||
if (Component) {
|
||||
return (
|
||||
<span key={id} className={className}>
|
||||
<Component {...item} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export { renderXoItem as default }
|
||||
|
||||
const GenericXoItem = connectStore(() => {
|
||||
const getObject = createGetObject()
|
||||
|
||||
return (state, props) => ({
|
||||
xoItem: getObject(state, props)
|
||||
})
|
||||
})(({ xoItem, ...props }) => xoItem
|
||||
? renderXoItem(xoItem, props)
|
||||
: <span className='text-muted'>no such item</span>
|
||||
)
|
||||
|
||||
export const renderXoItemFromId = (id, props) => <GenericXoItem {...props} id={id} />
|
||||
491
src/common/scheduling.js
Normal file
491
src/common/scheduling.js
Normal file
@@ -0,0 +1,491 @@
|
||||
import includes from 'lodash/includes'
|
||||
import join from 'lodash/join'
|
||||
import later from 'later'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import sortedIndex from 'lodash/sortedIndex'
|
||||
import { FormattedDate, FormattedTime } from 'react-intl'
|
||||
import {
|
||||
Tab,
|
||||
Tabs
|
||||
} from 'react-bootstrap-4/lib'
|
||||
|
||||
import _ from './intl'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import TimezonePicker from './timezone-picker'
|
||||
import { Card, CardHeader, CardBlock } from './card'
|
||||
import { Col, Row } from './grid'
|
||||
import { Range } from './form'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// By default later use UTC but we use this line for futures versions.
|
||||
later.date.UTC()
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const NAV_EACH_SELECTED = 1
|
||||
const NAV_EVERY_N = 2
|
||||
|
||||
const MIN_PREVIEWS = 5
|
||||
const MAX_PREVIEWS = 20
|
||||
|
||||
const MONTHS = [
|
||||
[ 0, 1, 2 ],
|
||||
[ 3, 4, 5 ],
|
||||
[ 6, 7, 8 ],
|
||||
[ 9, 10, 11 ]
|
||||
]
|
||||
|
||||
const DAYS = (() => {
|
||||
const days = []
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
days[i] = []
|
||||
|
||||
for (let j = 1; j < 8; j++) {
|
||||
days[i].push(7 * i + j)
|
||||
}
|
||||
}
|
||||
|
||||
days.push([29, 30, 31])
|
||||
|
||||
return days
|
||||
})()
|
||||
|
||||
const WEEK_DAYS = [
|
||||
[ 0, 1, 2 ],
|
||||
[ 3, 4, 5 ],
|
||||
[ 6 ]
|
||||
]
|
||||
|
||||
const HOURS = (() => {
|
||||
const hours = []
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
hours[i] = []
|
||||
|
||||
for (let j = 0; j < 8; j++) {
|
||||
hours[i].push(8 * i + j)
|
||||
}
|
||||
}
|
||||
|
||||
return hours
|
||||
})()
|
||||
|
||||
const MINS = (() => {
|
||||
const minutes = []
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
minutes[i] = []
|
||||
|
||||
for (let j = 0; j < 10; j++) {
|
||||
minutes[i].push(10 * i + j)
|
||||
}
|
||||
}
|
||||
|
||||
return minutes
|
||||
})()
|
||||
|
||||
const PICKTIME_TO_ID = {
|
||||
minute: 0,
|
||||
hour: 1,
|
||||
monthDay: 2,
|
||||
month: 3,
|
||||
weekDay: 4
|
||||
}
|
||||
|
||||
const TIME_FORMAT = {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
|
||||
// The timezone is not significant for displaying the date previews
|
||||
// as long as it is the same used to generate the next occurrences
|
||||
// from the cron patterns.
|
||||
|
||||
// Therefore we can use UTC everywhere and say to the user that the
|
||||
// previews are in the configured timezone.
|
||||
timeZone: 'UTC'
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// monthNum: [ 0 : 11 ]
|
||||
const getMonthName = (monthNum) =>
|
||||
<FormattedDate value={Date.UTC(1970, monthNum)} month='long' timeZone='UTC' />
|
||||
|
||||
// dayNum: [ 0 : 6 ]
|
||||
const getDayName = (dayNum) =>
|
||||
// January, 1970, 5th => Monday
|
||||
<FormattedDate value={Date.UTC(1970, 0, 4 + dayNum)} weekday='long' timeZone='UTC' />
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
cronPattern: propTypes.string.isRequired
|
||||
})
|
||||
export class SchedulePreview extends Component {
|
||||
_handleChange = value => {
|
||||
this.setState({
|
||||
value
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { cronPattern } = this.props
|
||||
const cronSched = later.parse.cron(cronPattern)
|
||||
const dates = later.schedule(cronSched).next(this.state.value || MIN_PREVIEWS)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='alert alert-info' role='alert'>
|
||||
{_('cronPattern')} <strong>{cronPattern}</strong>
|
||||
</div>
|
||||
<div className='form-inline p-b-1'>
|
||||
<Range min={MIN_PREVIEWS} max={MAX_PREVIEWS} onChange={this._handleChange} />
|
||||
</div>
|
||||
<ul className='list-group'>
|
||||
{map(dates, (date, id) => (
|
||||
<li className='list-group-item' key={id}>
|
||||
<FormattedTime value={date} {...TIME_FORMAT} />
|
||||
</li>
|
||||
))}
|
||||
<li className='list-group-item'>...</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
children: propTypes.any.isRequired,
|
||||
onChange: propTypes.func.isRequired,
|
||||
tdId: propTypes.number.isRequired,
|
||||
value: propTypes.bool.isRequired
|
||||
})
|
||||
class ToggleTd extends Component {
|
||||
_onClick = () => {
|
||||
const { props } = this
|
||||
props.onChange(props.tdId, !props.value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
return (
|
||||
<td style={{ cursor: 'pointer' }} className={props.value ? 'table-success' : ''} onClick={this._onClick}>
|
||||
{props.children}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
options: propTypes.array.isRequired,
|
||||
optionsRenderer: propTypes.func,
|
||||
onChange: propTypes.func.isRequired,
|
||||
value: propTypes.array.isRequired
|
||||
})
|
||||
class TableSelect extends Component {
|
||||
static defaultProps = {
|
||||
optionsRenderer: value => value
|
||||
}
|
||||
|
||||
_reset = () => {
|
||||
this.props.onChange([])
|
||||
}
|
||||
|
||||
_handleChange = (tdId, tdValue) => {
|
||||
const { props } = this
|
||||
|
||||
const newValue = props.value.slice()
|
||||
const index = sortedIndex(newValue, tdId)
|
||||
|
||||
if (tdValue) {
|
||||
// Add
|
||||
if (newValue[index] !== tdId) {
|
||||
newValue.splice(index, 0, tdId)
|
||||
}
|
||||
} else {
|
||||
// Remove
|
||||
if (newValue[index] === tdId) {
|
||||
newValue.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
props.onChange(newValue)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
options,
|
||||
optionsRenderer,
|
||||
value
|
||||
} = this.props
|
||||
const { length } = options[0]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className='table table-bordered table-sm'>
|
||||
<tbody>
|
||||
{map(options, (line, i) => (
|
||||
<tr key={i}>
|
||||
{map(line, (tdOption, j) => {
|
||||
const tdId = length * i + j
|
||||
return (
|
||||
<ToggleTd
|
||||
children={optionsRenderer(tdOption)}
|
||||
tdId={tdId}
|
||||
key={tdId}
|
||||
onChange={this._handleChange}
|
||||
value={includes(value, tdId)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className='btn btn-secondary pull-xs-right' onClick={this._reset}>
|
||||
{_('selectTableReset')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
optionsRenderer: propTypes.func,
|
||||
onChange: propTypes.func.isRequired,
|
||||
range: propTypes.array,
|
||||
labelId: propTypes.string.isRequired,
|
||||
value: propTypes.any.isRequired,
|
||||
valueRenderer: propTypes.func
|
||||
})
|
||||
class TimePicker extends Component {
|
||||
static defaultProps = {
|
||||
valueRenderer: e => +e
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {
|
||||
activeKey: NAV_EACH_SELECTED,
|
||||
tableValue: []
|
||||
}
|
||||
}
|
||||
|
||||
_update (props) {
|
||||
const { value, valueRenderer } = props
|
||||
|
||||
if (value.indexOf('/') === 1) {
|
||||
this.setState({
|
||||
activeKey: NAV_EVERY_N
|
||||
}, () => { this.refs.range.value = value.split('/')[1] })
|
||||
} else {
|
||||
this.setState({
|
||||
activeKey: NAV_EACH_SELECTED,
|
||||
tableValue: value === '*'
|
||||
? []
|
||||
: map(value.split(','), valueRenderer)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this._update(this.props)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
this._update(props)
|
||||
}
|
||||
|
||||
_selectTab = activeKey => {
|
||||
this.setState({
|
||||
activeKey
|
||||
}, () => {
|
||||
const { activeKey, tableValue } = this.state
|
||||
const { onChange } = this.props
|
||||
const { refs } = this
|
||||
|
||||
if (activeKey === NAV_EACH_SELECTED) {
|
||||
onChange(tableValue)
|
||||
} else {
|
||||
onChange(refs.range.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_handleTableValue = tableValue => {
|
||||
this.setState({
|
||||
tableValue
|
||||
}, () => this.props.onChange(tableValue))
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
onChange,
|
||||
options,
|
||||
optionsRenderer,
|
||||
range,
|
||||
labelId
|
||||
} = this.props
|
||||
const { tableValue } = this.state
|
||||
|
||||
const tableSelect = (
|
||||
<TableSelect
|
||||
onChange={this._handleTableValue}
|
||||
options={options}
|
||||
optionsRenderer={optionsRenderer}
|
||||
value={tableValue}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{_(`scheduling${labelId}`)}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{range
|
||||
? (
|
||||
<Tabs bsStyle='tabs' activeKey={this.state.activeKey} onSelect={this._selectTab}>
|
||||
<Tab tabClassName='nav-item' eventKey={NAV_EACH_SELECTED} title={_(`schedulingEachSelected${labelId}`)}>
|
||||
{tableSelect}
|
||||
</Tab>
|
||||
<Tab tabClassName='nav-item' eventKey={NAV_EVERY_N} title={_(`schedulingEveryN${labelId}`)}>
|
||||
<Range ref='range' min={range[0]} max={range[1]} onChange={onChange} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
) : tableSelect
|
||||
}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const HOURS_RANGE = [2, 12]
|
||||
const MINUTES_RANGE = [2, 30]
|
||||
|
||||
const decrement = e => e - 1
|
||||
|
||||
@propTypes({
|
||||
cronPattern: propTypes.string.isRequired,
|
||||
onChange: propTypes.func,
|
||||
timezone: propTypes.string
|
||||
})
|
||||
export default class Scheduler extends Component {
|
||||
_update (type, value) {
|
||||
if (Array.isArray(value)) {
|
||||
if (!value.length) {
|
||||
value = '*'
|
||||
} else {
|
||||
value = join(
|
||||
(type === 'monthDay' || type === 'month')
|
||||
? map(value, n => n + 1)
|
||||
: value,
|
||||
','
|
||||
)
|
||||
}
|
||||
} else {
|
||||
value = `*/${value}`
|
||||
}
|
||||
|
||||
const { props } = this
|
||||
const cronPattern = props.cronPattern.split(' ')
|
||||
cronPattern[PICKTIME_TO_ID[type]] = value
|
||||
|
||||
this.props.onChange({
|
||||
cronPattern: cronPattern.join(' '),
|
||||
timezone: props.timezone
|
||||
})
|
||||
}
|
||||
|
||||
_onHourChange = value => this._update('hour', value)
|
||||
_onMinuteChange = value => this._update('minute', value)
|
||||
_onMonthChange = value => this._update('month', value)
|
||||
_onMonthDayChange = value => this._update('monthDay', value)
|
||||
_onWeekDayChange = value => this._update('weekDay', value)
|
||||
|
||||
_onTimezoneChange = timezone => {
|
||||
const { props } = this
|
||||
props.onChange({
|
||||
cronPattern: props.cronPattern,
|
||||
timezone
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
cronPattern,
|
||||
timezone
|
||||
} = this.props
|
||||
const cronPatternArr = cronPattern.split(' ')
|
||||
|
||||
return (
|
||||
<div className='card-block'>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<TimePicker
|
||||
labelId='Month'
|
||||
optionsRenderer={getMonthName}
|
||||
options={MONTHS}
|
||||
onChange={this._onMonthChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['month']]}
|
||||
valueRenderer={decrement}
|
||||
/>
|
||||
<TimePicker
|
||||
labelId='MonthDay'
|
||||
options={DAYS}
|
||||
onChange={this._onMonthDayChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
|
||||
valueRenderer={decrement}
|
||||
/>
|
||||
<TimePicker
|
||||
labelId='WeekDay'
|
||||
optionsRenderer={getDayName}
|
||||
options={WEEK_DAYS}
|
||||
onChange={this._onWeekDayChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
|
||||
/>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<TimePicker
|
||||
labelId='Hour'
|
||||
options={HOURS}
|
||||
range={HOURS_RANGE}
|
||||
onChange={this._onHourChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['hour']]}
|
||||
/>
|
||||
<TimePicker
|
||||
labelId='Minute'
|
||||
options={MINS}
|
||||
range={MINUTES_RANGE}
|
||||
onChange={this._onMinuteChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['minute']]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<hr />
|
||||
<TimezonePicker value={timezone} onChange={this._onTimezoneChange} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
874
src/common/select-objects.js
Normal file
874
src/common/select-objects.js
Normal file
@@ -0,0 +1,874 @@
|
||||
import React from 'react'
|
||||
import assign from 'lodash/assign'
|
||||
import classNames from 'classnames'
|
||||
import filter from 'lodash/filter'
|
||||
import flatten from 'lodash/flatten'
|
||||
import forEach from 'lodash/forEach'
|
||||
import groupBy from 'lodash/groupBy'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import keyBy from 'lodash/keyBy'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import pick from 'lodash/pick'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
import store from 'store'
|
||||
import { parse as parseRemote } from 'xo-remote-parser'
|
||||
|
||||
import _ from './intl'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import renderXoItem from './render-xo-item'
|
||||
import { Select } from './form'
|
||||
import {
|
||||
createFilter,
|
||||
createGetObjectsOfType,
|
||||
createGetTags,
|
||||
createSelector,
|
||||
getObject
|
||||
} from './selectors'
|
||||
import {
|
||||
connectStore,
|
||||
mapPlus,
|
||||
resolveResourceSets
|
||||
} from './utils'
|
||||
import {
|
||||
isSrWritable,
|
||||
subscribeCurrentUser,
|
||||
subscribeGroups,
|
||||
subscribeIpPools,
|
||||
subscribeRemotes,
|
||||
subscribeResourceSets,
|
||||
subscribeRoles,
|
||||
subscribeUsers
|
||||
} from './xo'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const getLabel = object =>
|
||||
object.name_label ||
|
||||
object.name ||
|
||||
object.email ||
|
||||
(object.value && object.value.name) ||
|
||||
object.value ||
|
||||
object.label
|
||||
|
||||
// ===================================================================
|
||||
|
||||
/*
|
||||
* WITHOUT xoContainers :
|
||||
*
|
||||
* xoObjects: [
|
||||
* { type: 'myType', id: 'abc', label: 'First object' },
|
||||
* { type: 'myType', id: 'def', label: 'Second object' }
|
||||
* ]
|
||||
*
|
||||
*
|
||||
* WITH xoContainers :
|
||||
*
|
||||
* xoContainers: [
|
||||
* { type: 'containerType', id: 'ghi', label: 'First container' },
|
||||
* { type: 'containerType', id: 'jkl', label: 'Second container' }
|
||||
* ]
|
||||
*
|
||||
* xoObjects: {
|
||||
* ghi: [
|
||||
* { type: 'objectType', id: 'mno', label: 'First object' }
|
||||
* { type: 'objectType', id: 'pqr', label: 'Second object' }
|
||||
* ],
|
||||
* jkl: [
|
||||
* { type: 'objectType', id: 'stu', label: 'Third object' }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
@propTypes({
|
||||
autoFocus: propTypes.bool,
|
||||
clearable: propTypes.bool,
|
||||
defaultValue: propTypes.any,
|
||||
disabled: propTypes.bool,
|
||||
multi: propTypes.bool,
|
||||
onChange: propTypes.func,
|
||||
placeholder: propTypes.any.isRequired,
|
||||
predicate: propTypes.func,
|
||||
required: propTypes.bool,
|
||||
value: propTypes.any,
|
||||
xoContainers: propTypes.array,
|
||||
xoObjects: propTypes.oneOfType([
|
||||
propTypes.array,
|
||||
propTypes.objectOf(propTypes.array)
|
||||
]).isRequired
|
||||
})
|
||||
export class GenericSelect extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
value: this._setValue(props.value || props.defaultValue, props)
|
||||
}
|
||||
}
|
||||
|
||||
_getValue (xoObjectsById = this.state.xoObjectsById, props = this.props) {
|
||||
const { value } = this.state
|
||||
|
||||
if (props.multi) {
|
||||
// Returns the values of the selected objects
|
||||
// if they are contained in xoObjectsById.
|
||||
return mapPlus(value, (value, push) => {
|
||||
const o = xoObjectsById[value.value !== undefined ? value.value : value]
|
||||
|
||||
if (o) {
|
||||
push(o)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return xoObjectsById[value.value || value] || ''
|
||||
}
|
||||
|
||||
// Supports id strings and objects.
|
||||
_setValue (value, props = this.props) {
|
||||
if (props.multi) {
|
||||
return map(value, object => object.id !== undefined ? object.id : object)
|
||||
}
|
||||
|
||||
return (value != null)
|
||||
? value.id !== undefined ? value.id : value
|
||||
: ''
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { props } = this
|
||||
|
||||
this.setState({
|
||||
...this._computeOptions(props)
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
const { props } = this
|
||||
const { value, xoContainers, xoObjects } = newProps
|
||||
|
||||
if (
|
||||
xoContainers !== props.xoContainers ||
|
||||
xoObjects !== props.xoObjects
|
||||
) {
|
||||
const {
|
||||
options,
|
||||
xoObjectsById
|
||||
} = this._computeOptions(newProps)
|
||||
|
||||
const value = this._getValue(xoObjectsById, newProps)
|
||||
|
||||
this.setState({
|
||||
options,
|
||||
value: this._setValue(value, newProps),
|
||||
xoObjectsById
|
||||
})
|
||||
}
|
||||
|
||||
if (value !== props.value) {
|
||||
this.setState({
|
||||
value: this._setValue(value, newProps)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_computeOptions ({ xoContainers, xoObjects }) {
|
||||
if (!xoContainers) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!Array.isArray(xoObjects)) {
|
||||
throw new Error('without xoContainers, xoObjects must be an array')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
xoObjectsById: keyBy(xoObjects, 'id'),
|
||||
options: map(xoObjects, object => ({
|
||||
label: getLabel(object),
|
||||
value: object.id,
|
||||
xoItem: object
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (Array.isArray(xoObjects)) {
|
||||
throw new Error('with xoContainers, xoObjects must be an object')
|
||||
}
|
||||
}
|
||||
|
||||
const options = []
|
||||
const xoObjectsById = {}
|
||||
|
||||
forEach(xoContainers, container => {
|
||||
const containerObjects = keyBy(xoObjects[container.id], 'id')
|
||||
assign(xoObjectsById, containerObjects)
|
||||
|
||||
options.push({
|
||||
disabled: true,
|
||||
xoItem: container
|
||||
})
|
||||
|
||||
options.push.apply(options, map(containerObjects, object => ({
|
||||
label: `${getLabel(object)} ${getLabel(container)}`,
|
||||
value: object.id,
|
||||
xoItem: object
|
||||
})))
|
||||
})
|
||||
|
||||
return { xoObjectsById, options }
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this._getValue()
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.setState({
|
||||
value: this._setValue(value)
|
||||
})
|
||||
}
|
||||
|
||||
_handleChange = value => {
|
||||
const { onChange } = this.props
|
||||
|
||||
this.setState({
|
||||
value: this._setValue(value)
|
||||
}, onChange && (() => onChange(this.value)))
|
||||
}
|
||||
|
||||
// GroupBy: Display option with margin if not disabled and containers exists.
|
||||
_renderOption = option => (
|
||||
<span
|
||||
className={classNames(
|
||||
!option.disabled && this.props.xoContainers && 'm-l-1'
|
||||
)}
|
||||
>
|
||||
{renderXoItem(option.xoItem)}
|
||||
</span>
|
||||
)
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
|
||||
return (
|
||||
<Select
|
||||
autofocus={props.autoFocus}
|
||||
clearable={props.clearable}
|
||||
disabled={props.disabled}
|
||||
multi={props.multi}
|
||||
onChange={this._handleChange}
|
||||
openOnFocus
|
||||
optionRenderer={this._renderOption}
|
||||
options={state.options}
|
||||
placeholder={props.placeholder}
|
||||
required={props.required}
|
||||
value={state.value}
|
||||
valueRenderer={this._renderOption}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const makeStoreSelect = (createSelectors, props) => connectStore(
|
||||
createSelectors,
|
||||
{ withRef: true }
|
||||
)(
|
||||
class extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
{...props}
|
||||
{...this.props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const makeSubscriptionSelect = (subscribe, props) => (
|
||||
class extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this._getFilteredXoContainers = createFilter(
|
||||
() => this.state.xoContainers,
|
||||
() => this.props.containerPredicate
|
||||
)
|
||||
|
||||
this._getFilteredXoObjects = createSelector(
|
||||
() => this.state.xoObjects,
|
||||
() => this.state.xoContainers && this._getFilteredXoContainers(),
|
||||
() => this.props.predicate,
|
||||
(xoObjects, xoContainers, predicate) => {
|
||||
if (xoContainers == null) {
|
||||
return filter(xoObjects, predicate)
|
||||
} else {
|
||||
// Filter xoObjects with `predicate`...
|
||||
const filteredObjects = mapValues(xoObjects, xoObjectsGroup =>
|
||||
filter(xoObjectsGroup, predicate)
|
||||
)
|
||||
// ...and keep only those whose xoContainer hasn't been filtered out
|
||||
return pick(filteredObjects, map(xoContainers, container => container.id))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribe(::this.setState)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
{...props}
|
||||
{...this.props}
|
||||
xoObjects={this._getFilteredXoObjects()}
|
||||
xoContainers={this.state.xoContainers && this._getFilteredXoContainers()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
// XO objects.
|
||||
// ===================================================================
|
||||
|
||||
const getPredicate = (state, props) => props.predicate
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectHost = makeStoreSelect(() => {
|
||||
const getHostsByPool = createGetObjectsOfType('host').filter(
|
||||
getPredicate
|
||||
).sort()
|
||||
|
||||
return {
|
||||
xoObjects: getHostsByPool
|
||||
}
|
||||
}, { placeholder: _('selectHosts') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectPool = makeStoreSelect(() => ({
|
||||
xoObjects: createGetObjectsOfType('pool').filter(getPredicate).sort()
|
||||
}), { placeholder: _('selectPools') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectSr = makeStoreSelect(() => {
|
||||
const getSrsByContainer = createGetObjectsOfType('SR').filter(
|
||||
(_, { predicate }) => predicate || isSrWritable
|
||||
).sort().groupBy('$container')
|
||||
|
||||
const getContainerIds = createSelector(
|
||||
getSrsByContainer,
|
||||
srsByContainer => keys(srsByContainer)
|
||||
)
|
||||
|
||||
const getPools = createGetObjectsOfType('pool').pick(getContainerIds).sort()
|
||||
const getHosts = createGetObjectsOfType('host').pick(getContainerIds).sort()
|
||||
|
||||
const getContainers = createSelector(
|
||||
getPools,
|
||||
getHosts,
|
||||
(pools, hosts) => pools.concat(hosts)
|
||||
)
|
||||
|
||||
return {
|
||||
xoObjects: getSrsByContainer,
|
||||
xoContainers: getContainers
|
||||
}
|
||||
}, { placeholder: _('selectSrs') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectVm = makeStoreSelect(() => {
|
||||
const getVmsByContainer = createGetObjectsOfType('VM').filter(
|
||||
getPredicate
|
||||
).sort().groupBy('$container')
|
||||
|
||||
const getContainerIds = createSelector(
|
||||
getVmsByContainer,
|
||||
vmsByContainer => keys(vmsByContainer)
|
||||
)
|
||||
|
||||
const getPools = createGetObjectsOfType('pool').pick(getContainerIds).sort()
|
||||
const getHosts = createGetObjectsOfType('host').pick(getContainerIds).sort()
|
||||
|
||||
const getContainers = createSelector(
|
||||
getPools,
|
||||
getHosts,
|
||||
(pools, hosts) => pools.concat(hosts)
|
||||
)
|
||||
|
||||
return {
|
||||
xoObjects: getVmsByContainer,
|
||||
xoContainers: getContainers
|
||||
}
|
||||
}, { placeholder: _('selectVms') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectHostVm = makeStoreSelect(() => {
|
||||
const getHosts = createGetObjectsOfType('host').filter(
|
||||
getPredicate
|
||||
).sort()
|
||||
const getVms = createGetObjectsOfType('VM').filter(
|
||||
getPredicate
|
||||
).sort()
|
||||
|
||||
const getObjects = createSelector(
|
||||
getHosts,
|
||||
getVms,
|
||||
(hosts, vms) => hosts.concat(vms)
|
||||
)
|
||||
|
||||
return {
|
||||
xoObjects: getObjects
|
||||
}
|
||||
}, { placeholder: _('selectHostsVms') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectVmTemplate = makeStoreSelect(() => {
|
||||
const getVmTemplatesByPool = createGetObjectsOfType('VM-template').filter(
|
||||
getPredicate
|
||||
).sort().groupBy('$container')
|
||||
const getPools = createGetObjectsOfType('pool').pick(
|
||||
createSelector(
|
||||
getVmTemplatesByPool,
|
||||
vmTemplatesByPool => keys(vmTemplatesByPool)
|
||||
)
|
||||
).sort()
|
||||
|
||||
return {
|
||||
xoObjects: getVmTemplatesByPool,
|
||||
xoContainers: getPools
|
||||
}
|
||||
}, { placeholder: _('selectVmTemplates') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectNetwork = makeStoreSelect(() => {
|
||||
const getNetworksByPool = createGetObjectsOfType('network').filter(
|
||||
getPredicate
|
||||
).sort().groupBy('$pool')
|
||||
const getPools = createGetObjectsOfType('pool').pick(
|
||||
createSelector(
|
||||
getNetworksByPool,
|
||||
networksByPool => keys(networksByPool)
|
||||
)
|
||||
).sort()
|
||||
|
||||
return {
|
||||
xoObjects: getNetworksByPool,
|
||||
xoContainers: getPools
|
||||
}
|
||||
}, { placeholder: _('selectNetworks') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectPif = makeStoreSelect(() => {
|
||||
const getPifsByHost = createGetObjectsOfType('PIF').filter(
|
||||
getPredicate
|
||||
).sort().groupBy('$host')
|
||||
const getHosts = createGetObjectsOfType('host').pick(
|
||||
createSelector(
|
||||
getPifsByHost,
|
||||
networksByPool => keys(networksByPool)
|
||||
)
|
||||
).sort()
|
||||
|
||||
return {
|
||||
xoObjects: getPifsByHost,
|
||||
xoContainers: getHosts
|
||||
}
|
||||
}, { placeholder: _('selectPifs') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectTag = makeStoreSelect((_, props) => ({
|
||||
xoObjects: createSelector(
|
||||
createGetTags(
|
||||
'objects' in props
|
||||
? (_, props) => props.objects
|
||||
: undefined
|
||||
).filter(getPredicate).sort(),
|
||||
tags => map(tags, tag => ({ id: tag, type: 'tag', value: tag }))
|
||||
)
|
||||
}), { placeholder: _('selectTags') })
|
||||
|
||||
export const SelectHighLevelObject = makeStoreSelect(() => {
|
||||
const getHosts = createGetObjectsOfType('host')
|
||||
const getNetworks = createGetObjectsOfType('network')
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
const getSrs = createGetObjectsOfType('SR')
|
||||
const getVms = createGetObjectsOfType('VM')
|
||||
|
||||
const getHighLevelObjects = createSelector(
|
||||
getHosts,
|
||||
getNetworks,
|
||||
getPools,
|
||||
getSrs,
|
||||
getVms,
|
||||
(hosts, networks, pools, srs, vms) => sortBy(assign({}, hosts, networks, pools, srs, vms), ['type', 'name_label'])
|
||||
)
|
||||
|
||||
return {xoObjects: getHighLevelObjects}
|
||||
}, { placeholder: _('selectObjects') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectVdi = propTypes({
|
||||
srPredicate: propTypes.func
|
||||
})(makeStoreSelect(() => {
|
||||
const getSrs = createGetObjectsOfType('SR').filter((_, props) => props.srPredicate)
|
||||
const getVdis = createGetObjectsOfType('VDI').filter(createSelector(
|
||||
getSrs,
|
||||
getPredicate,
|
||||
(srs, predicate) => predicate ? vdi => srs[vdi.$SR] && predicate(vdi) : vdi => srs[vdi.$SR]
|
||||
)).sort().groupBy('$SR')
|
||||
|
||||
return {
|
||||
xoObjects: getVdis,
|
||||
xoContainers: getSrs.sort()
|
||||
}
|
||||
}, { placeholder: _('selectVdis') }))
|
||||
|
||||
// ===================================================================
|
||||
// Objects from subscriptions.
|
||||
// ===================================================================
|
||||
|
||||
export const SelectSubject = makeSubscriptionSelect(subscriber => {
|
||||
let subjects = {}
|
||||
|
||||
let usersLoaded, groupsLoaded
|
||||
const set = newSubjects => {
|
||||
subjects = newSubjects
|
||||
/* We must wait for groups AND users options to be loaded,
|
||||
* or a previously setted value belonging to one type or another might be discarded
|
||||
* by the internal <GenericSelect>
|
||||
*/
|
||||
if (usersLoaded && groupsLoaded) {
|
||||
subscriber({
|
||||
xoObjects: subjects
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribeGroups = subscribeGroups(groups => {
|
||||
groupsLoaded = true
|
||||
set([
|
||||
...filter(subjects, subject => subject.type === 'user'),
|
||||
...groups
|
||||
])
|
||||
})
|
||||
|
||||
const unsubscribeUsers = subscribeUsers(users => {
|
||||
usersLoaded = true
|
||||
set([
|
||||
...filter(subjects, subject => subject.type === 'group'),
|
||||
...users
|
||||
])
|
||||
})
|
||||
|
||||
return () => {
|
||||
unsubscribeGroups()
|
||||
unsubscribeUsers()
|
||||
}
|
||||
}, { placeholder: _('selectSubjects') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectRole = makeSubscriptionSelect(subscriber => {
|
||||
const unsubscribeRoles = subscribeRoles(roles => {
|
||||
const xoObjects = map(sortBy(roles, 'name'), role => ({...role, type: 'role'}))
|
||||
subscriber({xoObjects})
|
||||
})
|
||||
return unsubscribeRoles
|
||||
}, { placeholder: _('selectRole') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectRemote = makeSubscriptionSelect(subscriber => {
|
||||
const unsubscribeRemotes = subscribeRemotes(remotes => {
|
||||
const xoObjects = groupBy(
|
||||
map(sortBy(remotes, 'name'), remote => {
|
||||
remote = {...remote, ...parseRemote(remote.url)}
|
||||
return { id: remote.id, type: 'remote', value: remote }
|
||||
}),
|
||||
remote => remote.value.type
|
||||
)
|
||||
|
||||
subscriber({
|
||||
xoObjects,
|
||||
xoContainers: map(xoObjects, (remote, type) => ({
|
||||
id: type,
|
||||
label: type
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
return unsubscribeRemotes
|
||||
}, { placeholder: _('selectRemotes') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectResourceSet = makeSubscriptionSelect(subscriber => {
|
||||
const unsubscribeResourceSets = subscribeResourceSets(resourceSets => {
|
||||
const xoObjects = map(sortBy(resolveResourceSets(resourceSets), 'name'), resourceSet => ({...resourceSet, type: 'resourceSet'}))
|
||||
|
||||
subscriber({xoObjects})
|
||||
})
|
||||
|
||||
return unsubscribeResourceSets
|
||||
}, { placeholder: _('selectResourceSets') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectResourceSetsVmTemplate extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getTemplates = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
const { predicate } = this.props
|
||||
const templates = objectsByType['VM-template']
|
||||
return sortBy(predicate ? filter(templates, predicate) : templates, 'name_label')
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectResourceSetsVmTemplate')}
|
||||
{...this.props}
|
||||
xoObjects={this._getTemplates()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectResourceSetsSr extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getSrs = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
const { predicate } = this.props
|
||||
const srs = objectsByType['SR']
|
||||
return sortBy(predicate ? filter(srs, predicate) : srs, 'name_label')
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectResourceSetsSr')}
|
||||
{...this.props}
|
||||
xoObjects={this._getSrs()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectResourceSetsVdi extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getObject (id) {
|
||||
return getObject(store.getState(), id, true)
|
||||
}
|
||||
|
||||
_getSrs = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
const { srPredicate } = this.props
|
||||
const srs = objectsByType['SR']
|
||||
return srPredicate ? filter(srs, srPredicate) : srs
|
||||
}
|
||||
)
|
||||
|
||||
_getVdis = createSelector(
|
||||
this._getSrs,
|
||||
srs => sortBy(map(flatten(map(srs, sr => sr.VDIs)), this._getObject), 'name_label')
|
||||
)
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectResourceSetsVdi')}
|
||||
{...this.props}
|
||||
xoObjects={this._getVdis()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectResourceSetsNetwork extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getNetworks = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
const { predicate } = this.props
|
||||
const networks = objectsByType['network']
|
||||
return sortBy(predicate ? filter(networks, predicate) : networks, 'name_label')
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectResourceSetsNetwork')}
|
||||
{...this.props}
|
||||
xoObjects={this._getNetworks()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectSshKey extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeCurrentUser(user => {
|
||||
this.setState({
|
||||
sshKeys: user && user.preferences && map(user.preferences.sshKeys, (key, id) => ({
|
||||
id,
|
||||
label: key.title,
|
||||
type: 'sshKey'
|
||||
}))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectSshKey')}
|
||||
{...this.props}
|
||||
xoObjects={this.state.sshKeys || []}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectIp = makeSubscriptionSelect(subscriber => {
|
||||
const unsubscribeIpPools = subscribeIpPools(ipPools => {
|
||||
const sortedIpPools = sortBy(ipPools, 'name')
|
||||
const xoObjects = mapValues(
|
||||
groupBy(sortedIpPools, 'id'),
|
||||
ipPools => map(ipPools[0].addresses, (address, ip) => ({
|
||||
...address,
|
||||
id: ip,
|
||||
label: ip,
|
||||
type: 'ipAddress',
|
||||
used: !isEmpty(address.vifs)
|
||||
}))
|
||||
)
|
||||
const xoContainers = map(sortedIpPools, ipPool => ({
|
||||
...ipPool,
|
||||
type: 'ipPool'
|
||||
}))
|
||||
subscriber({ xoObjects, xoContainers })
|
||||
})
|
||||
|
||||
return unsubscribeIpPools
|
||||
}, { placeholder: _('selectIp') })
|
||||
475
src/common/selectors.js
Normal file
475
src/common/selectors.js
Normal file
@@ -0,0 +1,475 @@
|
||||
import checkPermissions from 'xo-acl-resolver'
|
||||
import filter from 'lodash/filter'
|
||||
import find from 'lodash/find'
|
||||
import forEach from 'lodash/forEach'
|
||||
import groupBy from 'lodash/groupBy'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isArrayLike from 'lodash/isArrayLike'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import pickBy from 'lodash/pickBy'
|
||||
import size from 'lodash/size'
|
||||
import slice from 'lodash/slice'
|
||||
import { createSelector as create } from 'reselect'
|
||||
|
||||
import invoke from './invoke'
|
||||
import shallowEqual from './shallow-equal'
|
||||
import { EMPTY_ARRAY, EMPTY_OBJECT } from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export {
|
||||
// That's usually the name we want to import.
|
||||
createSelector,
|
||||
|
||||
// But selectors.create is nice too :)
|
||||
createSelector as create
|
||||
} from 'reselect'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Wraps a function which returns a collection to returns the previous
|
||||
// result if the collection has not really changed (ie still has the
|
||||
// same items).
|
||||
//
|
||||
// Use case: in connect, to avoid rerendering a component where the
|
||||
// objects are still the same.
|
||||
const _createCollectionWrapper = selector => {
|
||||
let cache
|
||||
|
||||
return (...args) => {
|
||||
const value = selector(...args)
|
||||
if (!shallowEqual(value, cache)) {
|
||||
cache = value
|
||||
}
|
||||
return cache
|
||||
}
|
||||
}
|
||||
export { _createCollectionWrapper as createCollectionWrapper }
|
||||
|
||||
const _SELECTOR_PLACEHOLDER = Symbol('selector placeholder')
|
||||
|
||||
// Experimental!
|
||||
//
|
||||
// Similar to reselect's createSelector() but inputs can be either
|
||||
// selectors or plain values.
|
||||
//
|
||||
// To pass a function as a plain value, simply wrap it with an array.
|
||||
const _create2 = (...inputs) => {
|
||||
const resultFn = inputs.pop()
|
||||
|
||||
if (inputs.length === 1 && isArray(inputs[0])) {
|
||||
inputs = inputs[0]
|
||||
}
|
||||
|
||||
const n = inputs.length
|
||||
|
||||
const inputSelectors = []
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const input = inputs[i]
|
||||
|
||||
if (isFunction(input)) {
|
||||
inputSelectors.push(input)
|
||||
inputs[i] = _SELECTOR_PLACEHOLDER
|
||||
} else if (isArray(input) && input.length === 1) {
|
||||
inputs[i] = input[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (!inputSelectors.length) {
|
||||
throw new Error('no input selectors')
|
||||
}
|
||||
|
||||
return create(inputSelectors, function () {
|
||||
const args = new Array(n)
|
||||
for (let i = 0, j = 0; i < n; ++i) {
|
||||
const input = inputs[i]
|
||||
args[i] = input === _SELECTOR_PLACEHOLDER
|
||||
? arguments[j++]
|
||||
: input
|
||||
}
|
||||
|
||||
return resultFn.apply(this, args)
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Generic selector creators.
|
||||
|
||||
export const createCounter = (collection, predicate) =>
|
||||
_create2(
|
||||
collection,
|
||||
predicate,
|
||||
(collection, predicate) => {
|
||||
if (!predicate) {
|
||||
return size(collection)
|
||||
}
|
||||
|
||||
let count = 0
|
||||
forEach(collection, item => {
|
||||
if (predicate(item)) {
|
||||
++count
|
||||
}
|
||||
})
|
||||
return count
|
||||
}
|
||||
)
|
||||
|
||||
// Creates an object selector from an object selector and a properties
|
||||
// selector.
|
||||
//
|
||||
// Should only be used with a reasonable number of properties.
|
||||
export const createPicker = (object, props) =>
|
||||
_createCollectionWrapper(
|
||||
_create2(
|
||||
object, props,
|
||||
(object, props) => {
|
||||
const values = {}
|
||||
forEach(props, prop => {
|
||||
const value = object[prop]
|
||||
if (value) {
|
||||
values[prop] = value
|
||||
}
|
||||
})
|
||||
return values
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// Special cases:
|
||||
// - predicate == null → no filtering
|
||||
// - predicate === false → everything is filtered out
|
||||
export const createFilter = (collection, predicate) =>
|
||||
_createCollectionWrapper(
|
||||
_create2(
|
||||
collection,
|
||||
predicate,
|
||||
(collection, predicate) => predicate === false
|
||||
? (isArrayLike(collection) ? EMPTY_ARRAY : EMPTY_OBJECT)
|
||||
: predicate
|
||||
? (isArrayLike(collection) ? filter : pickBy)(collection, predicate)
|
||||
: collection
|
||||
)
|
||||
)
|
||||
|
||||
export const createFinder = (collection, predicate) =>
|
||||
_create2(
|
||||
collection,
|
||||
predicate,
|
||||
find
|
||||
)
|
||||
|
||||
export const createGroupBy = (collection, getter) =>
|
||||
_create2(
|
||||
collection,
|
||||
getter,
|
||||
groupBy
|
||||
)
|
||||
|
||||
export const createPager = (array, page, n = 25) => _createCollectionWrapper(
|
||||
_create2(
|
||||
array,
|
||||
page,
|
||||
n,
|
||||
(array, page, n) => {
|
||||
const start = (page - 1) * n
|
||||
return slice(array, start, start + n)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
export const createSort = (
|
||||
collection,
|
||||
getter = 'name_label',
|
||||
order = 'asc'
|
||||
) => _create2(collection, getter, order, orderBy)
|
||||
|
||||
export const createTop = (collection, iteratee, n) =>
|
||||
_createCollectionWrapper(
|
||||
_create2(
|
||||
collection,
|
||||
iteratee,
|
||||
n,
|
||||
(objects, iteratee, n) => {
|
||||
let results = orderBy(objects, iteratee, 'desc')
|
||||
if (n < results.length) {
|
||||
results.length = n
|
||||
}
|
||||
return results
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
// Root-ish selectors (no dependencies).
|
||||
|
||||
export const areObjectsFetched = state => state.objects.fetched
|
||||
|
||||
const _getId = (state, { routeParams, id }) => routeParams
|
||||
? routeParams.id
|
||||
: id
|
||||
|
||||
export const getLang = state => state.lang
|
||||
|
||||
export const getStatus = state => state.status
|
||||
|
||||
export const getUser = state => state.user
|
||||
|
||||
const _getPermissionsPredicate = invoke(() => {
|
||||
const getPredicate = create(
|
||||
state => state.permissions,
|
||||
state => state.objects,
|
||||
(permissions, objects) => {
|
||||
objects = objects.all
|
||||
const getObject = id => (objects[id] || EMPTY_OBJECT)
|
||||
|
||||
return id => checkPermissions(permissions, getObject, id.id || id, 'view')
|
||||
}
|
||||
)
|
||||
|
||||
return state => {
|
||||
const user = getUser(state)
|
||||
if (!user) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (user.permission === 'admin') {
|
||||
return // No predicate means no filtering.
|
||||
}
|
||||
|
||||
return getPredicate(state)
|
||||
}
|
||||
})
|
||||
|
||||
export const isAdmin = (...args) => {
|
||||
const user = getUser(...args)
|
||||
|
||||
return user && user.permission === 'admin'
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Common selector creators.
|
||||
|
||||
// Creates an object selector from an id selector.
|
||||
export const createGetObject = (idSelector = _getId) =>
|
||||
(state, props, useResourceSet) => {
|
||||
const object = state.objects.all[idSelector(state, props)]
|
||||
if (!object) {
|
||||
return
|
||||
}
|
||||
|
||||
if (useResourceSet) {
|
||||
return object
|
||||
}
|
||||
|
||||
const predicate = _getPermissionsPredicate(state)
|
||||
|
||||
if (!predicate) {
|
||||
if (predicate == null) {
|
||||
return object // no filtering
|
||||
}
|
||||
|
||||
// predicate is false.
|
||||
return
|
||||
}
|
||||
|
||||
if (predicate(object)) {
|
||||
return object
|
||||
}
|
||||
}
|
||||
|
||||
// Specialized createSort() configured for a given type.
|
||||
export const createSortForType = invoke(() => {
|
||||
const iterateesByType = {
|
||||
message: message => message.time,
|
||||
PIF: pif => pif.device,
|
||||
pool: pool => pool.name_label,
|
||||
pool_patch: patch => patch.name,
|
||||
tag: tag => tag,
|
||||
VBD: vbd => vbd.position,
|
||||
'VDI-snapshot': snapshot => snapshot.snapshot_time,
|
||||
'VM-snapshot': snapshot => snapshot.snapshot_time
|
||||
}
|
||||
const defaultIteratees = [
|
||||
object => object.$pool,
|
||||
object => object.name_label
|
||||
]
|
||||
const getIteratees = type => iterateesByType[type] || defaultIteratees
|
||||
|
||||
const ordersByType = {
|
||||
message: 'desc',
|
||||
'VDI-snapshot': 'desc',
|
||||
'VM-snapshot': 'desc'
|
||||
}
|
||||
const getOrders = type => ordersByType[type]
|
||||
|
||||
const autoSelector = (type, fn) => isFunction(type)
|
||||
? (state, props) => fn(type(state, props))
|
||||
: [ fn(type) ]
|
||||
|
||||
return (type, collection) => createSort(
|
||||
collection,
|
||||
autoSelector(type, getIteratees),
|
||||
autoSelector(type, getOrders),
|
||||
)
|
||||
})
|
||||
|
||||
// Add utility methods to a collection selector.
|
||||
const _extendCollectionSelector = (selector, objectsType) => {
|
||||
// Terminal methods.
|
||||
const _addCount = selector => {
|
||||
selector.count = predicate => createCounter(selector, predicate)
|
||||
return selector
|
||||
}
|
||||
_addCount(selector)
|
||||
const _addGroupBy = selector => {
|
||||
selector.groupBy = getter => createGroupBy(selector, getter)
|
||||
return selector
|
||||
}
|
||||
_addGroupBy(selector)
|
||||
const _addFind = selector => {
|
||||
selector.find = predicate => createFinder(selector, predicate)
|
||||
return selector
|
||||
}
|
||||
_addFind(selector)
|
||||
|
||||
// groupBy can be chained.
|
||||
const _addSort = selector => {
|
||||
// TODO: maybe memoize when no idsSelector.
|
||||
selector.sort = () => _addGroupBy(createSortForType(objectsType, selector))
|
||||
return selector
|
||||
}
|
||||
_addSort(selector)
|
||||
|
||||
// count, groupBy and sort can be chained.
|
||||
const _addFilter = selector => {
|
||||
selector.filter = predicate => _addCount(_addGroupBy(_addSort(
|
||||
createFilter(selector, predicate)
|
||||
)))
|
||||
return selector
|
||||
}
|
||||
_addFilter(selector)
|
||||
|
||||
// filter, groupBy and sort can be chained.
|
||||
selector.pick = idsSelector => _addFind(_addFilter(_addGroupBy(_addSort(
|
||||
createPicker(selector, idsSelector)
|
||||
))))
|
||||
|
||||
return selector
|
||||
}
|
||||
|
||||
// Creates a collection selector which returns all objects of a given
|
||||
// type.
|
||||
//
|
||||
// The selector as the following methods:
|
||||
//
|
||||
// - count: returns a selector which returns the number of objects
|
||||
// - filter: returns a selector which returns the objects filtered by
|
||||
// a predicate (count, groupBy and sort can be chained)
|
||||
// - find: returns a selector which returns the first object matching
|
||||
// a predicate
|
||||
// - groupBy: returns a selector which returns the objects grouped by
|
||||
// a value determined by a getter selector
|
||||
// - pick: returns a selector which returns only the objects with given
|
||||
// ids (filter, find, groupBy and sort can be chained)
|
||||
// - sort: returns a selector which returns the objects appropriately
|
||||
// sorted (groupBy can be chained)
|
||||
export const createGetObjectsOfType = type => {
|
||||
const getObjects = isFunction(type)
|
||||
? (state, props) => state.objects.byType[type(state, props)] || EMPTY_OBJECT
|
||||
: state => state.objects.byType[type] || EMPTY_OBJECT
|
||||
|
||||
return _extendCollectionSelector(createFilter(
|
||||
getObjects,
|
||||
_getPermissionsPredicate
|
||||
), type)
|
||||
}
|
||||
|
||||
export const createGetTags = collectionSelectors => {
|
||||
if (!collectionSelectors) {
|
||||
collectionSelectors = [
|
||||
createGetObjectsOfType('host'),
|
||||
createGetObjectsOfType('pool'),
|
||||
createGetObjectsOfType('VM')
|
||||
]
|
||||
}
|
||||
|
||||
const getTags = create(
|
||||
collectionSelectors,
|
||||
(...collections) => {
|
||||
const tags = {}
|
||||
|
||||
const addTag = tag => { tags[tag] = null }
|
||||
const addItemTags = item => { forEach(item.tags, addTag) }
|
||||
const addCollectionTags = collection => { forEach(collection, addItemTags) }
|
||||
forEach(collections, addCollectionTags)
|
||||
|
||||
return keys(tags)
|
||||
}
|
||||
)
|
||||
|
||||
return _extendCollectionSelector(getTags, 'tag')
|
||||
}
|
||||
|
||||
export const createGetObjectMessages = objectSelector =>
|
||||
createGetObjectsOfType('message').filter(
|
||||
create(
|
||||
(...args) => objectSelector(...args).id,
|
||||
id => message => message.$object === id
|
||||
)
|
||||
).sort()
|
||||
|
||||
// Example of use:
|
||||
// import store from 'store'
|
||||
// const object = getObject(store.getState(), objectId)
|
||||
// ...
|
||||
export const getObject = createGetObject((_, id) => id)
|
||||
|
||||
export const createDoesHostNeedRestart = hostSelector => {
|
||||
// Returns the first patch of the host which requires it to be
|
||||
// restarted.
|
||||
const restartPoolPatch = createGetObjectsOfType('pool_patch').pick(
|
||||
create(
|
||||
createGetObjectsOfType('host_patch').pick(
|
||||
(state, props) => {
|
||||
const host = hostSelector(state, props)
|
||||
return host && host.patches
|
||||
}
|
||||
).filter(create(
|
||||
(state, props) => {
|
||||
const host = hostSelector(state, props)
|
||||
return host && host.startTime
|
||||
},
|
||||
startTime => patch => patch.time > startTime
|
||||
)),
|
||||
hostPatches => map(hostPatches, hostPatch => hostPatch.pool_patch)
|
||||
)
|
||||
).find([ ({ guidance }) => find(guidance, action =>
|
||||
action === 'restartHost' || action === 'restartXapi'
|
||||
) ])
|
||||
|
||||
return (state, props) => restartPoolPatch(state, props) !== undefined
|
||||
}
|
||||
|
||||
export const createGetHostMetrics = hostSelector => _createCollectionWrapper(
|
||||
create(
|
||||
hostSelector,
|
||||
hosts => {
|
||||
const metrics = {
|
||||
count: 0,
|
||||
cpus: 0,
|
||||
memoryTotal: 0,
|
||||
memoryUsage: 0
|
||||
}
|
||||
forEach(hosts, host => {
|
||||
metrics.count++
|
||||
metrics.cpus += host.cpus.cores
|
||||
metrics.memoryTotal += host.memory.size
|
||||
metrics.memoryUsage += host.memory.usage
|
||||
})
|
||||
return metrics
|
||||
}
|
||||
)
|
||||
)
|
||||
42
src/common/shallow-equal.js
Normal file
42
src/common/shallow-equal.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// Tests that two collections (arrays or objects) have strictly equals
|
||||
// values (items or properties)
|
||||
const shallowEqual = (c1, c2) => {
|
||||
if (c1 === c2) {
|
||||
return true
|
||||
}
|
||||
|
||||
const type = typeof c1
|
||||
if (type !== typeof c2) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (type === 'array') {
|
||||
const { length } = c1
|
||||
if (length !== c2.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (let i = 0; i < length; ++i) {
|
||||
if (c1[i] !== c2[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
let n = 0
|
||||
for (const _ in c2) { // eslint-disable-line no-unused-vars
|
||||
++n
|
||||
}
|
||||
|
||||
for (const key in c1) {
|
||||
if (c1[key] !== c2[key]) {
|
||||
return false
|
||||
}
|
||||
--n
|
||||
}
|
||||
|
||||
return !n
|
||||
}
|
||||
export { shallowEqual as default }
|
||||
35
src/common/shortcuts.js
Normal file
35
src/common/shortcuts.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import Component from 'base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
import React from 'react'
|
||||
import remove from 'lodash/remove'
|
||||
import { Shortcuts as ReactShortcuts } from 'react-shortcuts'
|
||||
|
||||
let enabled = true
|
||||
const instances = []
|
||||
|
||||
const updateInstances = () => {
|
||||
forEach(instances, instance => instance.forceUpdate())
|
||||
}
|
||||
|
||||
export const enable = () => {
|
||||
enabled = true
|
||||
updateInstances()
|
||||
}
|
||||
|
||||
export const disable = () => {
|
||||
enabled = false
|
||||
updateInstances()
|
||||
}
|
||||
|
||||
export default class Shortcuts extends Component {
|
||||
componentDidMount () {
|
||||
instances.push(this)
|
||||
}
|
||||
componentWillUnmount () {
|
||||
remove(instances, this)
|
||||
}
|
||||
|
||||
render () {
|
||||
return enabled ? <ReactShortcuts {...this.props} /> : null
|
||||
}
|
||||
}
|
||||
19
src/common/single-line-row.js
Normal file
19
src/common/single-line-row.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { cloneElement } from 'react'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
|
||||
const SINGLE_LINE_STYLE = { display: 'flex' }
|
||||
const COL_STYLE = { marginTop: 'auto', marginBottom: 'auto' }
|
||||
|
||||
const SingleLineRow = propTypes({
|
||||
className: propTypes.string
|
||||
})(({
|
||||
children,
|
||||
className
|
||||
}) => <div
|
||||
className={`${className || ''} row`}
|
||||
style={SINGLE_LINE_STYLE}
|
||||
>
|
||||
{React.Children.map(children, child => child && cloneElement(child, { style: COL_STYLE }))}
|
||||
</div>)
|
||||
export { SingleLineRow as default }
|
||||
8
src/common/sorted-table/index.css
Normal file
8
src/common/sorted-table/index.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.clickableColumn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickableColumn:hover {
|
||||
color: #fff;
|
||||
background-color: #96b8d1;
|
||||
}
|
||||
360
src/common/sorted-table/index.js
Normal file
360
src/common/sorted-table/index.js
Normal file
@@ -0,0 +1,360 @@
|
||||
import _ from 'intl'
|
||||
import ceil from 'lodash/ceil'
|
||||
import debounce from 'lodash/debounce'
|
||||
import findIndex from 'lodash/findIndex'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import { Dropdown, MenuItem, Pagination } from 'react-bootstrap-4/lib'
|
||||
import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
|
||||
import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
|
||||
import { Portal } from 'react-overlays'
|
||||
|
||||
import Component from '../base-component'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types'
|
||||
import SingleLineRow from '../single-line-row'
|
||||
import { BlockLink } from '../link'
|
||||
import { Container, Col } from '../grid'
|
||||
import { create as createMatcher } from '../complex-matcher'
|
||||
import {
|
||||
createCounter,
|
||||
createFilter,
|
||||
createPager,
|
||||
createSelector,
|
||||
createSort
|
||||
} from '../selectors'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
filters: propTypes.object,
|
||||
nFilteredItems: propTypes.number.isRequired,
|
||||
nItems: propTypes.number.isRequired,
|
||||
onChange: propTypes.func.isRequired
|
||||
})
|
||||
class TableFilter extends Component {
|
||||
_cleanFilter = () => this._setFilter('')
|
||||
|
||||
_setFilter = filterValue => {
|
||||
const { filter } = this.refs
|
||||
filter.value = filterValue
|
||||
filter.focus()
|
||||
this.props.onChange(filterValue)
|
||||
}
|
||||
|
||||
_onChange = event => {
|
||||
this.props.onChange(event.target.value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
|
||||
return (
|
||||
<div className='input-group'>
|
||||
<span className='input-group-addon'>{props.nFilteredItems} / {props.nItems}</span>
|
||||
{isEmpty(props.filters)
|
||||
? <span className='input-group-addon'><Icon icon='search' /></span>
|
||||
: <div className='input-group-btn'>
|
||||
<Dropdown id='filter'>
|
||||
<DropdownToggle bsStyle='info'>
|
||||
<Icon icon='search' />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
{map(props.filters, (filter, label) =>
|
||||
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
|
||||
{_(label)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>}
|
||||
<input
|
||||
type='text'
|
||||
ref='filter'
|
||||
onChange={this._onChange}
|
||||
className='form-control'
|
||||
/>
|
||||
<div className='input-group-btn'>
|
||||
<button className='btn btn-secondary' onClick={this._cleanFilter}>
|
||||
<Icon icon='clear-search' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
columnId: propTypes.number.isRequired,
|
||||
name: propTypes.any.isRequired,
|
||||
sort: propTypes.func,
|
||||
sortIcon: propTypes.string
|
||||
})
|
||||
class ColumnHead extends Component {
|
||||
_sort = () => {
|
||||
const { props } = this
|
||||
props.sort(props.columnId)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { name, sortIcon } = this.props
|
||||
|
||||
if (!this.props.sort) {
|
||||
return <th>{name}</th>
|
||||
}
|
||||
|
||||
let className = styles.clickableColumn
|
||||
|
||||
if (sortIcon === 'asc' || sortIcon === 'desc') {
|
||||
className += ' bg-info'
|
||||
}
|
||||
|
||||
return (
|
||||
<th
|
||||
className={className}
|
||||
onClick={this._sort}
|
||||
>
|
||||
{name}
|
||||
<span className='pull-xs-right'>
|
||||
<Icon icon={sortIcon} />
|
||||
</span>
|
||||
</th>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const DEFAULT_ITEMS_PER_PAGE = 10
|
||||
|
||||
@propTypes({
|
||||
defaultColumn: propTypes.number,
|
||||
collection: propTypes.oneOfType([
|
||||
propTypes.array,
|
||||
propTypes.object
|
||||
]).isRequired,
|
||||
columns: propTypes.arrayOf(propTypes.shape({
|
||||
default: propTypes.bool,
|
||||
name: propTypes.node.isRequired,
|
||||
itemRenderer: propTypes.func.isRequired,
|
||||
sortCriteria: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
]),
|
||||
sortOrder: propTypes.string
|
||||
})).isRequired,
|
||||
filterContainer: propTypes.func,
|
||||
filters: propTypes.object,
|
||||
itemsPerPage: propTypes.number,
|
||||
paginationContainer: propTypes.func,
|
||||
rowLink: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
]),
|
||||
userData: propTypes.any
|
||||
})
|
||||
export default class SortedTable extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
let selectedColumn = props.defaultColumn
|
||||
if (selectedColumn == null) {
|
||||
selectedColumn = findIndex(props.columns, 'default')
|
||||
|
||||
if (selectedColumn === -1) {
|
||||
selectedColumn = 0
|
||||
}
|
||||
}
|
||||
|
||||
this.state = {
|
||||
selectedColumn,
|
||||
itemsPerPage: props.itemsPerPage || DEFAULT_ITEMS_PER_PAGE
|
||||
}
|
||||
|
||||
this._getSelectedColumn = () =>
|
||||
this.props.columns[this.state.selectedColumn]
|
||||
|
||||
this._getTotalNumberOfItems = createCounter(
|
||||
() => this.props.collection
|
||||
)
|
||||
|
||||
this._getAllItems = createSort(
|
||||
createFilter(
|
||||
() => this.props.collection,
|
||||
createSelector(
|
||||
() => this.state.filter || '',
|
||||
createMatcher
|
||||
)
|
||||
),
|
||||
createSelector(
|
||||
() => this._getSelectedColumn().sortCriteria,
|
||||
() => this.props.userData,
|
||||
(sortCriteria, userData) =>
|
||||
(typeof sortCriteria === 'function')
|
||||
? object => sortCriteria(object, userData)
|
||||
: sortCriteria
|
||||
),
|
||||
() => this.state.sortOrder
|
||||
)
|
||||
|
||||
this.state.activePage = 1
|
||||
|
||||
this._getVisibleItems = createPager(
|
||||
this._getAllItems,
|
||||
() => this.state.activePage,
|
||||
this.state.itemsPerPage
|
||||
)
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.setState({
|
||||
sortOrder: this.props.columns[this.state.selectedColumn].sortOrder === 'desc' ? 'desc' : 'asc'
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
// Force one Portal refresh.
|
||||
// Because Portal cannot see the container reference at first rendering.
|
||||
if (this.props.paginationContainer) {
|
||||
this.forceUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
_sort = columnId => {
|
||||
const { state } = this
|
||||
let sortOrder
|
||||
|
||||
if (state.selectedColumn === columnId) {
|
||||
sortOrder = state.sortOrder === 'desc'
|
||||
? 'asc'
|
||||
: 'desc'
|
||||
} else {
|
||||
sortOrder = this.props.columns[columnId].sortOrder === 'desc'
|
||||
? 'desc'
|
||||
: 'asc'
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedColumn: columnId,
|
||||
sortOrder
|
||||
})
|
||||
}
|
||||
|
||||
_onPageSelection = (_, event) => this.setState({
|
||||
activePage: event.eventKey
|
||||
})
|
||||
|
||||
_onFilterChange = debounce(filter => {
|
||||
this.setState({
|
||||
filter,
|
||||
activePage: 1
|
||||
})
|
||||
}, 500)
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const {
|
||||
paginationContainer,
|
||||
filterContainer,
|
||||
filters,
|
||||
rowLink,
|
||||
userData
|
||||
} = props
|
||||
|
||||
const nFilteredItems = this._getAllItems().length
|
||||
|
||||
const paginationInstance = (
|
||||
<Pagination
|
||||
first
|
||||
last
|
||||
prev
|
||||
next
|
||||
ellipsis
|
||||
boundaryLinks
|
||||
maxButtons={10}
|
||||
items={ceil(nFilteredItems / state.itemsPerPage)}
|
||||
activePage={this.state.activePage}
|
||||
onSelect={this._onPageSelection}
|
||||
/>
|
||||
)
|
||||
|
||||
const filterInstance = (
|
||||
<TableFilter
|
||||
filters={filters}
|
||||
nFilteredItems={nFilteredItems}
|
||||
nItems={this._getTotalNumberOfItems()}
|
||||
onChange={this._onFilterChange}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className='table'>
|
||||
<thead className='thead-default'>
|
||||
<tr>
|
||||
{map(props.columns, (column, key) => (
|
||||
<ColumnHead
|
||||
columnId={key}
|
||||
key={key}
|
||||
name={column.name}
|
||||
sort={column.sortCriteria && this._sort}
|
||||
sortIcon={state.selectedColumn === key ? state.sortOrder : 'sort'}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(this._getVisibleItems(), (item, i) => {
|
||||
const columns = map(props.columns, (column, key) => (
|
||||
<td key={key}>
|
||||
{column.itemRenderer(item, userData)}
|
||||
</td>
|
||||
))
|
||||
|
||||
const { id = i } = item
|
||||
|
||||
return rowLink
|
||||
? <BlockLink
|
||||
key={id}
|
||||
tagName='tr'
|
||||
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
|
||||
>{columns}</BlockLink>
|
||||
: <tr key={id}>{columns}</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{(!paginationContainer || !filterContainer) && (
|
||||
<Container>
|
||||
<SingleLineRow>
|
||||
<Col mediumSize={8}>
|
||||
{paginationContainer
|
||||
? (
|
||||
// Rebuild container function to refresh Portal component.
|
||||
<Portal container={() => paginationContainer()}>
|
||||
{paginationInstance}
|
||||
</Portal>
|
||||
) : paginationInstance
|
||||
}
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
{filterContainer
|
||||
? (
|
||||
<Portal container={() => filterContainer()}>
|
||||
{filterInstance}
|
||||
</Portal>
|
||||
) : filterInstance
|
||||
}
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</Container>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
52
src/common/store/actions.js
Normal file
52
src/common/store/actions.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import isFunction from 'lodash/isFunction'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const createAction = (() => {
|
||||
const { defineProperty } = Object
|
||||
const noop = function () {
|
||||
if (arguments.length) {
|
||||
throw new Error('this action expects no payload!')
|
||||
}
|
||||
}
|
||||
|
||||
return (type, payloadCreator = noop) => {
|
||||
const createActionObject = payload => {
|
||||
// Thunks
|
||||
if (isFunction(payload)) {
|
||||
return payload
|
||||
}
|
||||
|
||||
return payload === undefined
|
||||
? { type }
|
||||
: { type, payload }
|
||||
}
|
||||
|
||||
return defineProperty(
|
||||
(...args) => createActionObject(payloadCreator(...args)),
|
||||
'toString',
|
||||
{ value: () => type }
|
||||
)
|
||||
}
|
||||
})()
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const selectLang = createAction('SELECT_LANG', lang => lang)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const connected = createAction('CONNECTED')
|
||||
export const disconnected = createAction('DISCONNECTED')
|
||||
|
||||
export const updateObjects = createAction('UPDATE_OBJECTS', updates => updates)
|
||||
export const updatePermissions = createAction('UPDATE_PERMISSIONS', permissions => permissions)
|
||||
|
||||
export const signedIn = createAction('SIGNED_IN', user => user)
|
||||
export const signedOut = createAction('SIGNED_OUT')
|
||||
|
||||
export const xoaUpdaterState = createAction('XOA_UPDATER_STATE', state => state)
|
||||
export const xoaTrialState = createAction('XOA_TRIAL_STATE', state => state)
|
||||
export const xoaUpdaterLog = createAction('XOA_UPDATER_LOG', log => log)
|
||||
export const xoaRegisterState = createAction('XOA_REGISTER_STATE', registration => registration)
|
||||
export const xoaConfiguration = createAction('XOA_CONFIGURATION', configuration => configuration)
|
||||
13
src/common/store/dev-tools/dev.js
Normal file
13
src/common/store/dev-tools/dev.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import DockMonitor from 'redux-devtools-dock-monitor'
|
||||
import LogMonitor from 'redux-devtools-log-monitor'
|
||||
import React from 'react'
|
||||
import { createDevTools } from 'redux-devtools'
|
||||
|
||||
export default createDevTools(
|
||||
<DockMonitor
|
||||
changePositionKey='ctrl-q'
|
||||
toggleVisibilityKey='ctrl-h'
|
||||
>
|
||||
<LogMonitor />
|
||||
</DockMonitor>
|
||||
)
|
||||
1
src/common/store/dev-tools/index.js
Normal file
1
src/common/store/dev-tools/index.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = false // process.env.NODE_ENV !== 'production' && require('./dev-tools.dev')
|
||||
32
src/common/store/index.js
Normal file
32
src/common/store/index.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import reduxThunk from 'redux-thunk'
|
||||
import {
|
||||
applyMiddleware,
|
||||
combineReducers,
|
||||
compose,
|
||||
createStore
|
||||
} from 'redux'
|
||||
|
||||
import { connectStore as connectXo } from '../xo'
|
||||
|
||||
import DevTools from './dev-tools'
|
||||
import reducer from './reducer'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const enhancers = [
|
||||
applyMiddleware(reduxThunk)
|
||||
]
|
||||
DevTools && enhancers.push(DevTools.instrument())
|
||||
|
||||
const store = createStore(
|
||||
combineReducers(reducer),
|
||||
compose.apply(null, enhancers)
|
||||
)
|
||||
|
||||
connectXo(store)
|
||||
|
||||
if (process.env.XOA_PLAN < 5) {
|
||||
require('xoa-updater').connectStore(store)
|
||||
}
|
||||
|
||||
export default store
|
||||
152
src/common/store/reducer.js
Normal file
152
src/common/store/reducer.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import cookies from 'cookies-js'
|
||||
|
||||
import invoke from '../invoke'
|
||||
|
||||
import * as actions from './actions'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const createAsyncHandler = ({ error, next }) => (state, payload, action) => {
|
||||
if (action.error) {
|
||||
if (error) {
|
||||
return error(state, payload, action)
|
||||
}
|
||||
} else {
|
||||
if (next) {
|
||||
return next(state, payload, action)
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
// Action handlers are reducers but bound to a specific action.
|
||||
const combineActionHandlers = invoke(
|
||||
Object.hasOwnProperty,
|
||||
obj => {
|
||||
for (const prop in obj) {
|
||||
return prop
|
||||
}
|
||||
},
|
||||
(has, firstProp) => (initialState, handlers) => {
|
||||
let n = 0
|
||||
for (const actionType in handlers) {
|
||||
if (has.call(handlers, actionType)) {
|
||||
if (actionType === 'undefined') {
|
||||
throw new Error('invalid action type: undefined')
|
||||
}
|
||||
|
||||
++n
|
||||
|
||||
const handler = handlers[actionType]
|
||||
if (typeof handler === 'object') {
|
||||
handlers[actionType] = createAsyncHandler(handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!n) {
|
||||
throw new Error('no action handlers declared')
|
||||
}
|
||||
|
||||
// Optimization for this special case.
|
||||
if (n === 1) {
|
||||
const actionType = firstProp(handlers)
|
||||
const handler = handlers[actionType]
|
||||
|
||||
return (state = initialState, action) => (
|
||||
action.type === actionType
|
||||
? handler(state, action.payload, action)
|
||||
: state
|
||||
)
|
||||
}
|
||||
|
||||
return (state = initialState, action) => {
|
||||
const handler = handlers[action.type]
|
||||
|
||||
return handler
|
||||
? handler(state, action.payload, action)
|
||||
: state
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default {
|
||||
lang: combineActionHandlers(cookies.get('lang') || 'en', {
|
||||
[actions.selectLang]: (_, lang) => {
|
||||
cookies.set('lang', lang)
|
||||
|
||||
return lang
|
||||
}
|
||||
}),
|
||||
|
||||
permissions: combineActionHandlers({}, {
|
||||
[actions.updatePermissions]: (_, permissions) => permissions
|
||||
}),
|
||||
|
||||
objects: combineActionHandlers({
|
||||
all: {}, // Mutable for performance!
|
||||
byType: {}
|
||||
}, {
|
||||
[actions.updateObjects]: ({ all, byType: prevByType }, updates) => {
|
||||
const byType = { ...prevByType }
|
||||
const get = type => {
|
||||
const curr = byType[type]
|
||||
const prev = prevByType[type]
|
||||
return curr === prev
|
||||
? (byType[type] = { ...prev })
|
||||
: curr
|
||||
}
|
||||
|
||||
for (const id in updates) {
|
||||
const object = updates[id]
|
||||
|
||||
if (object) {
|
||||
all[id] = object
|
||||
get(object.type)[id] = object
|
||||
} else {
|
||||
const previous = all[id]
|
||||
if (previous) {
|
||||
delete all[id]
|
||||
delete get(previous.type)[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { all, byType, fetched: true }
|
||||
}
|
||||
}),
|
||||
|
||||
user: combineActionHandlers(null, {
|
||||
[actions.signedIn]: {
|
||||
next: (_, user) => user
|
||||
}
|
||||
}),
|
||||
|
||||
status: combineActionHandlers('disconnected', {
|
||||
[actions.connected]: () => 'connected',
|
||||
[actions.disconnected]: () => 'disconnected'
|
||||
}),
|
||||
|
||||
xoaUpdaterState: combineActionHandlers('disconnected', {
|
||||
[actions.xoaUpdaterState]: (_, state) => state
|
||||
}),
|
||||
xoaTrialState: combineActionHandlers({}, {
|
||||
[actions.xoaTrialState]: (_, state) => state
|
||||
}),
|
||||
xoaUpdaterLog: combineActionHandlers([], {
|
||||
[actions.xoaUpdaterLog]: (_, log) => log
|
||||
}),
|
||||
xoaRegisterState: combineActionHandlers({state: '?'}, {
|
||||
[actions.xoaRegisterState]: (_, registration) => registration
|
||||
}),
|
||||
xoaConfiguration: combineActionHandlers({proxyHost: '', proxyPort: '', proxyUser: ''}, { // defined values for controlled inputs
|
||||
[actions.xoaConfiguration]: (_, configuration) => {
|
||||
delete configuration.password
|
||||
return configuration
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
45
src/common/tab-button.js
Normal file
45
src/common/tab-button.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react'
|
||||
|
||||
import _ from './intl'
|
||||
import ActionButton from './action-button'
|
||||
import Icon from './icon'
|
||||
import Link from './link'
|
||||
|
||||
const STYLE = {
|
||||
marginBottom: '1em',
|
||||
marginLeft: '1em'
|
||||
}
|
||||
|
||||
const TabButton = ({
|
||||
labelId,
|
||||
...props
|
||||
}) => (
|
||||
<ActionButton
|
||||
{...props}
|
||||
size='large'
|
||||
style={STYLE}
|
||||
><span className='hidden-md-down'>{_(labelId)}</span></ActionButton>
|
||||
)
|
||||
export { TabButton as default }
|
||||
|
||||
export const TabButtonLink = ({
|
||||
labelId,
|
||||
icon,
|
||||
...props
|
||||
}) => (
|
||||
<Link
|
||||
{...props}
|
||||
className='btn btn-lg btn-primary'
|
||||
style={STYLE}
|
||||
>
|
||||
<span className='hidden-md-down'>
|
||||
{icon && (
|
||||
<span>
|
||||
<Icon icon={icon} />
|
||||
{' '}
|
||||
</span>
|
||||
)}
|
||||
{_(labelId)}
|
||||
</span>
|
||||
</Link>
|
||||
)
|
||||
134
src/common/tags.js
Normal file
134
src/common/tags.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import filter from 'lodash/filter'
|
||||
import includes from 'lodash/includes'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
const INPUT_STYLE = {
|
||||
margin: '2px',
|
||||
maxWidth: '4em'
|
||||
}
|
||||
const TAG_STYLE = {
|
||||
backgroundColor: '#2598d9',
|
||||
borderRadius: '0.5em',
|
||||
color: 'white',
|
||||
fontSize: '0.6em',
|
||||
margin: '0.2em',
|
||||
marginTop: '-0.1em',
|
||||
padding: '0.3em',
|
||||
verticalAlign: 'middle'
|
||||
}
|
||||
const ADD_TAG_STYLE = {
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8em',
|
||||
marginLeft: '0.2em'
|
||||
}
|
||||
const REMOVE_TAG_STYLE = {
|
||||
cursor: 'pointer'
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
labels: propTypes.arrayOf(React.PropTypes.string).isRequired,
|
||||
onChange: propTypes.func,
|
||||
onDelete: propTypes.func,
|
||||
onAdd: propTypes.func
|
||||
})
|
||||
export default class Tags extends Component {
|
||||
componentWillMount () {
|
||||
this.setState({editing: false})
|
||||
}
|
||||
|
||||
_startEdit = () => {
|
||||
this.setState({ editing: true })
|
||||
}
|
||||
_stopEdit = () => {
|
||||
this.setState({ editing: false })
|
||||
}
|
||||
|
||||
_addTag = newTag => {
|
||||
const { labels, onAdd, onChange } = this.props
|
||||
|
||||
if (!includes(labels, newTag)) {
|
||||
onAdd && onAdd(newTag)
|
||||
onChange && onChange([ ...labels, newTag ])
|
||||
}
|
||||
}
|
||||
_deleteTag = tag => {
|
||||
const { onChange, onDelete } = this.props
|
||||
|
||||
onDelete && onDelete(tag)
|
||||
onChange && onChange(filter(this.props.labels, t => t !== tag))
|
||||
}
|
||||
|
||||
_onKeyDown = event => {
|
||||
const { keyCode, target } = event
|
||||
|
||||
if (keyCode === 13) {
|
||||
if (target.value) {
|
||||
this._addTag(target.value)
|
||||
target.value = ''
|
||||
}
|
||||
} else if (keyCode === 27) {
|
||||
this._stopEdit()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
labels,
|
||||
onAdd,
|
||||
onChange,
|
||||
onDelete
|
||||
} = this.props
|
||||
|
||||
const deleteTag = (onDelete || onChange) && this._deleteTag
|
||||
|
||||
return (
|
||||
<span className='form-group' style={{ color: '#999' }}>
|
||||
<Icon icon='tags' />
|
||||
{' '}
|
||||
<span>
|
||||
{map(labels.sort(), (label, index) =>
|
||||
<Tag label={label} onDelete={deleteTag} key={index} />
|
||||
)}
|
||||
</span>
|
||||
{(onAdd || onChange) && !this.state.editing
|
||||
? <span onClick={this._startEdit} style={ADD_TAG_STYLE}>
|
||||
<Icon icon='add-tag' />
|
||||
</span>
|
||||
: <span>
|
||||
<input
|
||||
type='text'
|
||||
autoFocus
|
||||
style={INPUT_STYLE}
|
||||
onKeyDown={this._onKeyDown}
|
||||
onBlur={this._stopEdit}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const Tag = ({ label, onDelete }) => (
|
||||
<span style={TAG_STYLE}>
|
||||
{label}{' '}
|
||||
{onDelete
|
||||
? <span onClick={onDelete && (() => onDelete(label))} style={REMOVE_TAG_STYLE}>
|
||||
<Icon icon='remove-tag' />
|
||||
</span>
|
||||
: []
|
||||
}
|
||||
</span>
|
||||
)
|
||||
Tag.propTypes = {
|
||||
label: React.PropTypes.string.isRequired
|
||||
}
|
||||
106
src/common/timezone-picker.js
Normal file
106
src/common/timezone-picker.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import ActionButton from 'action-button'
|
||||
import map from 'lodash/map'
|
||||
import moment from 'moment-timezone'
|
||||
import React from 'react'
|
||||
|
||||
import _ from './intl'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import { getXoServerTimezone } from './xo'
|
||||
import { Select } from './form'
|
||||
|
||||
const XO_SERVER_TIMEZONE = 'xo-server'
|
||||
|
||||
@propTypes({
|
||||
defaultValue: propTypes.string,
|
||||
onChange: propTypes.func.isRequired,
|
||||
value: propTypes.string
|
||||
})
|
||||
export default class TimezonePicker extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state.options = map(moment.tz.names(), value => ({ label: value, value }))
|
||||
}
|
||||
|
||||
get value () {
|
||||
const value = this.refs.select.value
|
||||
return (value === XO_SERVER_TIMEZONE) ? null : value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value || XO_SERVER_TIMEZONE
|
||||
}
|
||||
|
||||
_updateTimezone (value) {
|
||||
this.props.onChange(value)
|
||||
}
|
||||
_handleChange = option => {
|
||||
return this._updateTimezone(
|
||||
!option || option.value === XO_SERVER_TIMEZONE
|
||||
? null
|
||||
: option.value
|
||||
)
|
||||
}
|
||||
_useServerTime = () => {
|
||||
this._updateTimezone(null)
|
||||
}
|
||||
_useLocalTime = () => {
|
||||
this._updateTimezone(moment.tz.guess())
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
// Use local timezone (Web browser) if no default value.
|
||||
if (this.props.value === undefined) {
|
||||
this._useLocalTime()
|
||||
}
|
||||
|
||||
getXoServerTimezone.then(serverTimezone => {
|
||||
this.setState({
|
||||
options: [{
|
||||
label: _('serverTimezoneOption', {
|
||||
value: serverTimezone
|
||||
}),
|
||||
value: XO_SERVER_TIMEZONE
|
||||
}].concat(this.state.options),
|
||||
serverTimezone
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
return (
|
||||
<div>
|
||||
<div className='alert alert-info' role='alert'>
|
||||
{_('timezonePickerServerValue')} <strong>{state.serverTimezone}</strong>
|
||||
</div>
|
||||
<Select
|
||||
className='m-b-1'
|
||||
defaultValue={props.defaultValue}
|
||||
onChange={this._handleChange}
|
||||
options={state.options}
|
||||
placeholder={_('selectTimezone')}
|
||||
ref='select'
|
||||
value={props.value || XO_SERVER_TIMEZONE}
|
||||
/>
|
||||
<div className='pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='m-r-1'
|
||||
handler={this._useServerTime}
|
||||
icon='time'
|
||||
>
|
||||
{_('timezonePickerUseServerTime')}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
btnStyle='secondary'
|
||||
handler={this._useLocalTime}
|
||||
icon='time'
|
||||
>
|
||||
{_('timezonePickerUseLocalTime')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
287
src/common/tooltip/get-position.js
Normal file
287
src/common/tooltip/get-position.js
Normal file
@@ -0,0 +1,287 @@
|
||||
// Source: https://github.com/wwayne/react-tooltip/blob/master/src/utils/getPosition.js
|
||||
|
||||
/**
|
||||
* Calculate the position of tooltip
|
||||
*
|
||||
* @params
|
||||
* - `e` {Event} the event of current mouse
|
||||
* - `target` {Element} the currentTarget of the event
|
||||
* - `node` {DOM} the react-tooltip object
|
||||
* - `place` {String} top / right / bottom / left
|
||||
* - `effect` {String} float / solid
|
||||
* - `offset` {Object} the offset to default position
|
||||
*
|
||||
* @return {Object
|
||||
* - `isNewState` {Bool} required
|
||||
* - `newState` {Object}
|
||||
* - `position` {OBject} {left: {Number}, top: {Number}}
|
||||
*/
|
||||
export default function (e, target, node, place, effect, offset) {
|
||||
const tipWidth = node.clientWidth
|
||||
const tipHeight = node.clientHeight
|
||||
const {mouseX, mouseY} = getCurrentOffset(e, target, effect)
|
||||
const defaultOffset = getDefaultPosition(effect, target.clientWidth, target.clientHeight, tipWidth, tipHeight)
|
||||
const {extraOffsetX, extraOffsetY} = calculateOffset(offset)
|
||||
|
||||
const windowWidth = window.innerWidth
|
||||
const windowHeight = window.innerHeight
|
||||
|
||||
const {parentTop, parentLeft} = getParent(target)
|
||||
|
||||
// Get the edge offset of the tooltip
|
||||
const getTipOffsetLeft = (place) => {
|
||||
const offsetX = defaultOffset[place].l
|
||||
return mouseX + offsetX + extraOffsetX
|
||||
}
|
||||
const getTipOffsetRight = (place) => {
|
||||
const offsetX = defaultOffset[place].r
|
||||
return mouseX + offsetX + extraOffsetX
|
||||
}
|
||||
const getTipOffsetTop = (place) => {
|
||||
const offsetY = defaultOffset[place].t
|
||||
return mouseY + offsetY + extraOffsetY
|
||||
}
|
||||
const getTipOffsetBottom = (place) => {
|
||||
const offsetY = defaultOffset[place].b
|
||||
return mouseY + offsetY + extraOffsetY
|
||||
}
|
||||
|
||||
// Judge if the tooltip has over the window(screen)
|
||||
const outsideVertical = () => {
|
||||
let result = false
|
||||
let newPlace
|
||||
if (getTipOffsetTop('left') < 0 &&
|
||||
getTipOffsetBottom('left') <= windowHeight &&
|
||||
getTipOffsetBottom('bottom') <= windowHeight) {
|
||||
result = true
|
||||
newPlace = 'bottom'
|
||||
} else if (getTipOffsetBottom('left') > windowHeight &&
|
||||
getTipOffsetTop('left') >= 0 &&
|
||||
getTipOffsetTop('top') >= 0) {
|
||||
result = true
|
||||
newPlace = 'top'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
const outsideLeft = () => {
|
||||
let {result, newPlace} = outsideVertical() // Deal with vertical as first priority
|
||||
if (result && outsideHorizontal().result) {
|
||||
return {result: false} // No need to change, if change to vertical will out of space
|
||||
}
|
||||
if (!result && getTipOffsetLeft('left') < 0 && getTipOffsetRight('right') <= windowWidth) {
|
||||
result = true // If vertical ok, but let out of side and right won't out of side
|
||||
newPlace = 'right'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
const outsideRight = () => {
|
||||
let {result, newPlace} = outsideVertical()
|
||||
if (result && outsideHorizontal().result) {
|
||||
return {result: false} // No need to change, if change to vertical will out of space
|
||||
}
|
||||
if (!result && getTipOffsetRight('right') > windowWidth && getTipOffsetLeft('left') >= 0) {
|
||||
result = true
|
||||
newPlace = 'left'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
|
||||
const outsideHorizontal = () => {
|
||||
let result = false
|
||||
let newPlace
|
||||
if (getTipOffsetLeft('top') < 0 &&
|
||||
getTipOffsetRight('top') <= windowWidth &&
|
||||
getTipOffsetRight('right') <= windowWidth) {
|
||||
result = true
|
||||
newPlace = 'right'
|
||||
} else if (getTipOffsetRight('top') > windowWidth &&
|
||||
getTipOffsetLeft('top') >= 0 &&
|
||||
getTipOffsetLeft('left') >= 0) {
|
||||
result = true
|
||||
newPlace = 'left'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
const outsideTop = () => {
|
||||
let {result, newPlace} = outsideHorizontal()
|
||||
if (result && outsideVertical().result) {
|
||||
return {result: false}
|
||||
}
|
||||
if (!result && getTipOffsetTop('top') < 0 && getTipOffsetBottom('bottom') <= windowHeight) {
|
||||
result = true
|
||||
newPlace = 'bottom'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
const outsideBottom = () => {
|
||||
let {result, newPlace} = outsideHorizontal()
|
||||
if (result && outsideVertical().result) {
|
||||
return {result: false}
|
||||
}
|
||||
if (!result && getTipOffsetBottom('bottom') > windowHeight && getTipOffsetTop('top') >= 0) {
|
||||
result = true
|
||||
newPlace = 'top'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
|
||||
// Return new state to change the placement to the reverse if possible
|
||||
const outsideLeftResult = outsideLeft()
|
||||
const outsideRightResult = outsideRight()
|
||||
const outsideTopResult = outsideTop()
|
||||
const outsideBottomResult = outsideBottom()
|
||||
|
||||
if (place === 'left' && outsideLeftResult.result) {
|
||||
return {
|
||||
isNewState: true,
|
||||
newState: {place: outsideLeftResult.newPlace}
|
||||
}
|
||||
} else if (place === 'right' && outsideRightResult.result) {
|
||||
return {
|
||||
isNewState: true,
|
||||
newState: {place: outsideRightResult.newPlace}
|
||||
}
|
||||
} else if (place === 'top' && outsideTopResult.result) {
|
||||
return {
|
||||
isNewState: true,
|
||||
newState: {place: outsideTopResult.newPlace}
|
||||
}
|
||||
} else if (place === 'bottom' && outsideBottomResult.result) {
|
||||
return {
|
||||
isNewState: true,
|
||||
newState: {place: outsideBottomResult.newPlace}
|
||||
}
|
||||
}
|
||||
|
||||
// Return tooltip offset position
|
||||
return {
|
||||
isNewState: false,
|
||||
position: {
|
||||
left: getTipOffsetLeft(place) - parentLeft,
|
||||
top: getTipOffsetTop(place) - parentTop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get current mouse offset
|
||||
const getCurrentOffset = (e, currentTarget, effect) => {
|
||||
const boundingClientRect = currentTarget.getBoundingClientRect()
|
||||
const targetTop = boundingClientRect.top
|
||||
const targetLeft = boundingClientRect.left
|
||||
const targetWidth = currentTarget.clientWidth
|
||||
const targetHeight = currentTarget.clientHeight
|
||||
|
||||
if (effect === 'float') {
|
||||
return {
|
||||
mouseX: e.clientX,
|
||||
mouseY: e.clientY
|
||||
}
|
||||
}
|
||||
return {
|
||||
mouseX: targetLeft + (targetWidth / 2),
|
||||
mouseY: targetTop + (targetHeight / 2)
|
||||
}
|
||||
}
|
||||
|
||||
// List all possibility of tooltip final offset
|
||||
// This is useful in judging if it is necessary for tooltip to switch position when out of window
|
||||
const getDefaultPosition = (effect, targetWidth, targetHeight, tipWidth, tipHeight) => {
|
||||
let top
|
||||
let right
|
||||
let bottom
|
||||
let left
|
||||
const disToMouse = 3
|
||||
const triangleHeight = 2
|
||||
const cursorHeight = 12 // Optimize for float bottom only, cause the cursor will hide the tooltip
|
||||
|
||||
if (effect === 'float') {
|
||||
top = {
|
||||
l: -(tipWidth / 2),
|
||||
r: tipWidth / 2,
|
||||
t: -(tipHeight + disToMouse + triangleHeight),
|
||||
b: -disToMouse
|
||||
}
|
||||
bottom = {
|
||||
l: -(tipWidth / 2),
|
||||
r: tipWidth / 2,
|
||||
t: disToMouse + cursorHeight,
|
||||
b: tipHeight + disToMouse + triangleHeight + cursorHeight
|
||||
}
|
||||
left = {
|
||||
l: -(tipWidth + disToMouse + triangleHeight),
|
||||
r: -disToMouse,
|
||||
t: -(tipHeight / 2),
|
||||
b: tipHeight / 2
|
||||
}
|
||||
right = {
|
||||
l: disToMouse,
|
||||
r: tipWidth + disToMouse + triangleHeight,
|
||||
t: -(tipHeight / 2),
|
||||
b: tipHeight / 2
|
||||
}
|
||||
} else if (effect === 'solid') {
|
||||
top = {
|
||||
l: -(tipWidth / 2),
|
||||
r: tipWidth / 2,
|
||||
t: -(targetHeight / 2 + tipHeight + triangleHeight),
|
||||
b: -(targetHeight / 2)
|
||||
}
|
||||
bottom = {
|
||||
l: -(tipWidth / 2),
|
||||
r: tipWidth / 2,
|
||||
t: targetHeight / 2,
|
||||
b: targetHeight / 2 + tipHeight + triangleHeight
|
||||
}
|
||||
left = {
|
||||
l: -(tipWidth + targetWidth / 2 + triangleHeight),
|
||||
r: -(targetWidth / 2),
|
||||
t: -(tipHeight / 2),
|
||||
b: tipHeight / 2
|
||||
}
|
||||
right = {
|
||||
l: targetWidth / 2,
|
||||
r: tipWidth + targetWidth / 2 + triangleHeight,
|
||||
t: -(tipHeight / 2),
|
||||
b: tipHeight / 2
|
||||
}
|
||||
}
|
||||
|
||||
return {top, bottom, left, right}
|
||||
}
|
||||
|
||||
// Consider additional offset into position calculation
|
||||
const calculateOffset = (offset) => {
|
||||
let extraOffsetX = 0
|
||||
let extraOffsetY = 0
|
||||
|
||||
if (Object.prototype.toString.apply(offset) === '[object String]') {
|
||||
offset = JSON.parse(offset.toString().replace(/'/g, '"'))
|
||||
}
|
||||
for (let key in offset) {
|
||||
if (key === 'top') {
|
||||
extraOffsetY -= parseInt(offset[key], 10)
|
||||
} else if (key === 'bottom') {
|
||||
extraOffsetY += parseInt(offset[key], 10)
|
||||
} else if (key === 'left') {
|
||||
extraOffsetX -= parseInt(offset[key], 10)
|
||||
} else if (key === 'right') {
|
||||
extraOffsetX += parseInt(offset[key], 10)
|
||||
}
|
||||
}
|
||||
|
||||
return {extraOffsetX, extraOffsetY}
|
||||
}
|
||||
|
||||
// Get the offset of the parent elements
|
||||
const getParent = (currentTarget) => {
|
||||
let currentParent = currentTarget
|
||||
while (currentParent) {
|
||||
if (currentParent.style.transform.length > 0) break
|
||||
currentParent = currentParent.parentElement
|
||||
}
|
||||
|
||||
const parentTop = currentParent && currentParent.getBoundingClientRect().top || 0
|
||||
const parentLeft = currentParent && currentParent.getBoundingClientRect().left || 0
|
||||
|
||||
return {parentTop, parentLeft}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user