Compare commits
787 Commits
v5.1.3
...
computed-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
595c4bd5a8 | ||
|
|
1a2f553094 | ||
|
|
4d69866532 | ||
|
|
495c97b44b | ||
|
|
e817b3254e | ||
|
|
dd6987efe9 | ||
|
|
d7f8d12d88 | ||
|
|
504895a730 | ||
|
|
cde92836f3 | ||
|
|
c787988b06 | ||
|
|
898434b267 | ||
|
|
6e44c65a07 | ||
|
|
03028bca50 | ||
|
|
c8669dc88f | ||
|
|
82240979c2 | ||
|
|
db5d495105 | ||
|
|
6e8dfe8833 | ||
|
|
242d9e20c4 | ||
|
|
e446eb0cd0 | ||
|
|
b63efe579a | ||
|
|
f3410f1491 | ||
|
|
b27ac11d56 | ||
|
|
a55d73614e | ||
|
|
25cd1957c7 | ||
|
|
abd97abc24 | ||
|
|
6ddfd909f0 | ||
|
|
e054eec555 | ||
|
|
e253657770 | ||
|
|
102e629e16 | ||
|
|
242a02836c | ||
|
|
6936f223f3 | ||
|
|
eb8dfc86ca | ||
|
|
02c715e1cc | ||
|
|
8cb53b0c4e | ||
|
|
629f68ffd7 | ||
|
|
d3691313e6 | ||
|
|
9aed4f6fba | ||
|
|
ef17cb1c6c | ||
|
|
ecc086f15d | ||
|
|
be9eb8ce91 | ||
|
|
18ca6b935c | ||
|
|
b3769019e5 | ||
|
|
506a6b0cf4 | ||
|
|
a18df93c4f | ||
|
|
684269321b | ||
|
|
d7eeeca268 | ||
|
|
4c21175ca7 | ||
|
|
377efcd054 | ||
|
|
072401f600 | ||
|
|
1ce7d94261 | ||
|
|
8178de8a6b | ||
|
|
eb37c7d7d8 | ||
|
|
9465459ef9 | ||
|
|
52a71cec91 | ||
|
|
2e1b32fadc | ||
|
|
0c8f3ea824 | ||
|
|
bf45cdd2b8 | ||
|
|
fd47403ec9 | ||
|
|
137b8e7f7f | ||
|
|
4a8c5a980a | ||
|
|
233aca7911 | ||
|
|
0178ce0a91 | ||
|
|
142233453a | ||
|
|
b12a804fb3 | ||
|
|
3fe5efbfab | ||
|
|
d71323a67d | ||
|
|
eb46711e34 | ||
|
|
748b09d8fa | ||
|
|
e6a32b53fc | ||
|
|
7ce9e8a959 | ||
|
|
b817cb86d0 | ||
|
|
3e1b2119c4 | ||
|
|
bfa31be3b7 | ||
|
|
584da2f56a | ||
|
|
6a071942a5 | ||
|
|
ac8787e930 | ||
|
|
ab60bc46cf | ||
|
|
b67310ae75 | ||
|
|
020618554a | ||
|
|
38ec7ac34f | ||
|
|
a78151c93e | ||
|
|
285b1fb36e | ||
|
|
16e8c87cc6 | ||
|
|
93ffb77e81 | ||
|
|
88f6d77047 | ||
|
|
3ff63927f3 | ||
|
|
d396593d99 | ||
|
|
62b64ad0b6 | ||
|
|
6f33a79644 | ||
|
|
67f31407d7 | ||
|
|
f05d2d0063 | ||
|
|
682deb4b56 | ||
|
|
64fac454b5 | ||
|
|
e9c60bc958 | ||
|
|
7a8c0831bd | ||
|
|
0ca1af8606 | ||
|
|
e81f88e676 | ||
|
|
e96a8af9ef | ||
|
|
d8393d8500 | ||
|
|
44b74e6135 | ||
|
|
f31417a85b | ||
|
|
1c6967594c | ||
|
|
59f8a58b21 | ||
|
|
4d1f647a89 | ||
|
|
86e5206b4d | ||
|
|
105ede5b1d | ||
|
|
bb8a25cc9d | ||
|
|
54c3d843be | ||
|
|
4a1407786c | ||
|
|
f5e3aef86c | ||
|
|
37c8a7c2b2 | ||
|
|
1a788fae7e | ||
|
|
8efc083a70 | ||
|
|
f196a9ebc4 | ||
|
|
06704ce467 | ||
|
|
8524db2903 | ||
|
|
60df3bc633 | ||
|
|
5014b95206 | ||
|
|
a2464fa968 | ||
|
|
033153c8b9 | ||
|
|
a74a857ffe | ||
|
|
f0fe369cfd | ||
|
|
457ba5f24c | ||
|
|
d41b04313a | ||
|
|
34be34e7b3 | ||
|
|
dbc9fdcfa6 | ||
|
|
76b20f0fb6 | ||
|
|
80ca2052c2 | ||
|
|
3e5d8be507 | ||
|
|
114e5e1fa0 | ||
|
|
c38d4e275b | ||
|
|
8cc9dea9aa | ||
|
|
d3dcf6d305 | ||
|
|
02439bd23d | ||
|
|
a9eb1f3d27 | ||
|
|
9a0544c4aa | ||
|
|
31c365313b | ||
|
|
b44017ca95 | ||
|
|
289112af27 | ||
|
|
4d2dc4eece | ||
|
|
712101d8d6 | ||
|
|
828ba5d448 | ||
|
|
03a2ff8e8c | ||
|
|
75487203cf | ||
|
|
6ad751f079 | ||
|
|
78ddad839e | ||
|
|
812cecdcc4 | ||
|
|
4b49da7d8f | ||
|
|
fc8c37d66c | ||
|
|
0d618e6477 | ||
|
|
d7e2b12d3d | ||
|
|
2ae4ed3999 | ||
|
|
eaaf70e52e | ||
|
|
bb4ebcd198 | ||
|
|
a8b7431a02 | ||
|
|
a5ec70f7fa | ||
|
|
75dfdd4854 | ||
|
|
3f29dd129f | ||
|
|
7b19341406 | ||
|
|
838ad58946 | ||
|
|
ec4a7325da | ||
|
|
efbd588c9e | ||
|
|
e535e064fa | ||
|
|
448068178b | ||
|
|
7308d9ca96 | ||
|
|
e642f54815 | ||
|
|
ebb6cb17ea | ||
|
|
08a0aa9f98 | ||
|
|
8a933c98e3 | ||
|
|
363b22bffe | ||
|
|
79a85659aa | ||
|
|
eca145e113 | ||
|
|
04b75fb3b3 | ||
|
|
582e220a02 | ||
|
|
2e87abefc4 | ||
|
|
5ea19ee56f | ||
|
|
2ee733399e | ||
|
|
73f228c719 | ||
|
|
ba79673715 | ||
|
|
86b0962063 | ||
|
|
e2a482a6ca | ||
|
|
8924444cc1 | ||
|
|
8e83b0ffd2 | ||
|
|
451384bcdc | ||
|
|
b733164c50 | ||
|
|
72d9d8ba86 | ||
|
|
7c7646c65c | ||
|
|
b1c087451e | ||
|
|
f0f72f3bdd | ||
|
|
0ab3267541 | ||
|
|
995e76d323 | ||
|
|
62a9f805c2 | ||
|
|
84ea95a641 | ||
|
|
316de42cd9 | ||
|
|
bc2256fc86 | ||
|
|
f0d85f4c4e | ||
|
|
1801f9cb06 | ||
|
|
4be018ad15 | ||
|
|
5dcf060975 | ||
|
|
59f6b1f0c8 | ||
|
|
ae38a85b19 | ||
|
|
324dbbcfc8 | ||
|
|
9770b77df4 | ||
|
|
0f91de389a | ||
|
|
7f5a623b37 | ||
|
|
c7cf73ff05 | ||
|
|
4aab425cef | ||
|
|
0d9666639f | ||
|
|
6c26c09685 | ||
|
|
819f650b48 | ||
|
|
353eba6365 | ||
|
|
063302b91d | ||
|
|
562b51bc2f | ||
|
|
e33a6f9a05 | ||
|
|
b9db4e7704 | ||
|
|
3270d9c3a7 | ||
|
|
6d7399f96c | ||
|
|
886ef87bc5 | ||
|
|
1e5dc9efe7 | ||
|
|
28ec66bf3b | ||
|
|
9199784a23 | ||
|
|
c7e447db6f | ||
|
|
f81615f8b6 | ||
|
|
12caceb02b | ||
|
|
30f71ab444 | ||
|
|
fe04481ca3 | ||
|
|
7766e8edcd | ||
|
|
31d417c9d3 | ||
|
|
5ed29197cf | ||
|
|
ff5f3e12d3 | ||
|
|
240180405c | ||
|
|
edca6495fc | ||
|
|
8a9b753b01 | ||
|
|
445fc696c9 | ||
|
|
492e2362be | ||
|
|
1acee209be | ||
|
|
6785c48709 | ||
|
|
808e674503 | ||
|
|
6b2650282d | ||
|
|
475be2ee30 | ||
|
|
12e1da4ef2 | ||
|
|
780d072bb7 | ||
|
|
f7e5a5cf92 | ||
|
|
3574c8de5c | ||
|
|
b09ab4d403 | ||
|
|
1997f4af51 | ||
|
|
347cd063a3 | ||
|
|
74a4519a33 | ||
|
|
20acf7cfb2 | ||
|
|
99bc34b2da | ||
|
|
f65b5e3ddd | ||
|
|
dc10492b84 | ||
|
|
6f7c10537b | ||
|
|
7f503cfc21 | ||
|
|
9dbef0c20a | ||
|
|
923166b4e3 | ||
|
|
b420128e40 | ||
|
|
7776a6ce23 | ||
|
|
8db949734a | ||
|
|
bb5bdfb9b2 | ||
|
|
9fac3ecd81 | ||
|
|
8a84cc2627 | ||
|
|
61179ec67d | ||
|
|
59fc5955ba | ||
|
|
e853ba6244 | ||
|
|
fb40ae7264 | ||
|
|
f629047be2 | ||
|
|
278d8adf1b | ||
|
|
87087d55aa | ||
|
|
46e95fe7eb | ||
|
|
090c9ea4d7 | ||
|
|
647eb7299e | ||
|
|
027652e80a | ||
|
|
185d380c36 | ||
|
|
9008b5c4e7 | ||
|
|
f5ad59803e | ||
|
|
81d1d7ba13 | ||
|
|
3328e71805 | ||
|
|
d7e3dbac26 | ||
|
|
905182bf2e | ||
|
|
a0146290ee | ||
|
|
173aa22432 | ||
|
|
9e5b871ebe | ||
|
|
8824ce55ec | ||
|
|
155edf5533 | ||
|
|
6d06e1f89d | ||
|
|
6d1e2c47d3 | ||
|
|
8b9b0346cb | ||
|
|
0d11817e3f | ||
|
|
a8cb209717 | ||
|
|
cf45ffddf1 | ||
|
|
2e0ea51c30 | ||
|
|
0f7f8c7330 | ||
|
|
808f72409f | ||
|
|
f8e2d29372 | ||
|
|
22dec27c65 | ||
|
|
89b3806a7a | ||
|
|
b6bedf9253 | ||
|
|
0d4983043b | ||
|
|
f9ff3fe168 | ||
|
|
4a25c5323f | ||
|
|
9b4e2d3bb8 | ||
|
|
3915efcf92 | ||
|
|
4591ff8522 | ||
|
|
e3491797f3 | ||
|
|
6eee167675 | ||
|
|
16b965b28a | ||
|
|
5125410efd | ||
|
|
1a4da2a8de | ||
|
|
991fbaec86 | ||
|
|
fb399278b3 | ||
|
|
b868092365 | ||
|
|
80fdc6849f | ||
|
|
25ffcb952b | ||
|
|
083ac1e2d6 | ||
|
|
5a4b553a60 | ||
|
|
b1135ef566 | ||
|
|
1928d1e00f | ||
|
|
a369f7f387 | ||
|
|
33d9801dfe | ||
|
|
8c7a031cca | ||
|
|
9484d87e76 | ||
|
|
4b6822d6e5 | ||
|
|
7241a0529b | ||
|
|
66083b4e50 | ||
|
|
f631b3cc64 | ||
|
|
bb58d9b4d6 | ||
|
|
93ebff1055 | ||
|
|
08aec1c09a | ||
|
|
8ca98a56fe | ||
|
|
705f53e3e5 | ||
|
|
adaf069d20 | ||
|
|
d7be7d8660 | ||
|
|
faddee86b6 | ||
|
|
c4fcc65d16 | ||
|
|
890631d33b | ||
|
|
8e8145bb48 | ||
|
|
d73d6719a5 | ||
|
|
3419bee198 | ||
|
|
4368fad393 | ||
|
|
ab93fdbf10 | ||
|
|
8fd7697a45 | ||
|
|
1121a60912 | ||
|
|
e7b4bd2fe4 | ||
|
|
fcd8bdd1b3 | ||
|
|
e6f140f575 | ||
|
|
bfe4c45fcf | ||
|
|
f95370124b | ||
|
|
2564343816 | ||
|
|
03734eb761 | ||
|
|
29d63a9fdd | ||
|
|
ca94b236a8 | ||
|
|
fa1ec30ba5 | ||
|
|
2b1423aebe | ||
|
|
373332141f | ||
|
|
ecf2cf15b5 | ||
|
|
4ee0831d93 | ||
|
|
7df2a88c13 | ||
|
|
3d52556c67 | ||
|
|
437b160a3f | ||
|
|
5c87b82e0c | ||
|
|
7f2bc79d5f | ||
|
|
837a61acf3 | ||
|
|
5971eed72a | ||
|
|
1b8224030b | ||
|
|
ed3ec3fa8b | ||
|
|
aa98ca49e5 | ||
|
|
44d35c2351 | ||
|
|
df8eb7a000 | ||
|
|
ac061c8750 | ||
|
|
656d3e55ac | ||
|
|
50641287f8 | ||
|
|
0bc072aa65 | ||
|
|
9d7d665520 | ||
|
|
819ea94e7b | ||
|
|
40753568df | ||
|
|
8793aed561 | ||
|
|
377a50bc09 | ||
|
|
fe5a43fbdf | ||
|
|
7f44220220 | ||
|
|
0df1610ca9 | ||
|
|
24c8b9e02d | ||
|
|
01b311f2ba | ||
|
|
a2bb3182f4 | ||
|
|
c86e15a310 | ||
|
|
862e5a95e7 | ||
|
|
73e2c7e849 | ||
|
|
0b0937e233 | ||
|
|
6bf114859f | ||
|
|
db6d67eeb7 | ||
|
|
a345d89aac | ||
|
|
e8f8ebb112 | ||
|
|
1dad5b5c3a | ||
|
|
5cc5ee4e87 | ||
|
|
e8d2b32a14 | ||
|
|
f492909e42 | ||
|
|
7ea17750a1 | ||
|
|
663e1f1a4b | ||
|
|
079310c67e | ||
|
|
5cf7f1f886 | ||
|
|
9f64af859e | ||
|
|
007aa776cb | ||
|
|
66bc092edd | ||
|
|
140a88ee12 | ||
|
|
f42758938d | ||
|
|
e19fd81536 | ||
|
|
73835ded96 | ||
|
|
1ec1a8bd94 | ||
|
|
f0b6d57ba8 | ||
|
|
f9a3ad14d1 | ||
|
|
1b86f533f7 | ||
|
|
46416fb026 | ||
|
|
54ed37c95d | ||
|
|
fd79b47d9e | ||
|
|
be8333824b | ||
|
|
55daffc791 | ||
|
|
375baf7fe5 | ||
|
|
815e74c93c | ||
|
|
547d6fbc93 | ||
|
|
b45a4b9e6c | ||
|
|
3436d0256a | ||
|
|
2627cfd426 | ||
|
|
34b18c00a1 | ||
|
|
e13af7f5f0 | ||
|
|
ca08613292 | ||
|
|
4ab63591a0 | ||
|
|
5b4f98b03b | ||
|
|
f396d61633 | ||
|
|
9f8c0c8cdf | ||
|
|
198777ffab | ||
|
|
29c5ca1132 | ||
|
|
05d6f3d1ed | ||
|
|
536e82de3d | ||
|
|
c59be7c315 | ||
|
|
b327bb5bd0 | ||
|
|
a3103587f5 | ||
|
|
1bb11b574f | ||
|
|
405efe6a31 | ||
|
|
73663c3703 | ||
|
|
421ee7125b | ||
|
|
1a6166b63c | ||
|
|
3828e75b7d | ||
|
|
154da142c7 | ||
|
|
312cd60dd1 | ||
|
|
6bf522f72f | ||
|
|
a844f8d459 | ||
|
|
8ee206174b | ||
|
|
1a08e24a5c | ||
|
|
086cd0e038 | ||
|
|
42d123318c | ||
|
|
89f160317c | ||
|
|
9ccd1a0362 | ||
|
|
d116d014bc | ||
|
|
7956cabcf4 | ||
|
|
36c61ad357 | ||
|
|
25d60360d5 | ||
|
|
1e5579e3ad | ||
|
|
77d43b2280 | ||
|
|
33e8929e8b | ||
|
|
b79fa9cb9f | ||
|
|
a2812a85bd | ||
|
|
e8ff46a8ba | ||
|
|
351c01d642 | ||
|
|
e333b1d083 | ||
|
|
5ad49de642 | ||
|
|
b45bb5c144 | ||
|
|
9402596f69 | ||
|
|
096687ae2c | ||
|
|
210b5de992 | ||
|
|
f742fdbf1b | ||
|
|
e7026c522d | ||
|
|
c21fc4beda | ||
|
|
edf6fe782e | ||
|
|
3cbb6c4a98 | ||
|
|
568a50acc5 | ||
|
|
fbcb756cef | ||
|
|
81eb4ba4f9 | ||
|
|
0cc14d2ab8 | ||
|
|
6aedadc982 | ||
|
|
a8d10dab3c | ||
|
|
1ff6ff1d7a | ||
|
|
8afe4a85dc | ||
|
|
c57fbdce63 | ||
|
|
bdc0278fd1 | ||
|
|
c3ac8d0587 | ||
|
|
f3a5e1e97c | ||
|
|
919aa5fc43 | ||
|
|
416c98ffd2 | ||
|
|
8094447183 | ||
|
|
575375d3e0 | ||
|
|
4296ae02dc | ||
|
|
0e40af0515 | ||
|
|
5d3a0e7a41 | ||
|
|
8ae2aae37a | ||
|
|
83b3cf406a | ||
|
|
1643ced4e0 | ||
|
|
b2a1840da7 | ||
|
|
b9f20d1e80 | ||
|
|
0c77781be8 | ||
|
|
83245af1e2 | ||
|
|
7db806a461 | ||
|
|
92b15fb1e2 | ||
|
|
7b5182111c | ||
|
|
82b1b81999 | ||
|
|
f0a430f350 | ||
|
|
90f95b7270 | ||
|
|
15e6a93fac | ||
|
|
01541a2577 | ||
|
|
8c70bc0a17 | ||
|
|
9d96074604 | ||
|
|
114a4028f4 | ||
|
|
b342a4ba17 | ||
|
|
fcbf037619 | ||
|
|
a8e4ab433d | ||
|
|
6613ba02ab | ||
|
|
2af7fde83f | ||
|
|
19a0d4bc98 | ||
|
|
9ed49b1f27 | ||
|
|
d56df30a22 | ||
|
|
64908068d9 | ||
|
|
fe69d59aeb | ||
|
|
b65e737f84 | ||
|
|
bd274fdc3c | ||
|
|
ac19249c63 | ||
|
|
2abff1fec8 | ||
|
|
f1a6cfae0d | ||
|
|
e43e90ed3c | ||
|
|
0ee88fe0dc | ||
|
|
07e7f2e14d | ||
|
|
366ab95a2f | ||
|
|
ca723068a1 | ||
|
|
e424a105b3 | ||
|
|
32d2f92413 | ||
|
|
898e2ff010 | ||
|
|
dfa5e76870 | ||
|
|
c93dd12fae | ||
|
|
dbb1b1e582 | ||
|
|
76388ee160 | ||
|
|
5ec2eee69a | ||
|
|
31875a36fe | ||
|
|
c50598b78e | ||
|
|
2f0c81d9ad | ||
|
|
c22f89c6bb | ||
|
|
568a23cd35 | ||
|
|
eb7c4c131d | ||
|
|
f0664cd2c7 | ||
|
|
570eb7bc89 | ||
|
|
1ee91b4925 | ||
|
|
69fee37f00 | ||
|
|
49be66ae69 | ||
|
|
a0efe6895c | ||
|
|
8ef07e917d | ||
|
|
d3995b7bab | ||
|
|
c353e71ce7 | ||
|
|
a3570a1c9f | ||
|
|
c593c98e6d | ||
|
|
a4b5b532f2 | ||
|
|
6357f23aeb | ||
|
|
01d9b3bd0e | ||
|
|
6b428f7587 | ||
|
|
f829aa76d7 | ||
|
|
a72051e96f | ||
|
|
797622ba66 | ||
|
|
39342cd662 | ||
|
|
051a3ac122 | ||
|
|
f842a321ba | ||
|
|
3cd2dd65d3 | ||
|
|
5ce7e0b108 | ||
|
|
71c2058cc8 | ||
|
|
f200d39d23 | ||
|
|
7932845ac5 | ||
|
|
94bda6ac9e | ||
|
|
7a65f80406 | ||
|
|
36ab58dad9 | ||
|
|
e9be9e3761 | ||
|
|
b54645c86c | ||
|
|
ab77d8430c | ||
|
|
c6f683b532 | ||
|
|
a2604f5156 | ||
|
|
5ae7f683d6 | ||
|
|
f953c89979 | ||
|
|
bb8aab02ea | ||
|
|
af0c03ff6a | ||
|
|
8859900537 | ||
|
|
130852ab85 | ||
|
|
65fa8f96b4 | ||
|
|
0a84e9e363 | ||
|
|
163c69454b | ||
|
|
49d3fde0f3 | ||
|
|
bb67e2254e | ||
|
|
6d2abc4e74 | ||
|
|
4875450053 | ||
|
|
19184ca8a0 | ||
|
|
654c3d324b | ||
|
|
c5b4811f16 | ||
|
|
7a9dc4fd59 | ||
|
|
e79096626a | ||
|
|
332d074d32 | ||
|
|
e511ecd76e | ||
|
|
bcfbd5eba9 | ||
|
|
9fa3db395b | ||
|
|
52a41ceb04 | ||
|
|
e65d67266d | ||
|
|
0d1045821c | ||
|
|
45d526dda2 | ||
|
|
e52f998e78 | ||
|
|
42ed3b9355 | ||
|
|
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 |
12
.eslintrc.js
Normal file
12
.eslintrc.js
Normal file
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
extends: ['standard', 'standard-jsx'],
|
||||
globals: {
|
||||
__DEV__: true,
|
||||
},
|
||||
parser: 'babel-eslint',
|
||||
rules: {
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
'no-var': 'error',
|
||||
'prefer-const': 'error',
|
||||
},
|
||||
}
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,9 +1,9 @@
|
||||
/.nyc_output/
|
||||
/bower_components/
|
||||
/dist/
|
||||
/node_modules/
|
||||
/src/common/intl/locales/index.js
|
||||
/src/common/themes/index.js
|
||||
|
||||
npm-debug.log
|
||||
npm-debug.log.*
|
||||
|
||||
!node_modules/*
|
||||
node_modules/*/
|
||||
pnpm-debug.log
|
||||
pnpm-debug.log.*
|
||||
|
||||
4
.prettierrc.js
Normal file
4
.prettierrc.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 'stable'
|
||||
#- '4' # Disabled for now because npm 2 cannot properly handled broken peer dependencies.
|
||||
- '6'
|
||||
#- '4' # npm 3's flat tree is needed because some packages do not
|
||||
# declare their deps correctly (e.g. chartist-plugin-tooltip)
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
cache: yarn
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
|
||||
533
CHANGELOG.md
533
CHANGELOG.md
@@ -1,5 +1,538 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.15.0** (2017-12-29)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* VDI resize online method removed in 7.3 [#2542](https://github.com/vatesfr/xo-web/issues/2542)
|
||||
* Smart replace VDI.pool_migrate removed from XenServer 7.3 Free [#2541](https://github.com/vatesfr/xo-web/issues/2541)
|
||||
* New memory constraints in XenServer 7.3 [#2540](https://github.com/vatesfr/xo-web/issues/2540)
|
||||
* Link to Settings/Logs for admins in error notifications [#2516](https://github.com/vatesfr/xo-web/issues/2516)
|
||||
* [Self Service] Do not use placehodlers to describe inputs [#2509](https://github.com/vatesfr/xo-web/issues/2509)
|
||||
* Obfuscate password in log in LDAP plugin test [#2506](https://github.com/vatesfr/xo-web/issues/2506)
|
||||
* Log rotation [#2492](https://github.com/vatesfr/xo-web/issues/2492)
|
||||
* Continuous Replication TAG [#2473](https://github.com/vatesfr/xo-web/issues/2473)
|
||||
* Graphs in VM list view [#2469](https://github.com/vatesfr/xo-web/issues/2469)
|
||||
* [Delta Backups] Do not include merge duration in transfer speed stat [#2426](https://github.com/vatesfr/xo-web/issues/2426)
|
||||
* Warning for disperse mode [#2537](https://github.com/vatesfr/xo-web/issues/2537)
|
||||
|
||||
### Bugs
|
||||
|
||||
* VM console doesn't work when using IPv6 in URL [#2530](https://github.com/vatesfr/xo-web/issues/2530)
|
||||
* Retention issue with failed basic backup [#2524](https://github.com/vatesfr/xo-web/issues/2524)
|
||||
* [VM/Advanced] Check that the autopower on setting is working [#2489](https://github.com/vatesfr/xo-web/issues/2489)
|
||||
* Cloud config drive create fail on XenServer < 7 [#2478](https://github.com/vatesfr/xo-web/issues/2478)
|
||||
* VM create fails due to missing vGPU id [#2466](https://github.com/vatesfr/xo-web/issues/2466)
|
||||
|
||||
|
||||
## **5.14.0** (2017-10-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* VM snapshot description display [#2458](https://github.com/vatesfr/xo-web/issues/2458)
|
||||
* [Home] Ability to sort VM by number of snapshots [#2450](https://github.com/vatesfr/xo-web/issues/2450)
|
||||
* Display XS version in host view [#2439](https://github.com/vatesfr/xo-web/issues/2439)
|
||||
* [File restore]: Clarify the possibility to select multiple files [#2438](https://github.com/vatesfr/xo-web/issues/2438)
|
||||
* [Continuous Replication] Time in replicated VMs [#2431](https://github.com/vatesfr/xo-web/issues/2431)
|
||||
* [SortedTable] Active page in URL param [#2405](https://github.com/vatesfr/xo-web/issues/2405)
|
||||
* replace all '...' with the UTF-8 equivalent [#2391](https://github.com/vatesfr/xo-web/issues/2391)
|
||||
* [SortedTable] Explicit when no items [#2388](https://github.com/vatesfr/xo-web/issues/2388)
|
||||
* Handle patching licenses [#2382](https://github.com/vatesfr/xo-web/issues/2382)
|
||||
* Credential leaking in logs for messages regarding invalid credentials and "too fast authentication" [#2363](https://github.com/vatesfr/xo-web/issues/2363)
|
||||
* [SortedTable] Keyboard support [#2330](https://github.com/vatesfr/xo-web/issues/2330)
|
||||
* token.create should accept an expiration [#1769](https://github.com/vatesfr/xo-web/issues/1769)
|
||||
* On updater error, display link to documentation [#1610](https://github.com/vatesfr/xo-web/issues/1610)
|
||||
* Add basic vGPU support [#2413](https://github.com/vatesfr/xo-web/issues/2413)
|
||||
* Storage View - Disk Tab - real disk usage [#2475](https://github.com/vatesfr/xo-web/issues/2475)
|
||||
|
||||
### Bugs
|
||||
|
||||
* Config drive - Custom config not working properly [#2449](https://github.com/vatesfr/xo-web/issues/2449)
|
||||
* Snapshot sorted table breaks copyVm [#2446](https://github.com/vatesfr/xo-web/issues/2446)
|
||||
* [vm/snapshots] Incorrect default sort order [#2442](https://github.com/vatesfr/xo-web/issues/2442)
|
||||
* [Backups/Jobs] Incorrect months mapping [#2427](https://github.com/vatesfr/xo-web/issues/2427)
|
||||
* [Xapi#barrier()] Not compatible with XenServer < 6.1 [#2418](https://github.com/vatesfr/xo-web/issues/2418)
|
||||
* [SortedTable] Change page when no more items on the page [#2401](https://github.com/vatesfr/xo-web/issues/2401)
|
||||
* Review and fix creating a VM from a snapshot [#2343](https://github.com/vatesfr/xo-web/issues/2343)
|
||||
* Unable to edit / save restored backup job [#1922](https://github.com/vatesfr/xo-web/issues/1922)
|
||||
|
||||
|
||||
## **5.13.0** (2017-09-29)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* replace all '...' with the UTF-8 equivalent [#2391](https://github.com/vatesfr/xo-web/issues/2391)
|
||||
* [SortedTable] Explicit when no items [#2388](https://github.com/vatesfr/xo-web/issues/2388)
|
||||
* Auto select iqn or lun if there is only one [#2379](https://github.com/vatesfr/xo-web/issues/2379)
|
||||
* [Sparklines] Hide points [#2370](https://github.com/vatesfr/xo-web/issues/2370)
|
||||
* Allow xo-server-recover-account to generate a random password [#2360](https://github.com/vatesfr/xo-web/issues/2360)
|
||||
* Add disk in existing VM as self user [#2348](https://github.com/vatesfr/xo-web/issues/2348)
|
||||
* Sorted table for Settings/server [#2340](https://github.com/vatesfr/xo-web/issues/2340)
|
||||
* Sign in should be case insensitive [#2337](https://github.com/vatesfr/xo-web/issues/2337)
|
||||
* [SortedTable] Extend checkbox click to whole column [#2329](https://github.com/vatesfr/xo-web/issues/2329)
|
||||
* [SortedTable] Ability to select all items (across pages) [#2324](https://github.com/vatesfr/xo-web/issues/2324)
|
||||
* [SortedTable] Range selection [#2323](https://github.com/vatesfr/xo-web/issues/2323)
|
||||
* Warning on SMB remote creation [#2316](https://github.com/vatesfr/xo-web/issues/2316)
|
||||
* [Home | SortedTable] Add link to syntax doc in the filter input [#2305](https://github.com/vatesfr/xo-web/issues/2305)
|
||||
* [SortedTable] Add optional binding of filter to an URL query [#2301](https://github.com/vatesfr/xo-web/issues/2301)
|
||||
* [Home][Keyboard navigation] Allow selecting the objects [#2214](https://github.com/vatesfr/xo-web/issues/2214)
|
||||
* SR view / Disks: option to display non managed VDIs [#1724](https://github.com/vatesfr/xo-web/issues/1724)
|
||||
* Continuous Replication Retention [#1692](https://github.com/vatesfr/xo-web/issues/1692)
|
||||
|
||||
### Bugs
|
||||
|
||||
* iSCSI issue on LUN selector [#2374](https://github.com/vatesfr/xo-web/issues/2374)
|
||||
* Errors in VM copy are not properly reported [#2347](https://github.com/vatesfr/xo-web/issues/2347)
|
||||
* Removing a PIF IP fails [#2346](https://github.com/vatesfr/xo-web/issues/2346)
|
||||
* Review and fix creating a VM from a snapshot [#2343](https://github.com/vatesfr/xo-web/issues/2343)
|
||||
* iSCSI LUN Detection fails with authentification [#2339](https://github.com/vatesfr/xo-web/issues/2339)
|
||||
* Fix PoolActionBar to add a new SR [#2307](https://github.com/vatesfr/xo-web/issues/2307)
|
||||
* [VM migration] Error if default SR not accessible to target host [#2180](https://github.com/vatesfr/xo-web/issues/2180)
|
||||
* A job shouldn't executable more than once at the same time [#2053](https://github.com/vatesfr/xo-web/issues/2053)
|
||||
|
||||
## **5.12.0** (2017-08-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* PIF selector with physical status [#2326](https://github.com/vatesfr/xo-web/issues/2326)
|
||||
* [SortedTable] Range selection [#2323](https://github.com/vatesfr/xo-web/issues/2323)
|
||||
* Self service filter for home/VM view [#2303](https://github.com/vatesfr/xo-web/issues/2303)
|
||||
* SR/Disks Display total of VDIs to coalesce [#2300](https://github.com/vatesfr/xo-web/issues/2300)
|
||||
* Pool filter in the task view [#2293](https://github.com/vatesfr/xo-web/issues/2293)
|
||||
* "Loading" while fetching objects [#2285](https://github.com/vatesfr/xo-web/issues/2285)
|
||||
* [SortedTable] Add grouped actions feature [#2276](https://github.com/vatesfr/xo-web/issues/2276)
|
||||
* Add a filter to the backups' log [#2246](https://github.com/vatesfr/xo-web/issues/2246)
|
||||
* It should not be possible to migrate a halted VM. [#2233](https://github.com/vatesfr/xo-web/issues/2233)
|
||||
* [Home][Keyboard navigation] Allow selecting the objects [#2214](https://github.com/vatesfr/xo-web/issues/2214)
|
||||
* Allow to set pool master [#2213](https://github.com/vatesfr/xo-web/issues/2213)
|
||||
* Continuous Replication Retention [#1692](https://github.com/vatesfr/xo-web/issues/1692)
|
||||
|
||||
### Bugs
|
||||
|
||||
* Home pagination bug [#2310](https://github.com/vatesfr/xo-web/issues/2310)
|
||||
* Fix PoolActionBar to add a new SR [#2307](https://github.com/vatesfr/xo-web/issues/2307)
|
||||
* VM snapshots are not correctly deleted [#2304](https://github.com/vatesfr/xo-web/issues/2304)
|
||||
* Parallel deletion of VMs fails [#2297](https://github.com/vatesfr/xo-web/issues/2297)
|
||||
* Continous replication create multiple zombie disks [#2292](https://github.com/vatesfr/xo-web/issues/2292)
|
||||
* Add user to Group issue [#2196](https://github.com/vatesfr/xo-web/issues/2196)
|
||||
* [VM migration] Error if default SR not accessible to target host [#2180](https://github.com/vatesfr/xo-web/issues/2180)
|
||||
|
||||
## **5.11.0** (2017-07-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Storage VHD chain health [\#2178](https://github.com/vatesfr/xo-web/issues/2178)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- No web VNC console [\#2258](https://github.com/vatesfr/xo-web/issues/2258)
|
||||
- Patching issues [\#2254](https://github.com/vatesfr/xo-web/issues/2254)
|
||||
- Advanced button in VM creation for self service user [\#2202](https://github.com/vatesfr/xo-web/issues/2202)
|
||||
- Hide "new VM" menu entry if not admin or not self service user [\#2191](https://github.com/vatesfr/xo-web/issues/2191)
|
||||
|
||||
## **5.10.0** (2017-06-30)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Improve backup log display [\#2239](https://github.com/vatesfr/xo-web/issues/2239)
|
||||
- Patch SR detection improvement [\#2215](https://github.com/vatesfr/xo-web/issues/2215)
|
||||
- Less strict coalesce detection [\#2207](https://github.com/vatesfr/xo-web/issues/2207)
|
||||
- IP pool UI improvement [\#2203](https://github.com/vatesfr/xo-web/issues/2203)
|
||||
- Ability to clear "Auto power on" flag for DR-ed VM [\#2097](https://github.com/vatesfr/xo-web/issues/2097)
|
||||
- [Delta backup restoration] Choose SR for each VDIs [\#2070](https://github.com/vatesfr/xo-web/issues/2070)
|
||||
- Ability to forget an host (even if no longer present) [\#1934](https://github.com/vatesfr/xo-web/issues/1934)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Cross pool migrate fail [\#2248](https://github.com/vatesfr/xo-web/issues/2248)
|
||||
- ActionButtons with modals stay in pending state forever [\#2222](https://github.com/vatesfr/xo-web/issues/2222)
|
||||
- Permission issue for a user on self service VMs [\#2212](https://github.com/vatesfr/xo-web/issues/2212)
|
||||
- Self-Service resource loophole [\#2198](https://github.com/vatesfr/xo-web/issues/2198)
|
||||
- Backup log no longer shows the name of destination VM [\#2195](https://github.com/vatesfr/xo-web/issues/2195)
|
||||
- State not restored when exiting modal dialog [\#2194](https://github.com/vatesfr/xo-web/issues/2194)
|
||||
- [Xapi#exportDeltaVm] Cannot read property 'managed' of undefined [\#2189](https://github.com/vatesfr/xo-web/issues/2189)
|
||||
- VNC keyboard layout change [\#404](https://github.com/vatesfr/xo-web/issues/404)
|
||||
|
||||
## **5.9.0** (2017-05-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Allow DR to remove previous backup first [\#2157](https://github.com/vatesfr/xo-web/issues/2157)
|
||||
- Feature request - add amount of RAM to memory bars [\#2149](https://github.com/vatesfr/xo-web/issues/2149)
|
||||
- Make the acceptability of invalid certificates configurable [\#2138](https://github.com/vatesfr/xo-web/issues/2138)
|
||||
- label of VM names in tasks link [\#2135](https://github.com/vatesfr/xo-web/issues/2135)
|
||||
- Backup report timezone [\#2133](https://github.com/vatesfr/xo-web/issues/2133)
|
||||
- xo-server-recover-account [\#2129](https://github.com/vatesfr/xo-web/issues/2129)
|
||||
- Detect disks attached to control domain [\#2126](https://github.com/vatesfr/xo-web/issues/2126)
|
||||
- Add task description in Tasks view [\#2125](https://github.com/vatesfr/xo-web/issues/2125)
|
||||
- Host reboot warning after patching for 7.1 [\#2124](https://github.com/vatesfr/xo-web/issues/2124)
|
||||
- Continuous Replication - possibility run VM without a clone [\#2119](https://github.com/vatesfr/xo-web/issues/2119)
|
||||
- Unreachable host should be detected [\#2099](https://github.com/vatesfr/xo-web/issues/2099)
|
||||
- Orange icon when host is is disabled [\#2098](https://github.com/vatesfr/xo-web/issues/2098)
|
||||
- Enhanced backup report logs [\#2096](https://github.com/vatesfr/xo-web/issues/2096)
|
||||
- Only show failures when configured to report on failures [\#2095](https://github.com/vatesfr/xo-web/issues/2095)
|
||||
- "Add all" button in self service [\#2081](https://github.com/vatesfr/xo-web/issues/2081)
|
||||
- Patch and pack mechanism changed on Ely [\#2058](https://github.com/vatesfr/xo-web/issues/2058)
|
||||
- Tip or ask people to patch from pool view [\#2057](https://github.com/vatesfr/xo-web/issues/2057)
|
||||
- File restore - Remind compatible backup [\#1930](https://github.com/vatesfr/xo-web/issues/1930)
|
||||
- Reporting for halted vm time [\#1613](https://github.com/vatesfr/xo-web/issues/1613)
|
||||
- Add standalone XS server to a pool and patch it to the pool level [\#878](https://github.com/vatesfr/xo-web/issues/878)
|
||||
- Add Cores-per-sockets [\#130](https://github.com/vatesfr/xo-web/issues/130)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- VM creation is broken for non-admins [\#2168](https://github.com/vatesfr/xo-web/issues/2168)
|
||||
- Can't create cloud config drive [\#2162](https://github.com/vatesfr/xo-web/issues/2162)
|
||||
- Select is "moving" [\#2142](https://github.com/vatesfr/xo-web/issues/2142)
|
||||
- Select issue for affinity host [\#2141](https://github.com/vatesfr/xo-web/issues/2141)
|
||||
- Dashboard Storage Usage incorrect [\#2123](https://github.com/vatesfr/xo-web/issues/2123)
|
||||
- Detect unmerged *base copy* and prevent too long chains [\#2047](https://github.com/vatesfr/xo-web/issues/2047)
|
||||
|
||||
|
||||
## **5.8.0** (2017-04-28)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Limit About view info for non-admins [\#2109](https://github.com/vatesfr/xo-web/issues/2109)
|
||||
- Enabling/disabling boot device on HVM VM [\#2105](https://github.com/vatesfr/xo-web/issues/2105)
|
||||
- Filter: Hide snapshots in SR disk view [\#2102](https://github.com/vatesfr/xo-web/issues/2102)
|
||||
- Smarter XOSAN install [\#2084](https://github.com/vatesfr/xo-web/issues/2084)
|
||||
- PL translation [\#2079](https://github.com/vatesfr/xo-web/issues/2079)
|
||||
- Remove the "share this VM" option if not in self service [\#2061](https://github.com/vatesfr/xo-web/issues/2061)
|
||||
- "connected" status graphics are not the same on the host storage and networking tabs [\#2060](https://github.com/vatesfr/xo-web/issues/2060)
|
||||
- Ability to view and edit `vga` and `videoram` fields in VM view [\#158](https://github.com/vatesfr/xo-web/issues/158)
|
||||
- Performances [\#1](https://github.com/vatesfr/xen-api/issues/1)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Dashboard display issues [\#2108](https://github.com/vatesfr/xo-web/issues/2108)
|
||||
- Dashboard CPUs Usage [\#2105](https://github.com/vatesfr/xo-web/issues/2105)
|
||||
- [Dashboard/Overview] Warning [\#2090](https://github.com/vatesfr/xo-web/issues/2090)
|
||||
- VM creation displays all networks [\#2086](https://github.com/vatesfr/xo-web/issues/2086)
|
||||
- Cannot change HA mode for a VM [\#2080](https://github.com/vatesfr/xo-web/issues/2080)
|
||||
- [Smart backup] Tags selection does not work [\#2077](https://github.com/vatesfr/xo-web/issues/2077)
|
||||
- [Backup jobs] Timeout should be in seconds, not milliseconds [\#2076](https://github.com/vatesfr/xo-web/issues/2076)
|
||||
- Missing VM templates [\#2075](https://github.com/vatesfr/xo-web/issues/2075)
|
||||
- [transport-email] From header not set [\#2074](https://github.com/vatesfr/xo-web/issues/2074)
|
||||
- Missing objects should be displayed in backup edition [\#2052](https://github.com/vatesfr/xo-web/issues/2052)
|
||||
|
||||
## **5.7.0** (2017-03-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Improve ActionButton error reporting [\#2048](https://github.com/vatesfr/xo-web/issues/2048)
|
||||
- Home view master checkbox UI issue [\#2027](https://github.com/vatesfr/xo-web/issues/2027)
|
||||
- HU Translation [\#2019](https://github.com/vatesfr/xo-web/issues/2019)
|
||||
- [Usage report] Add name for all objects [\#2017](https://github.com/vatesfr/xo-web/issues/2017)
|
||||
- [Home] Improve inter-types linkage [\#2012](https://github.com/vatesfr/xo-web/issues/2012)
|
||||
- Remove bootable checkboxes in VM creation [\#2007](https://github.com/vatesfr/xo-web/issues/2007)
|
||||
- Do not display bootable toggles for disks of non-PV VMs [\#1996](https://github.com/vatesfr/xo-web/issues/1996)
|
||||
- Try to match network VLAN for VM migration modal [\#1990](https://github.com/vatesfr/xo-web/issues/1990)
|
||||
- [Usage reports] Add VM names in addition to UUIDs [\#1984](https://github.com/vatesfr/xo-web/issues/1984)
|
||||
- Host affinity in "advanced" VM creation [\#1983](https://github.com/vatesfr/xo-web/issues/1983)
|
||||
- Add job tag in backup logs [\#1982](https://github.com/vatesfr/xo-web/issues/1982)
|
||||
- Possibility to add a label/description to servers [\#1965](https://github.com/vatesfr/xo-web/issues/1965)
|
||||
- Possibility to create shared VM in a resource set [\#1964](https://github.com/vatesfr/xo-web/issues/1964)
|
||||
- Clearer display of disabled (backup) jobs [\#1958](https://github.com/vatesfr/xo-web/issues/1958)
|
||||
- Job should have a configurable timeout [\#1956](https://github.com/vatesfr/xo-web/issues/1956)
|
||||
- Sort failed VMs in backup report [\#1950](https://github.com/vatesfr/xo-web/issues/1950)
|
||||
- Support for UNIX socket path [\#1944](https://github.com/vatesfr/xo-web/issues/1944)
|
||||
- Interface - Host Patching - Button Verbiage [\#1911](https://github.com/vatesfr/xo-web/issues/1911)
|
||||
- Display if a VM is in Self Service (and which group) [\#1905](https://github.com/vatesfr/xo-web/issues/1905)
|
||||
- Install supplemental pack on a whole pool [\#1896](https://github.com/vatesfr/xo-web/issues/1896)
|
||||
- Allow VM snapshots with ACLs [\#1865](https://github.com/vatesfr/xo-web/issues/1886)
|
||||
- Icon to indicate if a snapshot is quiesce [\#1858](https://github.com/vatesfr/xo-web/issues/1858)
|
||||
- Pool Ips input too permissive [\#1731](https://github.com/vatesfr/xo-web/issues/1731)
|
||||
- Select is going on top after each choice [\#1359](https://github.com/vatesfr/xo-web/issues/1359)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Missing objects should be displayed in backup edition [\#2052](https://github.com/vatesfr/xo-web/issues/2052)
|
||||
- Search bar content changes while typing [\#2035](https://github.com/vatesfr/xo-web/issues/2035)
|
||||
- VM.$guest_metrics.PV_drivers_up_to_date is deprecated in XS 7.1 [\#2024](https://github.com/vatesfr/xo-web/issues/2024)
|
||||
- Bootable flag selection checkbox for extra disk not fetched [\#1994](https://github.com/vatesfr/xo-web/issues/1994)
|
||||
- Home view − Changing type must reset paging [\#1993](https://github.com/vatesfr/xo-web/issues/1993)
|
||||
- XOSAN menu item should only be displayed to admins [\#1968](https://github.com/vatesfr/xo-web/issues/1968)
|
||||
- Object type change are not correctly handled in UI [\#1967](https://github.com/vatesfr/xo-web/issues/1967)
|
||||
- VM creation is stuck when using ISO/DVD as install method [\#1966](https://github.com/vatesfr/xo-web/issues/1966)
|
||||
- Install pack on whole pool fails [\#1957](https://github.com/vatesfr/xo-web/issues/1957)
|
||||
- Consoles are broken in next-release [\#1954](https://github.com/vatesfr/xo-web/issues/1954)
|
||||
- [VHD merge] Increase BAT when necessary [\#1939](https://github.com/vatesfr/xo-web/issues/1939)
|
||||
- Issue on VM restore time [\#1936](https://github.com/vatesfr/xo-web/issues/1936)
|
||||
- Two remotes should not be able to have the same name [\#1879](https://github.com/vatesfr/xo-web/issues/1879)
|
||||
- Selfservice limits not honored after VM creation [\#1695](https://github.com/vatesfr/xo-web/issues/1695)
|
||||
|
||||
## **5.6.0** (2017-01-27)
|
||||
|
||||
Reporting, LVM File level restore.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Do not stop patches install if already applied [\#1904](https://github.com/vatesfr/xo-web/issues/1904)
|
||||
- Improve scheduling UI [\#1893](https://github.com/vatesfr/xo-web/issues/1893)
|
||||
- Smart backup and tag [\#1885](https://github.com/vatesfr/xo-web/issues/1885)
|
||||
- Missing embeded API documention [\#1882](https://github.com/vatesfr/xo-web/issues/1882)
|
||||
- Add local DVD in CD selector [\#1880](https://github.com/vatesfr/xo-web/issues/1880)
|
||||
- File level restore for LVM [\#1878](https://github.com/vatesfr/xo-web/issues/1878)
|
||||
- Restore multiple files from file level restore [\#1877](https://github.com/vatesfr/xo-web/issues/1877)
|
||||
- Add a VM tab for host & pool views [\#1864](https://github.com/vatesfr/xo-web/issues/1864)
|
||||
- Icon to indicate if a snapshot is quiesce [\#1858](https://github.com/vatesfr/xo-web/issues/1858)
|
||||
- UI for disconnect hosts comp [\#1833](https://github.com/vatesfr/xo-web/issues/1833)
|
||||
- Eject all xs-guest.iso in a pool [\#1798](https://github.com/vatesfr/xo-web/issues/1798)
|
||||
- Display installed supplemental pack on host [\#1506](https://github.com/vatesfr/xo-web/issues/1506)
|
||||
- Install supplemental pack on host comp [\#1460](https://github.com/vatesfr/xo-web/issues/1460)
|
||||
- Pool-wide combined stats [\#1324](https://github.com/vatesfr/xo-web/issues/1324)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- IP-address not released when VM removed [\#1906](https://github.com/vatesfr/xo-web/issues/1906)
|
||||
- Interface broken due to new Bootstrap Alpha [\#1871](https://github.com/vatesfr/xo-web/issues/1871)
|
||||
- Self service recompute all limits broken [\#1866](https://github.com/vatesfr/xo-web/issues/1866)
|
||||
- Patch not found error for XS 6.5 [\#1863](https://github.com/vatesfr/xo-web/issues/1863)
|
||||
- Convert To Template issues [\#1855](https://github.com/vatesfr/xo-web/issues/1855)
|
||||
- Removing PIF seems to fail [\#1853](https://github.com/vatesfr/xo-web/issues/1853)
|
||||
- Depth should be >= 1 in backup creation [\#1851](https://github.com/vatesfr/xo-web/issues/1851)
|
||||
- Wrong link in Dashboard > Health [\#1850](https://github.com/vatesfr/xo-web/issues/1850)
|
||||
- Incorrect file dates shown in new File Restore feature [\#1840](https://github.com/vatesfr/xo-web/issues/1840)
|
||||
- IP allocation problem [\#1747](https://github.com/vatesfr/xo-web/issues/1747)
|
||||
- Selfservice limits not honored after VM creation [\#1695](https://github.com/vatesfr/xo-web/issues/1695)
|
||||
|
||||
## **5.5.0** (2016-12-20)
|
||||
|
||||
File level restore.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Better auto select network when migrate VM [\#1788](https://github.com/vatesfr/xo-web/issues/1788)
|
||||
- Plugin for passive backup job reporting in Nagios [\#1664](https://github.com/vatesfr/xo-web/issues/1664)
|
||||
- File level restore for delta backup [\#1590](https://github.com/vatesfr/xo-web/issues/1590)
|
||||
- Better select filters for ACLs [\#1515](https://github.com/vatesfr/xo-web/issues/1515)
|
||||
- All pools and "negative" filters [\#1503](https://github.com/vatesfr/xo-web/issues/1503)
|
||||
- VM copy with disk selection [\#826](https://github.com/vatesfr/xo-web/issues/826)
|
||||
- Disable metadata exports [\#1818](https://github.com/vatesfr/xo-web/issues/1818)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Tool small selector [\#1832](https://github.com/vatesfr/xo-web/issues/1832)
|
||||
- Replication does not work from a VM created by a CR or delta backup [\#1811](https://github.com/vatesfr/xo-web/issues/1811)
|
||||
- Can't add a SSH key in VM creation [\#1805](https://github.com/vatesfr/xo-web/issues/1805)
|
||||
- Issue when no default SR in a pool [\#1804](https://github.com/vatesfr/xo-web/issues/1804)
|
||||
- XOA doesn't refresh after an update anymore [\#1801](https://github.com/vatesfr/xo-web/issues/1801)
|
||||
- Shortcuts not inhibited on inputs on Safari [\#1691](https://github.com/vatesfr/xo-web/issues/1691)
|
||||
|
||||
## **5.4.0** (2016-11-23)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- XML display in alerts [\#1776](https://github.com/vatesfr/xo-web/issues/1776)
|
||||
- Remove some view for non admin users [\#1773](https://github.com/vatesfr/xo-web/issues/1773)
|
||||
- Complex matcher should support matching boolean values [\#1768](https://github.com/vatesfr/xo-web/issues/1768)
|
||||
- Home SR view [\#1764](https://github.com/vatesfr/xo-web/issues/1764)
|
||||
- Filter on tag click [\#1763](https://github.com/vatesfr/xo-web/issues/1763)
|
||||
- Testable plugins [\#1749](https://github.com/vatesfr/xo-web/issues/1749)
|
||||
- Backup/Restore Design fix. [\#1734](https://github.com/vatesfr/xo-web/issues/1734)
|
||||
- Display the owner of a \(backup\) job [\#1733](https://github.com/vatesfr/xo-web/issues/1733)
|
||||
- Use paginated table for backup jobs [\#1726](https://github.com/vatesfr/xo-web/issues/1726)
|
||||
- SR view / Disks: should display snapshot VDIs [\#1723](https://github.com/vatesfr/xo-web/issues/1723)
|
||||
- Restored VM should have an identifiable name [\#1719](https://github.com/vatesfr/xo-web/issues/1719)
|
||||
- If host reboot action returns NO\_HOSTS\_AVAILABLE, ask to force [\#1717](https://github.com/vatesfr/xo-web/issues/1717)
|
||||
- Hide xo-server timezone in backups [\#1706](https://github.com/vatesfr/xo-web/issues/1706)
|
||||
- Enable hyperlink for Hostname for Issues [\#1700](https://github.com/vatesfr/xo-web/issues/1700)
|
||||
- Pool/network - Modify column [\#1696](https://github.com/vatesfr/xo-web/issues/1696)
|
||||
- UI - Plugins - Display a message if no plugins [\#1670](https://github.com/vatesfr/xo-web/issues/1670)
|
||||
- Display warning/error for delta backup on XS older than 6.5 [\#1647](https://github.com/vatesfr/xo-web/issues/1647)
|
||||
- XO without internet access doesn't work [\#1629](https://github.com/vatesfr/xo-web/issues/1629)
|
||||
- Improve backup restore view [\#1609](https://github.com/vatesfr/xo-web/issues/1609)
|
||||
- UI Enhancement - Acronym for dummy [\#1604](https://github.com/vatesfr/xo-web/issues/1604)
|
||||
- Slack XO plugin for backup report [\#1593](https://github.com/vatesfr/xo-web/issues/1593)
|
||||
- Expose XAPI exceptions in the UI [\#1481](https://github.com/vatesfr/xo-web/issues/1481)
|
||||
- Running VMs in the host overview, all VMs in the pool overview [\#1432](https://github.com/vatesfr/xo-web/issues/1432)
|
||||
- Move location of NFS mount point [\#1405](https://github.com/vatesfr/xo-web/issues/1405)
|
||||
- Home: Pool list - additionnal informations for pool [\#1226](https://github.com/vatesfr/xo-web/issues/1226)
|
||||
- Modify VLAN of an existing network [\#1092](https://github.com/vatesfr/xo-web/issues/1092)
|
||||
- Wrong instructions for CLI upgrade [\#787](https://github.com/vatesfr/xo-web/issues/787)
|
||||
- Ability to export/import XO config [\#786](https://github.com/vatesfr/xo-web/issues/786)
|
||||
- Test button for transport-email plugin [\#697](https://github.com/vatesfr/xo-web/issues/697)
|
||||
- Merge `scheduler` API into `schedule` [\#664](https://github.com/vatesfr/xo-web/issues/664)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Should jobs be accessible to non admins? [\#1759](https://github.com/vatesfr/xo-web/issues/1759)
|
||||
- Schedules deletion is not working [\#1737](https://github.com/vatesfr/xo-web/issues/1737)
|
||||
- Editing a job from the jobs overview page does not work [\#1736](https://github.com/vatesfr/xo-web/issues/1736)
|
||||
- Editing a schedule from jobs overview does not work [\#1728](https://github.com/vatesfr/xo-web/issues/1728)
|
||||
- ACLs not correctly imported [\#1722](https://github.com/vatesfr/xo-web/issues/1722)
|
||||
- Some Bootstrap style broken [\#1721](https://github.com/vatesfr/xo-web/issues/1721)
|
||||
- Not properly sign out on auth token expiration [\#1711](https://github.com/vatesfr/xo-web/issues/1711)
|
||||
- Hosts/<UUID>/network status is incorrect [\#1702](https://github.com/vatesfr/xo-web/issues/1702)
|
||||
- Patches application fails "Found : Moved Temporarily" [\#1701](https://github.com/vatesfr/xo-web/issues/1701)
|
||||
- Password generation for user creation is not working [\#1678](https://github.com/vatesfr/xo-web/issues/1678)
|
||||
- \#/dashboard/health Remove All Orphaned VDIs [\#1622](https://github.com/vatesfr/xo-web/issues/1622)
|
||||
- Create a new SR - CIFS/SAMBA Broken [\#1615](https://github.com/vatesfr/xo-web/issues/1615)
|
||||
- xo-cli --list-objects: truncated output ? 64k buffer limitation ? [\#1356](https://github.com/vatesfr/xo-web/issues/1356)
|
||||
|
||||
## **5.3.0** (2016-10-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Missing favicon [\#1660](https://github.com/vatesfr/xo-web/issues/1660)
|
||||
- ipPools quota [\#1565](https://github.com/vatesfr/xo-web/issues/1565)
|
||||
- Dashboard - orphaned VDI [\#1654](https://github.com/vatesfr/xo-web/issues/1654)
|
||||
- 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)
|
||||
- Display memory bar in home/host view [\#1616](https://github.com/vatesfr/xo-web/issues/1616)
|
||||
- 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)
|
||||
- `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)
|
||||
- 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
|
||||
|
||||
- Display issues on plugin array edition [\#1663](https://github.com/vatesfr/xo-web/issues/1663)
|
||||
- Import of delta backups fails [\#1656](https://github.com/vatesfr/xo-web/issues/1656)
|
||||
- Host - Missing IP config for PIF [\#1651](https://github.com/vatesfr/xo-web/issues/1651)
|
||||
- Remote copy is always activating compression [\#1645](https://github.com/vatesfr/xo-web/issues/1645)
|
||||
- LB plugin UI problems [\#1630](https://github.com/vatesfr/xo-web/issues/1630)
|
||||
- Keyboard shortcuts should not work when a modal is open [\#1589](https://github.com/vatesfr/xo-web/issues/1589)
|
||||
- UI small bug in drop-down lists [\#1411](https://github.com/vatesfr/xo-web/issues/1411)
|
||||
- md5 delta backup error [\#1672](https://github.com/vatesfr/xo-web/issues/1672)
|
||||
- Can't edit VIF network [\#1640](https://github.com/vatesfr/xo-web/issues/1640)
|
||||
- Do not expose shortcuts while console is focused [\#1614](https://github.com/vatesfr/xo-web/issues/1614)
|
||||
- All users can see VM templates [\#1621](https://github.com/vatesfr/xo-web/issues/1621)
|
||||
- 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)
|
||||
- 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)
|
||||
- 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 VBD 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Xen Orchestra Web [](https://travis-ci.org/vatesfr/xo-web)
|
||||
# Xen Orchestra Web [](https://go.crisp.im/chat/embed/?website_id=-JzqzzwddSV7bKGtEyAQ) [](https://travis-ci.org/vatesfr/xo-web)
|
||||
|
||||

|
||||
|
||||
@@ -10,7 +10,7 @@ ___
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
181
gulpfile.js
181
gulpfile.js
@@ -2,28 +2,17 @@
|
||||
|
||||
// ===================================================================
|
||||
|
||||
var SRC_DIR = __dirname + '/src' // eslint-disable-line no-path-concat
|
||||
var DIST_DIR = __dirname + '/dist' // eslint-disable-line no-path-concat
|
||||
const SRC_DIR = __dirname + '/src' // eslint-disable-line no-path-concat
|
||||
const DIST_DIR = __dirname + '/dist' // eslint-disable-line no-path-concat
|
||||
|
||||
// Port to use for the livereload server.
|
||||
//
|
||||
// It must be available and if possible unique to not conflict with other projects.
|
||||
// http://www.random.org/integers/?num=1&min=1024&max=65535&col=1&base=10&format=plain&rnd=new
|
||||
var LIVERELOAD_PORT = 26242
|
||||
const 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.env.NODE_ENV === 'production'
|
||||
var DEVELOPMENT = !PRODUCTION
|
||||
const PRODUCTION = process.env.NODE_ENV === 'production'
|
||||
const DEVELOPMENT = !PRODUCTION
|
||||
|
||||
if (!process.env.XOA_PLAN) {
|
||||
process.env.XOA_PLAN = '5' // Open Source
|
||||
@@ -31,12 +20,12 @@ if (!process.env.XOA_PLAN) {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
var gulp = require('gulp')
|
||||
const gulp = require('gulp')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function lazyFn (factory) {
|
||||
var fn = function () {
|
||||
let fn = function () {
|
||||
fn = factory()
|
||||
return fn.apply(this, arguments)
|
||||
}
|
||||
@@ -48,19 +37,19 @@ function lazyFn (factory) {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var livereload = lazyFn(function () {
|
||||
var livereload = require('gulp-refresh')
|
||||
const livereload = lazyFn(function () {
|
||||
const livereload = require('gulp-refresh')
|
||||
livereload.listen({
|
||||
port: LIVERELOAD_PORT
|
||||
port: LIVERELOAD_PORT,
|
||||
})
|
||||
|
||||
return livereload
|
||||
})
|
||||
|
||||
var pipe = lazyFn(function () {
|
||||
var current
|
||||
const pipe = lazyFn(function () {
|
||||
let current
|
||||
function pipeCore (streams) {
|
||||
var i, n, stream
|
||||
let i, n, stream
|
||||
for (i = 0, n = streams.length; i < n; ++i) {
|
||||
stream = streams[i]
|
||||
if (!stream) {
|
||||
@@ -68,14 +57,12 @@ var pipe = lazyFn(function () {
|
||||
} else if (stream instanceof Array) {
|
||||
pipeCore(stream)
|
||||
} else {
|
||||
current = current
|
||||
? current.pipe(stream)
|
||||
: stream
|
||||
current = current ? current.pipe(stream) : stream
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var push = Array.prototype.push
|
||||
const push = Array.prototype.push
|
||||
return function (streams) {
|
||||
try {
|
||||
if (!(streams instanceof Array)) {
|
||||
@@ -92,7 +79,7 @@ var pipe = lazyFn(function () {
|
||||
}
|
||||
})
|
||||
|
||||
var resolvePath = lazyFn(function () {
|
||||
const resolvePath = lazyFn(function () {
|
||||
return require('path').resolve
|
||||
})
|
||||
|
||||
@@ -100,37 +87,35 @@ var resolvePath = lazyFn(function () {
|
||||
|
||||
// Similar to `gulp.src()` but the pattern is relative to `SRC_DIR`
|
||||
// and files are automatically watched when not in production mode.
|
||||
var src = lazyFn(function () {
|
||||
const src = lazyFn(function () {
|
||||
function resolve (path) {
|
||||
return path
|
||||
? resolvePath(SRC_DIR, path)
|
||||
: SRC_DIR
|
||||
return path ? resolvePath(SRC_DIR, path) : SRC_DIR
|
||||
}
|
||||
|
||||
return PRODUCTION
|
||||
? function src (pattern, opts) {
|
||||
var base = resolve(opts && opts.base)
|
||||
const base = resolve(opts && opts.base)
|
||||
|
||||
return gulp.src(pattern, {
|
||||
base: base,
|
||||
cwd: base,
|
||||
passthrough: opts && opts.passthrough,
|
||||
sourcemaps: opts && opts.sourcemaps
|
||||
sourcemaps: opts && opts.sourcemaps,
|
||||
})
|
||||
}
|
||||
: function src (pattern, opts) {
|
||||
var base = resolve(opts && opts.base)
|
||||
const base = resolve(opts && opts.base)
|
||||
|
||||
return pipe(
|
||||
gulp.src(pattern, {
|
||||
base: base,
|
||||
cwd: base,
|
||||
passthrough: opts && opts.passthrough,
|
||||
sourcemaps: opts && opts.sourcemaps
|
||||
sourcemaps: opts && opts.sourcemaps,
|
||||
}),
|
||||
require('gulp-watch')(pattern, {
|
||||
base: base,
|
||||
cwd: base
|
||||
cwd: base,
|
||||
}),
|
||||
require('gulp-plumber')()
|
||||
)
|
||||
@@ -140,17 +125,13 @@ var src = lazyFn(function () {
|
||||
// Similar to `gulp.dest()` but the output directory is relative to
|
||||
// `DIST_DIR` and default to `./`, and files are automatically live-
|
||||
// reloaded when not in production mode.
|
||||
var dest = lazyFn(function () {
|
||||
const dest = lazyFn(function () {
|
||||
function resolve (path) {
|
||||
return path
|
||||
? resolvePath(DIST_DIR, path)
|
||||
: DIST_DIR
|
||||
return path ? resolvePath(DIST_DIR, path) : DIST_DIR
|
||||
}
|
||||
|
||||
var opts = {
|
||||
sourcemaps: {
|
||||
path: '.'
|
||||
}
|
||||
const opts = {
|
||||
sourcemaps: '.',
|
||||
}
|
||||
|
||||
return PRODUCTION
|
||||
@@ -158,7 +139,7 @@ var dest = lazyFn(function () {
|
||||
return gulp.dest(resolve(path), opts)
|
||||
}
|
||||
: function dest (path) {
|
||||
var stream = gulp.dest(resolve(path), opts)
|
||||
const stream = gulp.dest(resolve(path), opts)
|
||||
stream.pipe(livereload())
|
||||
return stream
|
||||
}
|
||||
@@ -171,9 +152,9 @@ function browserify (path, opts) {
|
||||
opts = {}
|
||||
}
|
||||
|
||||
var bundler = require('browserify')(path, {
|
||||
let bundler = require('browserify')(path, {
|
||||
basedir: SRC_DIR,
|
||||
debug: DEVELOPMENT, // TODO: enable also in production but need to make it work with gulp-uglify.
|
||||
debug: true,
|
||||
extensions: opts.extensions,
|
||||
fullPaths: false,
|
||||
paths: SRC_DIR + '/common',
|
||||
@@ -181,12 +162,12 @@ function browserify (path, opts) {
|
||||
|
||||
// Required by Watchify.
|
||||
cache: {},
|
||||
packageCache: {}
|
||||
packageCache: {},
|
||||
})
|
||||
|
||||
var plugins = opts.plugins
|
||||
for (var i = 0, n = plugins && plugins.length; i < n; ++i) {
|
||||
var plugin = plugins[i]
|
||||
const plugins = opts.plugins
|
||||
for (let i = 0, n = plugins && plugins.length; i < n; ++i) {
|
||||
const plugin = plugins[i]
|
||||
bundler.plugin(require(plugin[0]), plugin[1])
|
||||
}
|
||||
|
||||
@@ -194,7 +175,11 @@ function browserify (path, opts) {
|
||||
// FIXME: does not work with react-intl (?!)
|
||||
// bundler.plugin('bundle-collapser/plugin')
|
||||
} else {
|
||||
bundler = require('watchify')(bundler)
|
||||
bundler = require('watchify')(bundler, {
|
||||
// do not watch in `node_modules`
|
||||
// https://github.com/browserify/watchify#options
|
||||
ignoreWatch: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Append the extension if necessary.
|
||||
@@ -203,11 +188,11 @@ function browserify (path, opts) {
|
||||
}
|
||||
path = resolvePath(SRC_DIR, path)
|
||||
|
||||
var stream = new (require('readable-stream'))({
|
||||
objectMode: true
|
||||
let stream = new (require('readable-stream'))({
|
||||
objectMode: true,
|
||||
})
|
||||
|
||||
var write
|
||||
let write
|
||||
function bundle () {
|
||||
bundler.bundle(function onBundle (error, buffer) {
|
||||
if (error) {
|
||||
@@ -215,11 +200,13 @@ function browserify (path, opts) {
|
||||
return
|
||||
}
|
||||
|
||||
write(new (require('vinyl'))({
|
||||
base: SRC_DIR,
|
||||
contents: buffer,
|
||||
path: path
|
||||
}))
|
||||
write(
|
||||
new (require('vinyl'))({
|
||||
base: SRC_DIR,
|
||||
contents: buffer,
|
||||
path: path,
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -249,11 +236,12 @@ function browserify (path, opts) {
|
||||
|
||||
gulp.task(function buildPages () {
|
||||
return pipe(
|
||||
src('index.jade', { sourcemaps: true }),
|
||||
require('gulp-jade')(),
|
||||
DEVELOPMENT && require('gulp-embedlr')({
|
||||
port: LIVERELOAD_PORT
|
||||
}),
|
||||
src('index.pug'),
|
||||
require('gulp-pug')(),
|
||||
DEVELOPMENT &&
|
||||
require('gulp-embedlr')({
|
||||
port: LIVERELOAD_PORT,
|
||||
}),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
@@ -263,12 +251,17 @@ gulp.task(function buildScripts () {
|
||||
browserify('./index.js', {
|
||||
plugins: [
|
||||
// ['css-modulesify', {
|
||||
['modular-css/browserify', {
|
||||
css: DIST_DIR + '/modules.css'
|
||||
}]
|
||||
]
|
||||
[
|
||||
'modular-cssify',
|
||||
{
|
||||
css: DIST_DIR + '/modules.css',
|
||||
from: undefined,
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
PRODUCTION && require('gulp-uglify')(),
|
||||
require('gulp-sourcemaps').init({ loadMaps: true }),
|
||||
PRODUCTION && require('gulp-uglify/composer')(require('uglify-es'))(),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
@@ -277,10 +270,7 @@ gulp.task(function buildStyles () {
|
||||
return pipe(
|
||||
src('index.scss', { sourcemaps: true }),
|
||||
require('gulp-sass')(),
|
||||
require('gulp-autoprefixer')([
|
||||
'last 1 version',
|
||||
'> 1%'
|
||||
]),
|
||||
require('gulp-autoprefixer')(['last 1 version', '> 1%']),
|
||||
PRODUCTION && require('gulp-csso')(),
|
||||
dest()
|
||||
)
|
||||
@@ -291,50 +281,23 @@ gulp.task(function copyAssets () {
|
||||
src(['assets/**/*', 'favicon.*']),
|
||||
src('fontawesome-webfont.*', {
|
||||
base: __dirname + '/node_modules/font-awesome/fonts', // eslint-disable-line no-path-concat
|
||||
passthrough: true
|
||||
passthrough: true,
|
||||
}),
|
||||
src(['!*.css', 'font-mfizz.*'], {
|
||||
base: __dirname + '/node_modules/font-mfizz/dist', // eslint-disable-line no-path-concat
|
||||
passthrough: true
|
||||
passthrough: true,
|
||||
}),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
|
||||
gulp.task('build', gulp.parallel(
|
||||
'buildPages',
|
||||
'buildScripts',
|
||||
'buildStyles',
|
||||
'copyAssets'
|
||||
))
|
||||
gulp.task(
|
||||
'build',
|
||||
gulp.parallel('buildPages', 'buildScripts', 'buildStyles', 'copyAssets')
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
231
package.json
231
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.1.3",
|
||||
"version": "5.15.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -31,102 +31,142 @@
|
||||
"npm": ">=3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ansi_up": "^1.3.0",
|
||||
"asap": "^2.0.4",
|
||||
"ava": "^0.15.0",
|
||||
"babel-eslint": "^6.0.0",
|
||||
"@nraynaud/novnc": "0.6.1",
|
||||
"ansi_up": "^2.0.2",
|
||||
"asap": "^2.0.6",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.1.2",
|
||||
"babel-plugin-dev": "^1.0.0",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"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-env": "^1.6.1",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"babel-runtime": "^6.6.1",
|
||||
"babelify": "^7.2.0",
|
||||
"babel-preset-stage-0": "^6.24.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"babelify": "^8.0.0",
|
||||
"benchmark": "^2.1.0",
|
||||
"bootstrap": "github:twbs/bootstrap#v4-dev",
|
||||
"browserify": "^13.0.0",
|
||||
"bundle-collapser": "^1.2.1",
|
||||
"chartist-plugin-legend": "^0.3.1",
|
||||
"bootstrap": "4.0.0-alpha.5",
|
||||
"browserify": "^15.1.0",
|
||||
"bundle-collapser": "^1.3.0",
|
||||
"chartist": "^0.10.1",
|
||||
"chartist-plugin-legend": "^0.6.1",
|
||||
"chartist-plugin-tooltip": "0.0.11",
|
||||
"classnames": "^2.2.3",
|
||||
"connect": "^3.4.1",
|
||||
"complex-matcher": "^0.1.1",
|
||||
"cookies-js": "^1.2.2",
|
||||
"d3": "^4.0.0-alpha.50",
|
||||
"dependency-check": "^2.5.1",
|
||||
"font-awesome": "^4.5.0",
|
||||
"font-mfizz": "github:fizzed/font-mfizz",
|
||||
"ghooks": "^1.1.1",
|
||||
"globby": "^6.0.0",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-autoprefixer": "^3.1.0",
|
||||
"gulp-csso": "^2.0.0",
|
||||
"d3": "^4.12.2",
|
||||
"dependency-check": "^2.9.2",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-adapter-react-15": "^1.0.5",
|
||||
"enzyme-to-json": "^3.3.0",
|
||||
"eslint": "^4.14.0",
|
||||
"eslint-config-standard": "^10.2.1",
|
||||
"eslint-config-standard-jsx": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-node": "^5.2.1",
|
||||
"eslint-plugin-promise": "^3.6.0",
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"event-to-promise": "^0.8.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"font-mfizz": "^2.4.1",
|
||||
"get-stream": "^3.0.0",
|
||||
"globby": "^7.1.1",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-autoprefixer": "^4.1.0",
|
||||
"gulp-csso": "^3.0.0",
|
||||
"gulp-embedlr": "^0.5.2",
|
||||
"gulp-jade": "^1.1.0",
|
||||
"gulp-plumber": "^1.1.0",
|
||||
"gulp-pug": "^3.1.0",
|
||||
"gulp-refresh": "^1.1.0",
|
||||
"gulp-sass": "^2.2.0",
|
||||
"gulp-uglify": "^2.0.0",
|
||||
"gulp-watch": "^4.3.5",
|
||||
"human-format": "^0.6.0",
|
||||
"jsonrpc-websocket-client": "0.0.1-5",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"gulp-sourcemaps": "^2.6.2",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"gulp-watch": "^5.0.0",
|
||||
"human-format": "^0.10.0",
|
||||
"husky": "^0.14.3",
|
||||
"immutable": "^3.8.2",
|
||||
"index-modules": "^0.3.0",
|
||||
"is-ip": "^2.0.0",
|
||||
"jest": "^22.0.4",
|
||||
"jsonrpc-websocket-client": "^0.2.0",
|
||||
"kindof": "^2.0.0",
|
||||
"later": "^1.2.0",
|
||||
"lint-staged": "^6.0.0",
|
||||
"lodash": "^4.6.1",
|
||||
"loose-envify": "^1.1.0",
|
||||
"marked": "^0.3.5",
|
||||
"modular-css": "^0.25.0",
|
||||
"moment": "^2.13.0",
|
||||
"moment-timezone": "^0.5.4",
|
||||
"notifyjs": "^2.0.1",
|
||||
"novnc-node": "^0.5.3",
|
||||
"promise-toolbox": "^0.4.0",
|
||||
"make-error": "^1.3.2",
|
||||
"marked": "^0.3.9",
|
||||
"modular-cssify": "^7.2.0",
|
||||
"moment": "^2.20.1",
|
||||
"moment-timezone": "^0.5.14",
|
||||
"notifyjs": "^3.0.0",
|
||||
"prettier": "^1.9.2",
|
||||
"promise-toolbox": "^0.9.5",
|
||||
"prop-types": "^15.6.0",
|
||||
"random-password": "^0.1.2",
|
||||
"react": "^15.0.0",
|
||||
"react-addons-shallow-compare": "^15.1.0",
|
||||
"react": "^15.4.1",
|
||||
"react-addons-shallow-compare": "^15.6.2",
|
||||
"react-addons-test-utils": "^15.6.2",
|
||||
"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-chartist": "^0.13.0",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-dnd": "^2.5.4",
|
||||
"react-dnd-html5-backend": "^2.5.4",
|
||||
"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.2.0",
|
||||
"react-notify": "^2.0.1",
|
||||
"react-redux": "^4.4.0",
|
||||
"react-router": "^3.0.0-alpha.1",
|
||||
"react-select": "^1.0.0-beta13",
|
||||
"react-sparklines": "^1.5.0",
|
||||
"react-virtualized": "^7.4.0",
|
||||
"readable-stream": "^2.0.6",
|
||||
"redux": "^3.3.1",
|
||||
"redux-devtools": "^3.1.1",
|
||||
"redux-devtools-dock-monitor": "^1.1.0",
|
||||
"redux-devtools-log-monitor": "^1.0.5",
|
||||
"react-dom": "^15.4.1",
|
||||
"react-dropzone": "^4.2.3",
|
||||
"react-intl": "^2.4.0",
|
||||
"react-key-handler": "^1.0.1",
|
||||
"react-notify": "^3.0.0",
|
||||
"react-overlays": "^0.8.3",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-router": "^3.0.0",
|
||||
"react-select": "^1.1.0",
|
||||
"react-shortcuts": "^2.0.0",
|
||||
"react-sparklines": "1.6.0",
|
||||
"react-test-renderer": "^15.6.2",
|
||||
"react-virtualized": "^8.0.8",
|
||||
"readable-stream": "^2.3.3",
|
||||
"redux": "^3.7.2",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.2.1",
|
||||
"serve-static": "^1.10.2",
|
||||
"standard": "^7.0.0",
|
||||
"superagent": "^2.0.0",
|
||||
"vinyl": "^1.1.1",
|
||||
"reselect": "^2.5.4",
|
||||
"semver": "^5.4.1",
|
||||
"styled-components": "^2.4.0",
|
||||
"tar-stream": "^1.5.5",
|
||||
"uglify-es": "^3.3.4",
|
||||
"uncontrollable-input": "^0.1.1",
|
||||
"url-parse": "^1.2.0",
|
||||
"vinyl": "^2.1.0",
|
||||
"watchify": "^3.7.0",
|
||||
"xo-acl-resolver": "^0.2.1",
|
||||
"whatwg-fetch": "^2.0.3",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.2.3",
|
||||
"xo-common": "^0.1.1",
|
||||
"xo-lib": "^0.8.0",
|
||||
"xo-remote-parser": "^0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"benchmarks": "./tools/run-benchmarks.js 'src/**/*.bench.js'",
|
||||
"build": "npm run build-indexes && NODE_ENV=production gulp build",
|
||||
"build-indexes": "./tools/generate-index src/common/intl/locales",
|
||||
"dev": "npm run build-indexes && gulp build server",
|
||||
"dev-test": "ava --watch",
|
||||
"lint": "standard",
|
||||
"posttest": "npm run lint",
|
||||
"prepublish": "npm run build",
|
||||
"test": "ava"
|
||||
"build-indexes": "index-modules --auto src",
|
||||
"clean": "gulp clean",
|
||||
"dev": "npm run build-indexes && NODE_ENV=development gulp build",
|
||||
"dev-test": "jest --watch",
|
||||
"lint-staged-stash": "touch .lint-staged && git stash save --include-untracked --keep-index && true",
|
||||
"lint-staged-unstash": "git stash pop && rm -f .lint-staged && true",
|
||||
"posttest": "eslint --ignore-path .gitignore src/",
|
||||
"prebuild": "npm run clean",
|
||||
"precommit": "lint-staged",
|
||||
"predev": "npm run clean",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "jest"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
@@ -134,17 +174,14 @@
|
||||
"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",
|
||||
@@ -153,24 +190,38 @@
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"dev",
|
||||
"lodash",
|
||||
"transform-decorators-legacy",
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
"es2015",
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"browsers": ">2%"
|
||||
}
|
||||
}
|
||||
],
|
||||
"react",
|
||||
"stage-0"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"ghooks": {
|
||||
"commit-msg": "npm test"
|
||||
}
|
||||
"jest": {
|
||||
"setupTestFrameworkScriptFile": "./setup-tests.js",
|
||||
"snapshotSerializers": [
|
||||
"enzyme-to-json/serializer"
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
"lint-staged-stash",
|
||||
"prettier --write",
|
||||
"eslint --fix",
|
||||
"jest --findRelatedTests --passWithNoTests",
|
||||
"git add",
|
||||
"lint-staged-unstash"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
4
setup-tests.js
Normal file
4
setup-tests.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { configure } from 'enzyme'
|
||||
import Adapter from 'enzyme-adapter-react-15'
|
||||
|
||||
configure({ adapter: new Adapter() })
|
||||
@@ -4,16 +4,16 @@
|
||||
$ct-series-colors: (
|
||||
$brand-success,
|
||||
$brand-primary,
|
||||
#60bd68,
|
||||
#f17cb0,
|
||||
#b2912f,
|
||||
#b276b2,
|
||||
#decf3f,
|
||||
#f15854,
|
||||
#4d4d4d,
|
||||
#dda458,
|
||||
#eacf7d,
|
||||
#86797d,
|
||||
#b276b2,
|
||||
#f15854,
|
||||
#b2912f,
|
||||
#decf3f,
|
||||
#dda458,
|
||||
#60bd68,
|
||||
#4d4d4d,
|
||||
#eacf7d,
|
||||
#b2c326,
|
||||
#6188e2,
|
||||
#a748ca
|
||||
@@ -27,6 +27,13 @@ $ct-series-colors: (
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
// safari has a bug in flex computing that prevent charts from showing see #1755
|
||||
// by fixing the height with a value found in Chrome it seems like it fixes the issue without breaking the layout
|
||||
// elsewhere
|
||||
.dashboardItem .ct-chart {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
// Line in charts with only 2px in width
|
||||
.ct-line {
|
||||
stroke-width: 2px;
|
||||
|
||||
19
src/common/__snapshots__/grid.spec.js.snap
Normal file
19
src/common/__snapshots__/grid.spec.js.snap
Normal file
@@ -0,0 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Col 1`] = `
|
||||
<div
|
||||
className="col-xs-12"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Container 1`] = `
|
||||
<div
|
||||
className="container-fluid"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Row 1`] = `
|
||||
<div
|
||||
className=" row"
|
||||
/>
|
||||
`;
|
||||
@@ -1,36 +1,60 @@
|
||||
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 propTypes from 'prop-types-decorator'
|
||||
import React, { cloneElement } from 'react'
|
||||
import { noop } from 'lodash'
|
||||
|
||||
const ActionBar = ({ actions, param }) => (
|
||||
import ButtonGroup from './button-group'
|
||||
|
||||
export const Action = ({
|
||||
display,
|
||||
handler,
|
||||
handlerParam,
|
||||
icon,
|
||||
label,
|
||||
pending,
|
||||
redirectOnSuccess,
|
||||
}) => (
|
||||
<ActionButton
|
||||
handler={handler}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
pending={pending}
|
||||
redirectOnSuccess={redirectOnSuccess}
|
||||
size='large'
|
||||
tooltip={display === 'icon' ? label : undefined}
|
||||
>
|
||||
{display === 'both' && label}
|
||||
</ActionButton>
|
||||
)
|
||||
|
||||
Action.propTypes = {
|
||||
display: propTypes.oneOf(['icon', 'both']),
|
||||
handler: propTypes.func.isRequired,
|
||||
icon: propTypes.string.isRequired,
|
||||
label: propTypes.node,
|
||||
pending: propTypes.bool,
|
||||
redirectOnSuccess: propTypes.string,
|
||||
}
|
||||
|
||||
const ActionBar = ({ children, handlerParam = noop, display = 'both' }) => (
|
||||
<ButtonGroup>
|
||||
{map(actions, ({ handler, handlerParam = param, label, icon }, index) => (
|
||||
<Tooltip key={index} content={_(label)}>
|
||||
<ActionButton
|
||||
key={index}
|
||||
btnStyle='secondary'
|
||||
handler={handler}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
size='large'
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
{React.Children.map(children, (child, key) => {
|
||||
if (!child) {
|
||||
return
|
||||
}
|
||||
|
||||
const { props } = child
|
||||
return cloneElement(child, {
|
||||
display: props.display || display,
|
||||
handlerParam: props.handlerParam || handlerParam,
|
||||
key,
|
||||
})
|
||||
})}
|
||||
</ButtonGroup>
|
||||
)
|
||||
|
||||
ActionBar.propTypes = {
|
||||
actions: React.PropTypes.arrayOf(
|
||||
React.PropTypes.shape({
|
||||
label: React.PropTypes.string.isRequired,
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
handler: React.PropTypes.func
|
||||
})
|
||||
).isRequired,
|
||||
display: React.PropTypes.oneOf(['icon', 'text', 'both'])
|
||||
display: propTypes.oneOf(['icon', 'both']),
|
||||
handlerParam: propTypes.any,
|
||||
}
|
||||
export { ActionBar as default }
|
||||
|
||||
@@ -1,68 +1,95 @@
|
||||
import Icon from 'icon'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import React from 'react'
|
||||
import { Button } from 'react-bootstrap-4/lib'
|
||||
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import logError from './log-error'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import Tooltip from './tooltip'
|
||||
import { error as _error } from './notification'
|
||||
|
||||
@propTypes({
|
||||
btnStyle: propTypes.string,
|
||||
// React element to use as button content
|
||||
children: propTypes.node,
|
||||
|
||||
// whether this button is disabled (default to false)
|
||||
disabled: propTypes.bool,
|
||||
|
||||
// form identifier
|
||||
//
|
||||
// if provided, this button and its action are associated to this
|
||||
// form for the submit event
|
||||
form: propTypes.string,
|
||||
|
||||
// function to call when the action is triggered (via a clik on the
|
||||
// button or submit on the form)
|
||||
handler: propTypes.func.isRequired,
|
||||
|
||||
// optional value which will be passed as first param to the handler
|
||||
handlerParam: propTypes.any,
|
||||
|
||||
// XO icon to use for this button
|
||||
icon: propTypes.string.isRequired,
|
||||
redirectOnSuccess: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
]),
|
||||
size: propTypes.oneOf([
|
||||
'large',
|
||||
'small'
|
||||
])
|
||||
|
||||
// whether the action of this action is already underway
|
||||
pending: propTypes.bool,
|
||||
|
||||
// path to redirect to when the triggered action finish successfully
|
||||
//
|
||||
// if a function, it will be called with the result of the action to
|
||||
// compute the path
|
||||
redirectOnSuccess: propTypes.oneOfType([propTypes.func, propTypes.string]),
|
||||
|
||||
// React element to use tooltip for the component
|
||||
tooltip: propTypes.node,
|
||||
})
|
||||
export default class ActionButton extends Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
router: propTypes.object,
|
||||
}
|
||||
|
||||
async _execute () {
|
||||
if (this.state.working) {
|
||||
if (this.props.pending || this.state.working) {
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
handler,
|
||||
handlerParam
|
||||
} = this.props
|
||||
const { children, handler, handlerParam, tooltip } = this.props
|
||||
|
||||
try {
|
||||
this.setState({
|
||||
error: null,
|
||||
working: true
|
||||
error: undefined,
|
||||
working: true,
|
||||
})
|
||||
|
||||
const result = await handler(handlerParam)
|
||||
|
||||
let { redirectOnSuccess } = this.props
|
||||
const { redirectOnSuccess } = this.props
|
||||
if (redirectOnSuccess) {
|
||||
if (isFunction(redirectOnSuccess)) {
|
||||
redirectOnSuccess = redirectOnSuccess(result)
|
||||
}
|
||||
return this.context.router.push(redirectOnSuccess)
|
||||
return this.context.router.push(
|
||||
isFunction(redirectOnSuccess)
|
||||
? redirectOnSuccess(result)
|
||||
: redirectOnSuccess
|
||||
)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
working: false
|
||||
working: false,
|
||||
})
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
error,
|
||||
working: false
|
||||
working: false,
|
||||
})
|
||||
logError(error)
|
||||
|
||||
// ignore when undefined because it usually means that the action has been canceled
|
||||
if (error !== undefined) {
|
||||
logError(error)
|
||||
_error(
|
||||
children || tooltip || error.name,
|
||||
error.message || String(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
_execute = ::this._execute
|
||||
@@ -76,7 +103,9 @@ export default class ActionButton extends Component {
|
||||
const { form } = this.props
|
||||
|
||||
if (form) {
|
||||
document.getElementById(form).addEventListener('submit', this._eventListener)
|
||||
document
|
||||
.getElementById(form)
|
||||
.addEventListener('submit', this._eventListener)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,36 +113,39 @@ export default class ActionButton extends Component {
|
||||
const { form } = this.props
|
||||
|
||||
if (form) {
|
||||
document.getElementById(form).removeEventListener('submit', this._eventListener)
|
||||
document
|
||||
.getElementById(form)
|
||||
.removeEventListener('submit', this._eventListener)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
props: {
|
||||
btnStyle,
|
||||
children,
|
||||
className,
|
||||
disabled,
|
||||
form,
|
||||
icon,
|
||||
size: bsSize,
|
||||
style
|
||||
},
|
||||
state: { error, working }
|
||||
props: { children, icon, pending, tooltip, ...props },
|
||||
state: { error, working },
|
||||
} = this
|
||||
|
||||
return <Button
|
||||
bsStyle={error ? 'warning' : btnStyle}
|
||||
form={form}
|
||||
onClick={!form && this._execute}
|
||||
disabled={working || disabled}
|
||||
type={form ? 'submit' : 'button'}
|
||||
{...{ bsSize, className, style }}
|
||||
>
|
||||
<Icon icon={working ? 'loading' : icon} fixedWidth />
|
||||
{children && ' '}
|
||||
{children}
|
||||
</Button>
|
||||
if (error !== undefined) {
|
||||
props.btnStyle = 'warning'
|
||||
}
|
||||
if (pending || working) {
|
||||
props.disabled = true
|
||||
}
|
||||
delete props.handler
|
||||
delete props.handlerParam
|
||||
if (props.form === undefined) {
|
||||
props.onClick = this._execute
|
||||
}
|
||||
delete props.redirectOnSuccess
|
||||
|
||||
const button = (
|
||||
<Button {...props}>
|
||||
<Icon icon={pending || working ? 'loading' : icon} fixedWidth />
|
||||
{children && ' '}
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
|
||||
return tooltip ? <Tooltip content={tooltip}>{button}</Tooltip> : button
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,6 @@ import ActionButton from '../action-button'
|
||||
import styles from './index.css'
|
||||
|
||||
const ActionRowButton = props => (
|
||||
<ActionButton
|
||||
{...props}
|
||||
className={styles.button}
|
||||
size='small'
|
||||
/>
|
||||
<ActionButton {...props} className={styles.button} size='small' />
|
||||
)
|
||||
export { ActionRowButton as default }
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const ActionToggle = ({ className, value, ...props }) =>
|
||||
const ActionToggle = ({ className, value, ...props }) => (
|
||||
<ActionButton
|
||||
{...props}
|
||||
btnStyle={value ? 'success' : null}
|
||||
icon={value ? 'toggle-on' : 'toggle-off'}
|
||||
/>
|
||||
)
|
||||
|
||||
export default propTypes({
|
||||
value: propTypes.bool
|
||||
value: propTypes.bool,
|
||||
})(ActionToggle)
|
||||
|
||||
29
src/common/add-subscriptions.js
Normal file
29
src/common/add-subscriptions.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
|
||||
const call = fn => fn()
|
||||
|
||||
// `subscriptions` can be a function if we want to ensure that the subscription
|
||||
// callbacks have been correctly initialized when there are circular dependencies
|
||||
const addSubscriptions = subscriptions => Component =>
|
||||
class SubscriptionWrapper extends React.PureComponent {
|
||||
_unsubscribes = null
|
||||
|
||||
componentWillMount () {
|
||||
this._unsubscribes = map(
|
||||
typeof subscriptions === 'function' ? subscriptions(this.props) : subscriptions,
|
||||
(subscribe, prop) =>
|
||||
subscribe(value => this.setState({ [prop]: value }))
|
||||
)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this._unsubscribes.forEach(call)
|
||||
this._unsubscribes = null
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Component {...this.props} {...this.state} />
|
||||
}
|
||||
}
|
||||
export { addSubscriptions as default }
|
||||
@@ -1,11 +1,26 @@
|
||||
import forEach from 'lodash/forEach'
|
||||
import { Component } from 'react'
|
||||
import { PureComponent } from 'react'
|
||||
import { cowSet } from 'utils'
|
||||
import { includes, isArray, forEach, map } from 'lodash'
|
||||
|
||||
import getEventValue from './get-event-value'
|
||||
import invoke from './invoke'
|
||||
import shallowEqual from './shallow-equal'
|
||||
|
||||
export default class BaseComponent extends Component {
|
||||
// Should components logs every renders?
|
||||
//
|
||||
// Usually set to process.env.NODE_ENV !== 'production'.
|
||||
const VERBOSE = false
|
||||
|
||||
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 PureComponent {
|
||||
constructor (props, context) {
|
||||
super(props, context)
|
||||
|
||||
@@ -14,41 +29,74 @@ export default class BaseComponent extends Component {
|
||||
|
||||
this._linkedState = null
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
this.render = invoke(this.render, render => () => {
|
||||
if (VERBOSE) {
|
||||
this.render = (render => () => {
|
||||
console.log('render', this.constructor.name)
|
||||
|
||||
return render.call(this)
|
||||
})
|
||||
})(this.render)
|
||||
}
|
||||
}
|
||||
|
||||
// See https://preactjs.com/guide/linked-state
|
||||
linkState (name) {
|
||||
linkState (name, targetPath) {
|
||||
const key = targetPath !== undefined ? `${name}##${targetPath}` : name
|
||||
|
||||
let linkedState = this._linkedState
|
||||
let cb
|
||||
if (!linkedState) {
|
||||
if (linkedState === null) {
|
||||
linkedState = this._linkedState = {}
|
||||
} else if ((cb = linkedState[name])) {
|
||||
} else if ((cb = linkedState[key]) !== undefined) {
|
||||
return cb
|
||||
}
|
||||
|
||||
return (linkedState[name] = event => {
|
||||
let getValue
|
||||
if (targetPath !== undefined) {
|
||||
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]: getEventValue(event)
|
||||
[name]: getValue(event),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
shouldComponentUpdate (newProps, newState) {
|
||||
return !(
|
||||
shallowEqual(this.props, newProps) &&
|
||||
shallowEqual(this.state, newState)
|
||||
)
|
||||
toggleState (name) {
|
||||
let linkedState = this._linkedState
|
||||
let cb
|
||||
if (linkedState === null) {
|
||||
linkedState = this._linkedState = {}
|
||||
} else if ((cb = linkedState[name]) !== undefined) {
|
||||
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],
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (VERBOSE) {
|
||||
const diff = (name, old, cur) => {
|
||||
const keys = []
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const sendNotification = (title, body) => {
|
||||
new Notify(title, {
|
||||
body,
|
||||
timeout: 5,
|
||||
icon: 'assets/logo.png'
|
||||
icon: 'assets/logo.png',
|
||||
}).show()
|
||||
}
|
||||
|
||||
|
||||
9
src/common/button-group.js
Normal file
9
src/common/button-group.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
const ButtonGroup = ({ children }) => (
|
||||
<div className='btn-group' role='group'>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export { ButtonGroup as default }
|
||||
28
src/common/button-link.js
Normal file
28
src/common/button-link.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import { routerShape } from 'react-router/lib/PropTypes'
|
||||
|
||||
import Button from './button'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const ButtonLink = ({ to, ...props }, { router }) => {
|
||||
props.onClick = () => {
|
||||
router.push(to)
|
||||
}
|
||||
|
||||
return <Button {...props} />
|
||||
}
|
||||
|
||||
propTypes(
|
||||
{
|
||||
to: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.object,
|
||||
propTypes.string,
|
||||
]),
|
||||
},
|
||||
{
|
||||
router: routerShape,
|
||||
}
|
||||
)(ButtonLink)
|
||||
|
||||
export { ButtonLink as default }
|
||||
53
src/common/button.js
Normal file
53
src/common/button.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const Button = ({
|
||||
active,
|
||||
block,
|
||||
btnStyle = 'secondary',
|
||||
children,
|
||||
outline,
|
||||
size,
|
||||
...props
|
||||
}) => {
|
||||
props.className = classNames(
|
||||
props.className,
|
||||
'btn',
|
||||
`btn${outline ? '-outline' : ''}-${btnStyle}`,
|
||||
active !== undefined && 'active',
|
||||
block && 'btn-block',
|
||||
size === 'large' ? 'btn-lg' : size === 'small' ? 'btn-sm' : null
|
||||
)
|
||||
if (props.type === undefined && props.form === undefined) {
|
||||
props.type = 'button'
|
||||
}
|
||||
|
||||
return <button {...props}>{children}</button>
|
||||
}
|
||||
|
||||
propTypes({
|
||||
active: propTypes.bool,
|
||||
block: propTypes.bool,
|
||||
|
||||
// Bootstrap button style
|
||||
//
|
||||
// See https://v4-alpha.getbootstrap.com/components/buttons/#examples
|
||||
//
|
||||
// The default value (secondary) is not listed here because it does
|
||||
// not make sense to explicit it.
|
||||
btnStyle: propTypes.oneOf([
|
||||
'danger',
|
||||
'info',
|
||||
'link',
|
||||
'primary',
|
||||
'success',
|
||||
'warning',
|
||||
]),
|
||||
|
||||
outline: propTypes.bool,
|
||||
size: propTypes.oneOf(['large', 'small']),
|
||||
})(Button)
|
||||
|
||||
export { Button as default }
|
||||
@@ -1,51 +1,40 @@
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const CARD_STYLE = {
|
||||
minHeight: '100%'
|
||||
minHeight: '100%',
|
||||
}
|
||||
|
||||
const CARD_STYLE_WITH_SHADOW = {
|
||||
...CARD_STYLE,
|
||||
boxShadow: '0 10px 6px -6px #777' // https://css-tricks.com/almanac/properties/b/box-shadow/
|
||||
boxShadow: '0 10px 6px -6px #777', // https://css-tricks.com/almanac/properties/b/box-shadow/
|
||||
}
|
||||
|
||||
const CARD_HEADER_STYLE = {
|
||||
minHeight: '100%',
|
||||
textAlign: 'center'
|
||||
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>
|
||||
))
|
||||
shadow: propTypes.bool,
|
||||
})(({ shadow, ...props }) => {
|
||||
props.className = 'card'
|
||||
props.style = shadow ? CARD_STYLE_WITH_SHADOW : CARD_STYLE
|
||||
|
||||
return <div {...props} />
|
||||
})
|
||||
|
||||
export const CardHeader = propTypes({
|
||||
className: propTypes.string
|
||||
})(({
|
||||
children,
|
||||
className
|
||||
}) => (
|
||||
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>
|
||||
className: propTypes.string,
|
||||
})(({ children, className }) => (
|
||||
<div className={`card-block ${className || ''}`}>{children}</div>
|
||||
))
|
||||
|
||||
@@ -2,11 +2,10 @@ import React from 'react'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const CenterPanel = ({ children }) =>
|
||||
const CenterPanel = ({ children }) => (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export { CenterPanel as default }
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
import React from 'react'
|
||||
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
@propTypes({
|
||||
children: propTypes.any.isRequired,
|
||||
className: propTypes.string,
|
||||
buttonText: propTypes.any.isRequired
|
||||
buttonText: propTypes.any.isRequired,
|
||||
defaultOpen: propTypes.bool,
|
||||
})
|
||||
export default class Collapse extends Component {
|
||||
state = {
|
||||
isOpened: this.props.defaultOpen,
|
||||
}
|
||||
|
||||
_onClick = () => {
|
||||
this.setState({
|
||||
isOpened: !this.state.isOpened
|
||||
isOpened: !this.state.isOpened,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,9 +28,10 @@ export default class Collapse extends Component {
|
||||
|
||||
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>
|
||||
<Button block btnStyle='primary' size='large' onClick={this._onClick}>
|
||||
{props.buttonText}{' '}
|
||||
<Icon icon={`chevron-${isOpened ? 'up' : 'down'}`} />
|
||||
</Button>
|
||||
{isOpened && props.children}
|
||||
</div>
|
||||
)
|
||||
|
||||
61
src/common/combobox.js
Normal file
61
src/common/combobox.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { isEmpty, map } from 'lodash'
|
||||
import { DropdownButton, MenuItem } from 'react-bootstrap-4/lib'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
@uncontrollableInput({
|
||||
defaultValue: '',
|
||||
})
|
||||
@propTypes({
|
||||
disabled: propTypes.bool,
|
||||
options: propTypes.oneOfType([
|
||||
propTypes.arrayOf(propTypes.string),
|
||||
propTypes.objectOf(propTypes.string),
|
||||
]),
|
||||
onChange: propTypes.func.isRequired,
|
||||
value: propTypes.string.isRequired,
|
||||
})
|
||||
export default class Combobox extends Component {
|
||||
_handleChange = event => {
|
||||
this.props.onChange(event.target.value)
|
||||
}
|
||||
|
||||
_setText (value) {
|
||||
this.props.onChange(value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { options, ...props } = this.props
|
||||
|
||||
props.className = 'form-control'
|
||||
props.onChange = this._handleChange
|
||||
const Input = <input {...props} />
|
||||
|
||||
if (isEmpty(options)) {
|
||||
return Input
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='input-group'>
|
||||
<div className='input-group-btn'>
|
||||
<DropdownButton
|
||||
bsStyle='secondary'
|
||||
disabled={props.disabled}
|
||||
id='selectInput'
|
||||
title=''
|
||||
>
|
||||
{map(options, option => (
|
||||
<MenuItem key={option} onClick={() => this._setText(option)}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownButton>
|
||||
</div>
|
||||
{Input}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.button {
|
||||
border-radius: 0px;
|
||||
};
|
||||
@@ -1,101 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import {
|
||||
parse,
|
||||
toString
|
||||
} from './'
|
||||
import {
|
||||
ast,
|
||||
pattern
|
||||
} from './index.fixtures'
|
||||
|
||||
export default ({ benchmark }) => {
|
||||
benchmark('parse', () => {
|
||||
parse(pattern)
|
||||
})
|
||||
|
||||
benchmark('toString', () => {
|
||||
ast::toString()
|
||||
})
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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')
|
||||
]))
|
||||
])
|
||||
@@ -1,405 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
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())
|
||||
})
|
||||
126
src/common/computed.js
Normal file
126
src/common/computed.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
|
||||
const {
|
||||
create,
|
||||
defineProperty,
|
||||
defineProperties,
|
||||
getOwnPropertyDescriptors = obj => {
|
||||
const descriptors = {}
|
||||
const { getOwnPropertyDescriptor } = Object
|
||||
for (const prop in obj) {
|
||||
const descriptor = getOwnPropertyDescriptor(obj, prop)
|
||||
if (descriptor !== undefined) {
|
||||
descriptors[prop] = descriptor
|
||||
}
|
||||
}
|
||||
return descriptors
|
||||
},
|
||||
prototype: { hasOwnProperty },
|
||||
} = Object
|
||||
|
||||
const makePropsSpy =
|
||||
typeof Proxy !== 'undefined'
|
||||
? (obj, spy) =>
|
||||
new Proxy(obj, {
|
||||
get: (target, property) => (spy[property] = target[property]),
|
||||
})
|
||||
: (obj, spy) => {
|
||||
const descriptors = {}
|
||||
const props = getOwnPropertyDescriptors(obj)
|
||||
for (const prop in props) {
|
||||
const { configurable, enumerable, get, value } = props[prop]
|
||||
descriptors[prop] = {
|
||||
configurable,
|
||||
enumerable,
|
||||
get:
|
||||
get !== undefined
|
||||
? () => (spy[prop] = get.call(obj))
|
||||
: () => (spy[prop] = value),
|
||||
}
|
||||
}
|
||||
return create(null, descriptors)
|
||||
}
|
||||
|
||||
// Decorator which provides computed properties for React components.
|
||||
//
|
||||
// ```js
|
||||
// const MyComponent = computed({
|
||||
// fullName: ({ firstName, lastName }) => `${lastName}, ${firstName}`
|
||||
// })(({ fullName }) =>
|
||||
// <p>{fullName}</p>
|
||||
// )
|
||||
// ```
|
||||
const computed = computed => Component =>
|
||||
class extends PureComponent {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this._computedCache = create(null)
|
||||
this._computedDeps = create(null)
|
||||
|
||||
const descriptors = (this._descriptors = {})
|
||||
for (const name in computed) {
|
||||
if (!hasOwnProperty.call(computed, name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const transform = computed[name]
|
||||
let running = false
|
||||
descriptors[name] = {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
// this is necessary to allow a computed value to depend on
|
||||
// itself
|
||||
if (running) {
|
||||
console.log(name, 'running')
|
||||
return this.props[name]
|
||||
}
|
||||
|
||||
const cache = this._computedCache
|
||||
const deps = this._computedDeps
|
||||
|
||||
const dependencies = deps[name]
|
||||
let needsRecompute = dependencies === undefined
|
||||
if (!needsRecompute) {
|
||||
const { props } = this
|
||||
for (const depName in dependencies) {
|
||||
const value =
|
||||
depName === name || !(depName in cache)
|
||||
? props[depName]
|
||||
: cache[depName]
|
||||
needsRecompute = value !== dependencies[depName]
|
||||
if (needsRecompute) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(name, needsRecompute)
|
||||
|
||||
if (needsRecompute) {
|
||||
running = true
|
||||
cache[name] = transform(
|
||||
makePropsSpy(this._props, (deps[name] = create(null)))
|
||||
)
|
||||
running = false
|
||||
}
|
||||
|
||||
const value = cache[name]
|
||||
defineProperty(this._props, name, {
|
||||
enumerable: true,
|
||||
value,
|
||||
})
|
||||
return value
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
this._props = defineProperties({ ...this.props }, this._descriptors)
|
||||
|
||||
return <Component {...this._props} />
|
||||
}
|
||||
}
|
||||
export { computed as default }
|
||||
@@ -2,26 +2,33 @@ import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import classNames from 'classnames'
|
||||
import React, { createElement } from 'react'
|
||||
|
||||
import _ from '../intl'
|
||||
import Button from '../button'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const Copiable = propTypes({
|
||||
data: propTypes.string,
|
||||
tagName: propTypes.string
|
||||
})(props => createElement(
|
||||
props.tagName || 'span',
|
||||
{
|
||||
...props,
|
||||
className: classNames(styles.container, props.className)
|
||||
},
|
||||
props.children,
|
||||
' ',
|
||||
<CopyToClipboard text={props.data || props.children}>
|
||||
<button className={classNames('btn btn-sm btn-secondary', styles.button)}>
|
||||
<Icon icon='clipboard' />
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
))
|
||||
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={styles.button} size='small'>
|
||||
<Icon icon='clipboard' />
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
)
|
||||
)
|
||||
export { Copiable as default }
|
||||
|
||||
60
src/common/debounce-component-decorator.js
Normal file
60
src/common/debounce-component-decorator.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
import getEventValue from './get-event-value'
|
||||
|
||||
const DEFAULT_DELAY = ({ debounceTimeout = 250 }) => debounceTimeout
|
||||
|
||||
const debounceComponentDecorator = (delay = DEFAULT_DELAY) => Component =>
|
||||
class DebouncedComponent extends React.Component {
|
||||
constructor (props) {
|
||||
super()
|
||||
this.state = { value: props.value }
|
||||
|
||||
this._notify = debounce(event => {
|
||||
this.props.onChange(event)
|
||||
}, typeof delay === 'function' ? delay(props) : delay)
|
||||
|
||||
this._onChange = event => {
|
||||
this.setState({ value: getEventValue(event) })
|
||||
|
||||
event.persist()
|
||||
this._notify(event)
|
||||
}
|
||||
|
||||
this._wrappedInstance = null
|
||||
this._onRef = ref => {
|
||||
this._wrappedInstance = ref
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps ({ value }) {
|
||||
if (value !== this.props.value) {
|
||||
this._notify.cancel()
|
||||
this.setState({ value })
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this._notify.flush()
|
||||
}
|
||||
|
||||
getWrappedInstance () {
|
||||
return this._wrappedInstance
|
||||
}
|
||||
|
||||
render () {
|
||||
const props = {
|
||||
...this.props,
|
||||
onChange: this._onChange,
|
||||
ref: this._onRef,
|
||||
value: this.state.value,
|
||||
}
|
||||
return <Component {...props} />
|
||||
}
|
||||
}
|
||||
export { debounceComponentDecorator as default }
|
||||
|
||||
// common components
|
||||
export const Input = debounceComponentDecorator()('input')
|
||||
export const Textarea = debounceComponentDecorator()('textarea')
|
||||
@@ -1,19 +1,21 @@
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import { isPromise } from 'promise-toolbox'
|
||||
|
||||
const toString = value => JSON.stringify(value, null, 2)
|
||||
const toString = value =>
|
||||
value === undefined ? 'undefined' : 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
|
||||
promise: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
status: 'pending'
|
||||
status: 'pending',
|
||||
}
|
||||
|
||||
props.promise.then(
|
||||
@@ -33,21 +35,26 @@ class DebugAsync extends Component {
|
||||
return <pre>{'Promise { <pending> }'}</pre>
|
||||
}
|
||||
|
||||
return <pre>
|
||||
{'Promise { '}
|
||||
return (
|
||||
<pre>
|
||||
{'Promise { '}
|
||||
{status === 'rejected' && '<rejected> '}
|
||||
{toString(value)}
|
||||
{' }'}
|
||||
</pre>
|
||||
{' }'}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const Debug = ({ value }) => isPromise(value)
|
||||
? <DebugAsync promise={value} />
|
||||
: <pre>{toString(value)}</pre>
|
||||
const Debug = ({ value }) =>
|
||||
isPromise(value) ? (
|
||||
<DebugAsync promise={value} />
|
||||
) : (
|
||||
<pre>{toString(value)}</pre>
|
||||
)
|
||||
|
||||
Debug.propTypes = {
|
||||
value: PropTypes.any.isRequired
|
||||
value: PropTypes.any.isRequired,
|
||||
}
|
||||
|
||||
export { Debug as default }
|
||||
|
||||
22
src/common/dropzone/index.css
Normal file
22
src/common/dropzone/index.css
Normal file
@@ -0,0 +1,22 @@
|
||||
@value dropzoneColor: #8f8686;
|
||||
|
||||
.dropzone {
|
||||
border-radius: 4px;
|
||||
border: 2px dashed dropzoneColor;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 12em;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.activeDropzone {
|
||||
background: #f0f0f0;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.dropzoneText {
|
||||
color: dropzoneColor;
|
||||
font-size: 1.2em;
|
||||
margin: auto;
|
||||
}
|
||||
26
src/common/dropzone/index.js
Normal file
26
src/common/dropzone/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import Component from 'base-component'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import ReactDropzone from 'react-dropzone'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@propTypes({
|
||||
onDrop: propTypes.func,
|
||||
message: propTypes.node,
|
||||
})
|
||||
export default class Dropzone extends Component {
|
||||
render () {
|
||||
const { onDrop, message } = this.props
|
||||
|
||||
return (
|
||||
<ReactDropzone
|
||||
onDrop={onDrop}
|
||||
className={styles.dropzone}
|
||||
activeClassName={styles.activeDropzone}
|
||||
>
|
||||
<div className={styles.dropzoneText}>{message}</div>
|
||||
</ReactDropzone>
|
||||
)
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import classNames from 'classnames'
|
||||
import findKey from 'lodash/findKey'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import isString from 'lodash/isString'
|
||||
@@ -5,46 +6,43 @@ 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 _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
import Icon from '../icon'
|
||||
import logError from '../log-error'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import Tooltip from '../tooltip'
|
||||
import { formatSize } from '../utils'
|
||||
import { SizeInput } from '../form'
|
||||
import {
|
||||
SelectHost,
|
||||
SelectIp,
|
||||
SelectNetwork,
|
||||
SelectPool,
|
||||
SelectRemote,
|
||||
SelectResourceSetIp,
|
||||
SelectSr,
|
||||
SelectSubject,
|
||||
SelectTag,
|
||||
SelectVgpuType,
|
||||
SelectVm,
|
||||
SelectVmTemplate
|
||||
} from './select-objects'
|
||||
SelectVmTemplate,
|
||||
} from '../select-objects'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const LONG_CLICK = 400
|
||||
const SELECT_STYLE = { padding: '0px' }
|
||||
const SIZE_STYLE = { width: '10rem' }
|
||||
const EDITABLE_STYLE = {
|
||||
borderBottom: '1px dashed #ccc',
|
||||
cursor: 'context-menu'
|
||||
}
|
||||
const LONG_EDITABLE_STYLE = {
|
||||
cursor: 'context-menu'
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
alt: propTypes.node.isRequired
|
||||
alt: propTypes.node.isRequired,
|
||||
})
|
||||
class Hover extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
hover: false
|
||||
hover: false,
|
||||
}
|
||||
|
||||
this._onMouseEnter = () => this.setState({ hover: true })
|
||||
@@ -53,25 +51,18 @@ class Hover extends Component {
|
||||
|
||||
render () {
|
||||
if (this.state.hover) {
|
||||
return <span onMouseLeave={this._onMouseLeave}>
|
||||
{this.props.alt}
|
||||
</span>
|
||||
return <span onMouseLeave={this._onMouseLeave}>{this.props.alt}</span>
|
||||
}
|
||||
|
||||
return <span onMouseEnter={this._onMouseEnter}>
|
||||
{this.props.children}
|
||||
</span>
|
||||
return <span onMouseEnter={this._onMouseEnter}>{this.props.children}</span>
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
onChange: propTypes.func.isRequired,
|
||||
onUndo: propTypes.oneOfType([
|
||||
propTypes.bool,
|
||||
propTypes.func
|
||||
]),
|
||||
onUndo: propTypes.oneOfType([propTypes.bool, propTypes.func]),
|
||||
useLongClick: propTypes.bool,
|
||||
value: propTypes.any.isRequired
|
||||
value: propTypes.any.isRequired,
|
||||
})
|
||||
class Editable extends Component {
|
||||
get value () {
|
||||
@@ -97,7 +88,7 @@ class Editable extends Component {
|
||||
this.setState({
|
||||
editing: true,
|
||||
error: null,
|
||||
saving: false
|
||||
saving: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -115,10 +106,7 @@ class Editable extends Component {
|
||||
}
|
||||
|
||||
_save () {
|
||||
return this.__save(
|
||||
() => this.value,
|
||||
this.props.onChange
|
||||
)
|
||||
return this.__save(() => this.value, this.props.onChange)
|
||||
}
|
||||
|
||||
async __save (getValue, saveValue) {
|
||||
@@ -139,8 +127,9 @@ class Editable extends Component {
|
||||
this._closeEdition()
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
error: isString(error) ? error : error.message,
|
||||
saving: false
|
||||
// `error` may be undefined if the action has been cancelled
|
||||
error: error !== undefined && (isString(error) ? error : error.message),
|
||||
saving: false,
|
||||
})
|
||||
logError(error)
|
||||
}
|
||||
@@ -163,34 +152,59 @@ class Editable extends Component {
|
||||
const { useLongClick } = props
|
||||
|
||||
const success = <Icon icon='success' />
|
||||
return <span style={useLongClick ? LONG_EDITABLE_STYLE : EDITABLE_STYLE}>
|
||||
return (
|
||||
<span
|
||||
onClick={!useLongClick && this._openEdition}
|
||||
onMouseDown={useLongClick && this.__startTimer}
|
||||
onMouseUp={useLongClick && this.__stopTimer}
|
||||
className={classNames(
|
||||
styles.clickToEdit,
|
||||
!useLongClick && styles.shortClick
|
||||
)}
|
||||
>
|
||||
{this._renderDisplay()}
|
||||
</span>
|
||||
{previous != null && (onUndo !== false
|
||||
? <Hover
|
||||
alt={<a onClick={this._undo}><Icon icon='undo' /></a>}
|
||||
<span
|
||||
onClick={useLongClick ? undefined : this._openEdition}
|
||||
onMouseDown={useLongClick ? this.__startTimer : undefined}
|
||||
onMouseUp={useLongClick ? this.__stopTimer : undefined}
|
||||
>
|
||||
{success}
|
||||
</Hover>
|
||||
: success
|
||||
)}
|
||||
</span>
|
||||
{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>
|
||||
return (
|
||||
<span>
|
||||
{this._renderEdition()}
|
||||
{saving && (
|
||||
<span>
|
||||
{' '}
|
||||
<Icon icon='loading' />
|
||||
</span>
|
||||
)}
|
||||
{error != null && (
|
||||
<span>
|
||||
{' '}
|
||||
<Tooltip content={error}>
|
||||
<Icon icon='error' />
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +213,7 @@ class Editable extends Component {
|
||||
maxLength: propTypes.number,
|
||||
minLength: propTypes.number,
|
||||
pattern: propTypes.string,
|
||||
value: propTypes.string.isRequired
|
||||
value: propTypes.string.isRequired,
|
||||
})
|
||||
export class Text extends Editable {
|
||||
get value () {
|
||||
@@ -219,25 +233,22 @@ export class Text extends Editable {
|
||||
}
|
||||
|
||||
_renderDisplay () {
|
||||
const {
|
||||
children,
|
||||
value
|
||||
} = this.props
|
||||
const { children, value } = this.props
|
||||
|
||||
if (children || value) {
|
||||
return <span> {children || value} </span>
|
||||
}
|
||||
|
||||
const {
|
||||
placeholder,
|
||||
useLongClick
|
||||
} = this.props
|
||||
const { placeholder, useLongClick } = this.props
|
||||
|
||||
return <span className='text-muted'>
|
||||
{placeholder ||
|
||||
(useLongClick ? _('editableLongClickPlaceholder') : _('editableClickPlaceholder'))
|
||||
}
|
||||
</span>
|
||||
return (
|
||||
<span className='text-muted'>
|
||||
{placeholder ||
|
||||
(useLongClick
|
||||
? _('editableLongClickPlaceholder')
|
||||
: _('editableClickPlaceholder'))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
_renderEdition () {
|
||||
@@ -249,24 +260,26 @@ export class Text extends Editable {
|
||||
'autoComplete',
|
||||
'maxLength',
|
||||
'minLength',
|
||||
'pattern'
|
||||
'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`
|
||||
}}
|
||||
type={this._isPassword ? 'password' : 'text'}
|
||||
/>
|
||||
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'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +291,7 @@ export class Password extends Text {
|
||||
|
||||
@propTypes({
|
||||
nullable: propTypes.bool,
|
||||
value: propTypes.number
|
||||
value: propTypes.number,
|
||||
})
|
||||
export class Number extends Component {
|
||||
get value () {
|
||||
@@ -301,117 +314,117 @@ export class Number extends Component {
|
||||
|
||||
render () {
|
||||
const { value } = this.props
|
||||
return <Text
|
||||
{...this.props}
|
||||
onChange={this._onChange}
|
||||
value={value === null ? '' : String(value)}
|
||||
/>
|
||||
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
|
||||
options: propTypes.oneOfType([propTypes.array, propTypes.object]).isRequired,
|
||||
renderer: propTypes.func,
|
||||
})
|
||||
export class Select extends Editable {
|
||||
constructor (props) {
|
||||
super()
|
||||
|
||||
this._defaultValue = findKey(props.options, option => option === props.value)
|
||||
componentWillReceiveProps (props) {
|
||||
if (
|
||||
props.value !== this.props.value ||
|
||||
props.options !== this.props.options
|
||||
) {
|
||||
this.setState({
|
||||
valueKey: findKey(props.options, option => option === props.value),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.props.options[this._select.value]
|
||||
return this.props.options[this.state.valueKey]
|
||||
}
|
||||
|
||||
_onChange = event => {
|
||||
this._save()
|
||||
this.setState({ valueKey: getEventValue(event) }, this._save)
|
||||
}
|
||||
_optionToJsx = (option, index) => {
|
||||
const { labelProp } = this.props
|
||||
return <option
|
||||
key={index}
|
||||
value={index}
|
||||
>
|
||||
{labelProp ? option[labelProp] : option}
|
||||
</option>
|
||||
|
||||
_optionToJsx = (option, key) => {
|
||||
const { renderer } = this.props
|
||||
|
||||
return (
|
||||
<option key={key} value={key}>
|
||||
{renderer ? renderer(option) : 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>
|
||||
const { children, renderer, value } = this.props
|
||||
|
||||
return children || <span>{renderer ? renderer(value) : value}</span>
|
||||
}
|
||||
|
||||
_renderEdition () {
|
||||
const { saving } = this.state
|
||||
const { saving, valueKey } = this.state
|
||||
const { options } = this.props
|
||||
|
||||
return <select
|
||||
autoFocus
|
||||
className='form-control'
|
||||
defaultValue={this._defaultValue}
|
||||
onBlur={this._closeEdition}
|
||||
onChange={this._onChange}
|
||||
onKeyDown={this._onKeyDown}
|
||||
readOnly={saving}
|
||||
ref={this._onEditionMount}
|
||||
style={SELECT_STYLE}
|
||||
>
|
||||
{map(options, this._optionToJsx)}
|
||||
</select>
|
||||
return (
|
||||
<select
|
||||
autoFocus
|
||||
className={classNames('form-control', styles.select)}
|
||||
onBlur={this._closeEdition}
|
||||
onChange={this._onChange}
|
||||
onKeyDown={this._onKeyDown}
|
||||
readOnly={saving}
|
||||
ref={this._onEditionMount}
|
||||
value={valueKey}
|
||||
>
|
||||
{map(options, this._optionToJsx)}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const MAP_TYPE_SELECT = {
|
||||
host: SelectHost,
|
||||
ip: SelectIp,
|
||||
network: SelectNetwork,
|
||||
pool: SelectPool,
|
||||
remote: SelectRemote,
|
||||
resourceSetIp: SelectResourceSetIp,
|
||||
SR: SelectSr,
|
||||
subject: SelectSubject,
|
||||
tag: SelectTag,
|
||||
vgpuType: SelectVgpuType,
|
||||
VM: SelectVm,
|
||||
'VM-template': SelectVmTemplate
|
||||
'VM-template': SelectVmTemplate,
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
labelProp: propTypes.string.isRequired,
|
||||
predicate: propTypes.func,
|
||||
value: propTypes.oneOfType([
|
||||
propTypes.string,
|
||||
propTypes.object
|
||||
]).isRequired
|
||||
value: propTypes.oneOfType([propTypes.string, propTypes.object]),
|
||||
})
|
||||
export class XoSelect extends Editable {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
return this.state.value
|
||||
}
|
||||
|
||||
_renderDisplay () {
|
||||
return this.props.children ||
|
||||
<span>{this.props.value[this.props.labelProp]}</span>
|
||||
return (
|
||||
this.props.children || (
|
||||
<span>{this.props.value[this.props.labelProp]}</span>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
_onChange = object => {
|
||||
object ? this._save() : this._closeEdition()
|
||||
}
|
||||
_onChange = object => this.setState({ value: object }, object && this._save)
|
||||
|
||||
_renderEdition () {
|
||||
const {
|
||||
placeholder,
|
||||
predicate,
|
||||
saving,
|
||||
xoType
|
||||
} = this.props
|
||||
const { saving, xoType, ...props } = this.props
|
||||
|
||||
const Select = MAP_TYPE_SELECT[xoType]
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
@@ -422,21 +435,21 @@ export class XoSelect extends Editable {
|
||||
|
||||
// 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>
|
||||
return (
|
||||
<a onBlur={this._closeEdition}>
|
||||
<Select
|
||||
{...props}
|
||||
autoFocus
|
||||
disabled={saving}
|
||||
onChange={this._onChange}
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
value: propTypes.number.isRequired
|
||||
value: propTypes.number.isRequired,
|
||||
})
|
||||
export class Size extends Editable {
|
||||
get value () {
|
||||
@@ -454,24 +467,31 @@ export class Size extends Editable {
|
||||
}, 10)
|
||||
}
|
||||
|
||||
_focus = () => { this._focused = true }
|
||||
_focus = () => {
|
||||
this._focused = true
|
||||
}
|
||||
|
||||
_renderEdition () {
|
||||
const { saving } = this.state
|
||||
const { value } = this.props
|
||||
|
||||
return <span
|
||||
onBlur={this._closeEditionIfUnfocused}
|
||||
onFocus={this._focus}
|
||||
onKeyDown={this._onKeyDown}
|
||||
>
|
||||
<SizeInput
|
||||
autoFocus
|
||||
ref='input'
|
||||
readOnly={saving}
|
||||
style={SIZE_STYLE}
|
||||
defaultValue={value}
|
||||
/>
|
||||
</span>
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,24 +3,22 @@ import React from 'react'
|
||||
const ellipsisStyle = {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
whiteSpace: 'nowrap',
|
||||
}
|
||||
|
||||
const ellipsisContainerStyle = {
|
||||
display: 'flex'
|
||||
display: 'flex',
|
||||
}
|
||||
|
||||
const Ellipsis = ({ children }) => (
|
||||
<span style={ellipsisStyle}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
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>
|
||||
{React.Children.map(
|
||||
children,
|
||||
child =>
|
||||
child == null || child.type === Ellipsis ? child : <span>{child}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
11
src/common/fetch.js
Normal file
11
src/common/fetch.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import 'whatwg-fetch'
|
||||
|
||||
const { fetch } = window
|
||||
export { fetch as default }
|
||||
|
||||
export const post = (url, body, opts) =>
|
||||
fetch(url, {
|
||||
...opts,
|
||||
body,
|
||||
method: 'POST',
|
||||
})
|
||||
@@ -11,14 +11,8 @@ import identity from 'lodash/identity'
|
||||
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 (!length || !predicate || (i = findIndex(array, predicate)) === -1) {
|
||||
return initial == null ? array.slice(0) : array.concat(initial)
|
||||
}
|
||||
|
||||
if (reducer == null) {
|
||||
@@ -26,9 +20,7 @@ const filterReduce = (array, predicate, reducer, initial) => {
|
||||
}
|
||||
|
||||
const result = array.slice(0, i)
|
||||
let value = initial == null
|
||||
? array[i]
|
||||
: reducer(initial, array[i], i, array)
|
||||
let value = initial == null ? array[i] : reducer(initial, array[i], i, array)
|
||||
|
||||
for (i = i + 1; i < length; ++i) {
|
||||
const current = array[i]
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
import test from 'ava'
|
||||
/* eslint-env jest */
|
||||
|
||||
import filterReduce from './filter-reduce'
|
||||
|
||||
const add = (a, b) => a + b
|
||||
const data = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
|
||||
const data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
const isEven = x => !(x & 1)
|
||||
|
||||
test('filterReduce', t => {
|
||||
it('filterReduce', () => {
|
||||
// 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 ]
|
||||
)
|
||||
expect(filterReduce(data, isEven, add)).toEqual([1, 3, 5, 7, 9, 20])
|
||||
|
||||
// The default reducer is the identity.
|
||||
t.deepEqual(
|
||||
filterReduce(data, isEven),
|
||||
[ 1, 3, 5, 7, 9, 0 ]
|
||||
)
|
||||
expect(filterReduce(data, isEven)).toEqual([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 ]
|
||||
)
|
||||
expect(filterReduce(data, isEven, add, 22)).toEqual([1, 3, 5, 7, 9, 42])
|
||||
})
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
import * as Grid from './grid'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
export const LabelCol = propTypes({
|
||||
children: propTypes.any.isRequired
|
||||
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>
|
||||
))
|
||||
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>
|
||||
))
|
||||
children: propTypes.arrayOf(propTypes.element).isRequired,
|
||||
})(({ children }) => <Grid.Row className='form-group'>{children}</Grid.Row>)
|
||||
|
||||
@@ -5,18 +5,16 @@ 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 SingleLineRow from 'single-line-row'
|
||||
import { Container, Col } from 'grid'
|
||||
import { DropdownButton, MenuItem } from 'react-bootstrap-4/lib'
|
||||
|
||||
import Button from '../button'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import {
|
||||
firstDefined,
|
||||
formatSizeRaw,
|
||||
parseSize
|
||||
} from '../utils'
|
||||
import defined from '../xo-defined'
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import { formatSizeRaw, parseSize } from '../utils'
|
||||
|
||||
export Select from './select'
|
||||
export SelectPlainObject from './select-plain-object'
|
||||
@@ -24,7 +22,7 @@ export SelectPlainObject from './select-plain-object'
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
enableGenerator: propTypes.bool
|
||||
enableGenerator: propTypes.bool,
|
||||
})
|
||||
export class Password extends Component {
|
||||
get value () {
|
||||
@@ -36,114 +34,98 @@ export class Password extends Component {
|
||||
}
|
||||
|
||||
_generate = () => {
|
||||
this.refs.field.value = randomPassword(8)
|
||||
const value = randomPassword(8)
|
||||
const isControlled = this.props.value !== undefined
|
||||
if (isControlled) {
|
||||
this.props.onChange(value)
|
||||
} else {
|
||||
this.refs.field.value = value
|
||||
}
|
||||
|
||||
// FIXME: in controlled mode, visibility should only be updated
|
||||
// when the value prop is changed according to the emitted value.
|
||||
this.setState({
|
||||
visible: true
|
||||
visible: true,
|
||||
})
|
||||
}
|
||||
|
||||
_toggleVisibility = () => {
|
||||
this.setState({
|
||||
visible: !this.state.visible
|
||||
visible: !this.state.visible,
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
className,
|
||||
enableGenerator = false,
|
||||
...props
|
||||
} = this.props
|
||||
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>
|
||||
return (
|
||||
<div className='input-group'>
|
||||
{enableGenerator && (
|
||||
<span className='input-group-btn'>
|
||||
<Button 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 onClick={this._toggleVisibility}>
|
||||
<Icon icon={visible ? 'shown' : 'hidden'} />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
defaultValue: propTypes.number,
|
||||
max: propTypes.number.isRequired,
|
||||
min: propTypes.number.isRequired,
|
||||
onChange: propTypes.func,
|
||||
step: propTypes.number,
|
||||
onChange: propTypes.func
|
||||
value: propTypes.number,
|
||||
})
|
||||
export class Range extends Component {
|
||||
constructor (props) {
|
||||
super()
|
||||
this.state = {
|
||||
value: props.defaultValue || props.min
|
||||
componentDidMount () {
|
||||
const { min, onChange, value } = this.props
|
||||
|
||||
if (!value) {
|
||||
onChange && onChange(min)
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.state.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
const { onChange } = this.props
|
||||
this.state.value = +value
|
||||
|
||||
if (onChange) {
|
||||
onChange(value)
|
||||
}
|
||||
}
|
||||
|
||||
_handleChange = event => {
|
||||
const { onChange } = this.props
|
||||
const { value } = event.target
|
||||
|
||||
if (value === this.state.value) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
value
|
||||
}, onChange && (() => onChange(value)))
|
||||
}
|
||||
_onChange = value => this.props.onChange(getEventValue(value))
|
||||
|
||||
render () {
|
||||
const {
|
||||
props
|
||||
} = this
|
||||
const step = props.step || 1
|
||||
const { value } = this.state
|
||||
const { max, min, step, value } = this.props
|
||||
|
||||
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>
|
||||
<Container>
|
||||
<SingleLineRow>
|
||||
<Col size={2}>
|
||||
<span className='pull-right'>{value}</span>
|
||||
</Col>
|
||||
<Col size={10}>
|
||||
<input
|
||||
className='form-control'
|
||||
max={max}
|
||||
min={min}
|
||||
onChange={this._onChange}
|
||||
step={step}
|
||||
type='range'
|
||||
value={value}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -162,86 +144,121 @@ const DEFAULT_UNIT = 'GiB'
|
||||
readOnly: propTypes.bool,
|
||||
required: propTypes.bool,
|
||||
style: propTypes.object,
|
||||
value: propTypes.number
|
||||
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, 0))
|
||||
this.state = this._createStateFromBytes(
|
||||
defined(props.value, props.defaultValue, null)
|
||||
)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
const { value } = newProps
|
||||
if (value == null && value === this.props.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const { _bytes, _unit, _value } = this
|
||||
this._bytes = this._unit = this._value = null
|
||||
|
||||
if (value === _bytes) {
|
||||
// Update input value
|
||||
this.setState({
|
||||
unit: _unit,
|
||||
value: _value
|
||||
})
|
||||
} else {
|
||||
componentWillReceiveProps (props) {
|
||||
const { value } = props
|
||||
if (value !== undefined && value !== this.props.value) {
|
||||
this.setState(this._createStateFromBytes(value))
|
||||
}
|
||||
}
|
||||
|
||||
_createStateFromBytes = bytes => {
|
||||
const humanSize = bytes && formatSizeRaw(bytes)
|
||||
_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 {
|
||||
unit: humanSize && humanSize.value ? humanSize.prefix + 'B' : this.props.defaultUnit || DEFAULT_UNIT,
|
||||
value: humanSize ? round(humanSize.value, 3) : ''
|
||||
input: String(round(value, 2)),
|
||||
unit: `${prefix}B`,
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { unit, value } = this.state
|
||||
return parseSize(value + ' ' + unit)
|
||||
const { input, unit } = this.state
|
||||
|
||||
if (!input) {
|
||||
return null
|
||||
}
|
||||
|
||||
return parseSize(`${+input} ${unit}`)
|
||||
}
|
||||
|
||||
set value (newValue) {
|
||||
set value (value) {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
this.props.value != null
|
||||
this.props.value !== undefined
|
||||
) {
|
||||
throw new Error('cannot set value of controlled SizeInput')
|
||||
}
|
||||
this.setState(this._createStateFromBytes(newValue))
|
||||
this.setState(this._createStateFromBytes(value))
|
||||
}
|
||||
|
||||
_onChange = value =>
|
||||
this.props.onChange && this.props.onChange(value)
|
||||
_onChange (input, unit) {
|
||||
const { onChange } = this.props
|
||||
|
||||
_updateValue = event => {
|
||||
const { value } = event.target
|
||||
if (this.props.value != null) {
|
||||
this._value = value
|
||||
this._unit = this.state.unit
|
||||
this._bytes = parseSize((value || 0) + ' ' + this.state.unit)
|
||||
// Empty input equals null.
|
||||
const bytes = input ? parseSize(`${+input} ${unit}`) : null
|
||||
|
||||
this._onChange(this._bytes)
|
||||
} else {
|
||||
this.setState({ value }, () => {
|
||||
this._onChange(this.value)
|
||||
})
|
||||
}
|
||||
}
|
||||
_updateUnit = unit => {
|
||||
if (this.props.value != null) {
|
||||
this._value = this.state.value
|
||||
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
|
||||
this._bytes = parseSize((this.state.value || 0) + ' ' + unit)
|
||||
|
||||
this._onChange(this._bytes)
|
||||
} else {
|
||||
this.setState({ unit }, () => {
|
||||
this._onChange(this.value)
|
||||
})
|
||||
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
|
||||
|
||||
if (Number.isNaN(number)) {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,50 +266,40 @@ export class SizeInput extends BaseComponent {
|
||||
const {
|
||||
autoFocus,
|
||||
className,
|
||||
placeholder,
|
||||
readOnly,
|
||||
placeholder,
|
||||
required,
|
||||
style
|
||||
style,
|
||||
} = this.props
|
||||
|
||||
const {
|
||||
value,
|
||||
unit
|
||||
} = this.state
|
||||
|
||||
return <span
|
||||
className={classNames(className, 'input-group')}
|
||||
style={style}
|
||||
>
|
||||
<input
|
||||
autoFocus={autoFocus}
|
||||
className='form-control'
|
||||
min={0}
|
||||
onChange={this._updateValue}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
required={required}
|
||||
type='number'
|
||||
value={value}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<DropdownButton
|
||||
bsStyle='secondary'
|
||||
return (
|
||||
<span className={classNames('input-group', className)} style={style}>
|
||||
<input
|
||||
autoFocus={autoFocus}
|
||||
className='form-control'
|
||||
disabled={readOnly}
|
||||
id='size'
|
||||
pullRight
|
||||
title={unit}
|
||||
>
|
||||
{map(UNITS, unit =>
|
||||
<MenuItem
|
||||
key={unit}
|
||||
onClick={() => this._updateUnit(unit)}
|
||||
>
|
||||
{unit}
|
||||
</MenuItem>
|
||||
)}
|
||||
</DropdownButton>
|
||||
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>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,82 @@
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Component from 'base-component'
|
||||
import find from 'lodash/find'
|
||||
import map from 'lodash/map'
|
||||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
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,
|
||||
placeholder: propTypes.node,
|
||||
predicate: propTypes.func,
|
||||
required: propTypes.bool
|
||||
required: propTypes.bool,
|
||||
value: propTypes.any,
|
||||
})
|
||||
@uncontrollableInput()
|
||||
export default class SelectPlainObject extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
value: this._computeValue(props.defaultValue, props)
|
||||
componentDidMount () {
|
||||
const { options, value } = this.props
|
||||
|
||||
this.setState({
|
||||
options: this._computeOptions(options),
|
||||
value: this._computeValue(value, this.props),
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
if (newProps !== this.props) {
|
||||
this.setState({
|
||||
options: this._computeOptions(newProps.options),
|
||||
value: this._computeValue(newProps.value, newProps),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_computeValue (value, props = this.props) {
|
||||
let { optionKey } = props
|
||||
optionKey || (optionKey = 'id')
|
||||
const reduceValue = value => value != null ? (value[optionKey] || value) : ''
|
||||
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)
|
||||
label: optionRenderer(option),
|
||||
}))
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { optionKey = 'id' } = this.props
|
||||
const { value } = this.state
|
||||
const { options } = this.props
|
||||
_getObject (value) {
|
||||
if (value == null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const { optionKey = 'id', options } = this.props
|
||||
|
||||
const pickValue = value => {
|
||||
value = value.value || value
|
||||
return find(options, option => option[optionKey] === value || option === value)
|
||||
return find(
|
||||
options,
|
||||
option => option[optionKey] === value || option === value
|
||||
)
|
||||
}
|
||||
|
||||
if (this.props.multi) {
|
||||
@@ -80,18 +86,12 @@ export default class SelectPlainObject extends Component {
|
||||
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) }))
|
||||
if (onChange) {
|
||||
onChange(this._getObject(value))
|
||||
}
|
||||
}
|
||||
|
||||
_renderOption = option => option.label
|
||||
@@ -101,7 +101,7 @@ export default class SelectPlainObject extends Component {
|
||||
|
||||
return (
|
||||
<Select
|
||||
autofocus={props.autoFocus}
|
||||
autoFocus={props.autoFocus}
|
||||
disabled={props.disabled}
|
||||
multi={props.multi}
|
||||
onChange={this._handleChange}
|
||||
@@ -111,7 +111,8 @@ export default class SelectPlainObject extends Component {
|
||||
placeholder={props.placeholder}
|
||||
required={props.required}
|
||||
value={state.value}
|
||||
valueRenderer={this._renderOption} />
|
||||
valueRenderer={this._renderOption}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +1,89 @@
|
||||
import map from 'lodash/map'
|
||||
import React, { Component } from 'react'
|
||||
import ReactSelect from 'react-select'
|
||||
import {
|
||||
AutoSizer,
|
||||
VirtualScroll
|
||||
} from 'react-virtualized'
|
||||
import sum from 'lodash/sum'
|
||||
import { AutoSizer, CellMeasurer, List } from 'react-virtualized'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
const SELECT_MENU_STYLE = {
|
||||
overflow: 'hidden'
|
||||
overflow: 'hidden',
|
||||
}
|
||||
|
||||
const SELECT_STYLE = {
|
||||
minWidth: '10em'
|
||||
minWidth: '10em',
|
||||
}
|
||||
|
||||
const LIST_STYLE = {
|
||||
whiteSpace: 'normal',
|
||||
}
|
||||
|
||||
const MAX_OPTIONS = 5
|
||||
|
||||
// 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]
|
||||
optionRenderer: (option, labelKey) => option[labelKey],
|
||||
}
|
||||
|
||||
_renderMenu = ({
|
||||
focusedOption,
|
||||
options,
|
||||
...otherOptions
|
||||
}) => {
|
||||
const {
|
||||
maxHeight,
|
||||
optionHeight
|
||||
} = this.props
|
||||
_renderMenu = ({ focusedOption, options, ...otherOptions }) => {
|
||||
const { maxHeight } = this.props
|
||||
|
||||
const focusedOptionIndex = options.indexOf(focusedOption)
|
||||
const height = Math.min(maxHeight, options.length * optionHeight)
|
||||
let height = options.length > MAX_OPTIONS && maxHeight
|
||||
|
||||
const wrappedRowRenderer = ({ index }) =>
|
||||
const wrappedRowRenderer = ({ index, key, style }) =>
|
||||
this._optionRenderer({
|
||||
...otherOptions,
|
||||
focusedOption,
|
||||
focusedOptionIndex,
|
||||
key,
|
||||
option: options[index],
|
||||
options
|
||||
options,
|
||||
style,
|
||||
})
|
||||
|
||||
return (
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<VirtualScroll
|
||||
height={height}
|
||||
rowCount={options.length}
|
||||
rowHeight={optionHeight}
|
||||
rowRenderer={wrappedRowRenderer}
|
||||
scrollToIndex={focusedOptionIndex}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
{({ width }) =>
|
||||
width ? (
|
||||
<CellMeasurer
|
||||
cellRenderer={({ rowIndex }) =>
|
||||
wrappedRowRenderer({ index: rowIndex })
|
||||
}
|
||||
columnCount={1}
|
||||
rowCount={options.length}
|
||||
// FIXME: 16 px: ugly workaround to take into account the scrollbar
|
||||
// during the offscreen render to measure the row height
|
||||
// See https://github.com/bvaughn/react-virtualized/issues/401
|
||||
width={width - 16}
|
||||
>
|
||||
{({ getRowHeight }) => {
|
||||
if (options.length <= MAX_OPTIONS) {
|
||||
height = sum(
|
||||
map(options, (_, index) => getRowHeight({ index }))
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<List
|
||||
height={height}
|
||||
rowCount={options.length}
|
||||
rowHeight={getRowHeight}
|
||||
rowRenderer={wrappedRowRenderer}
|
||||
scrollToIndex={focusedOptionIndex}
|
||||
style={LIST_STYLE}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</CellMeasurer>
|
||||
) : null
|
||||
}
|
||||
</AutoSizer>
|
||||
)
|
||||
}
|
||||
@@ -68,9 +91,11 @@ export default class Select extends Component {
|
||||
_optionRenderer = ({
|
||||
focusedOption,
|
||||
focusOption,
|
||||
key,
|
||||
labelKey,
|
||||
option,
|
||||
selectValue
|
||||
style,
|
||||
selectValue,
|
||||
}) => {
|
||||
let className = 'Select-option'
|
||||
|
||||
@@ -89,9 +114,10 @@ export default class Select extends Component {
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={!disabled && (() => selectValue(option))}
|
||||
onMouseOver={!disabled && (() => focusOption(option))}
|
||||
style={{ height: props.optionHeight }}
|
||||
onClick={disabled ? undefined : () => selectValue(option)}
|
||||
onMouseOver={disabled ? undefined : () => focusOption(option)}
|
||||
style={style}
|
||||
key={key}
|
||||
>
|
||||
{props.optionRenderer(option, labelKey)}
|
||||
</div>
|
||||
@@ -101,7 +127,9 @@ export default class Select extends Component {
|
||||
render () {
|
||||
return (
|
||||
<ReactSelect
|
||||
closeOnSelect={!this.props.multi}
|
||||
{...this.props}
|
||||
backspaceToRemoveMessage=''
|
||||
menuRenderer={this._renderMenu}
|
||||
menuStyle={SELECT_MENU_STYLE}
|
||||
style={SELECT_STYLE}
|
||||
|
||||
46
src/common/form/toggle.js
Normal file
46
src/common/form/toggle.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
|
||||
import Component from '../base-component'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
@uncontrollableInput()
|
||||
@propTypes({
|
||||
className: propTypes.string,
|
||||
onChange: propTypes.func.isRequired,
|
||||
icon: propTypes.string,
|
||||
iconOn: propTypes.string,
|
||||
iconOff: propTypes.string,
|
||||
iconSize: propTypes.number,
|
||||
value: propTypes.bool.isRequired,
|
||||
})
|
||||
export default class Toggle extends Component {
|
||||
static defaultProps = {
|
||||
iconOn: 'toggle-on',
|
||||
iconOff: 'toggle-off',
|
||||
iconSize: 2,
|
||||
}
|
||||
|
||||
_toggle = () => {
|
||||
const { props } = this
|
||||
props.onChange(!props.value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
|
||||
return (
|
||||
<Icon
|
||||
className={classNames(
|
||||
props.disabled ? 'text-muted' : props.value ? 'text-success' : null,
|
||||
props.className
|
||||
)}
|
||||
icon={props.icon || (props.value ? props.iconOn : props.iconOff)}
|
||||
onClick={this._toggle}
|
||||
size={props.iconSize}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.checkbox {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,8 @@ const getEventValue = event => {
|
||||
return event
|
||||
}
|
||||
|
||||
return (
|
||||
target.nodeName.toLowerCase() === 'input' &&
|
||||
return target.nodeName.toLowerCase() === 'input' &&
|
||||
target.type.toLowerCase() === 'checkbox'
|
||||
)
|
||||
? target.checked
|
||||
: target.value
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
// A column can contain content or a row.
|
||||
export const Col = propTypes({
|
||||
className: propTypes.string,
|
||||
size: propTypes.number,
|
||||
@@ -12,47 +13,49 @@ export const Col = propTypes({
|
||||
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>)
|
||||
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>
|
||||
)
|
||||
)
|
||||
|
||||
// This is the root component of the grid layout, containers should not be
|
||||
// nested.
|
||||
export const Container = propTypes({
|
||||
className: propTypes.string
|
||||
})(({
|
||||
children,
|
||||
className
|
||||
}) => <div className={classNames(className, 'container-fluid')}>
|
||||
{children}
|
||||
</div>)
|
||||
className: propTypes.string,
|
||||
})(({ children, className }) => (
|
||||
<div className={classNames(className, 'container-fluid')}>{children}</div>
|
||||
))
|
||||
|
||||
// Only columns can be children of a row.
|
||||
export const Row = propTypes({
|
||||
className: propTypes.string
|
||||
})(({
|
||||
children,
|
||||
className
|
||||
}) => <div className={`${className || ''} row`}>
|
||||
{children}
|
||||
</div>)
|
||||
className: propTypes.string,
|
||||
})(({ children, className }) => (
|
||||
<div className={`${className || ''} row`}>{children}</div>
|
||||
))
|
||||
|
||||
13
src/common/grid.spec.js
Normal file
13
src/common/grid.spec.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import React from 'react'
|
||||
import { forEach } from 'lodash'
|
||||
import { shallow } from 'enzyme'
|
||||
|
||||
import * as grid from './grid'
|
||||
|
||||
forEach(grid, (Component, name) => {
|
||||
it(name, () => {
|
||||
expect(shallow(<Component />)).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@@ -1,16 +1,33 @@
|
||||
const common = {
|
||||
homeFilterNone: '',
|
||||
}
|
||||
|
||||
export const VM = {
|
||||
...common,
|
||||
homeFilterPendingVms: 'current_operations:"" ',
|
||||
homeFilterNonRunningVms: '!power_state:running ',
|
||||
homeFilterHvmGuests: 'virtualizationMode:hvm ',
|
||||
homeFilterRunningVms: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
homeFilterTags: 'tags:',
|
||||
}
|
||||
|
||||
export const host = {
|
||||
...common,
|
||||
homeFilterRunningHosts: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
homeFilterTags: 'tags:',
|
||||
}
|
||||
|
||||
export const pool = {
|
||||
homeFilterTags: 'tags:'
|
||||
...common,
|
||||
homeFilterTags: 'tags:',
|
||||
}
|
||||
|
||||
export const vmTemplate = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:',
|
||||
}
|
||||
|
||||
export const SR = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:',
|
||||
}
|
||||
|
||||
40
src/common/home-tags.js
Normal file
40
src/common/home-tags.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as CM from 'complex-matcher'
|
||||
import React from 'react'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import Tags from './tags'
|
||||
|
||||
@propTypes({
|
||||
labels: propTypes.arrayOf(React.PropTypes.string).isRequired,
|
||||
onAdd: propTypes.func,
|
||||
onChange: propTypes.func,
|
||||
onDelete: propTypes.func,
|
||||
type: propTypes.string,
|
||||
})
|
||||
export default class HomeTags extends Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object,
|
||||
}
|
||||
|
||||
_onClick = label => {
|
||||
const s = encodeURIComponent(
|
||||
new CM.Property('tags', new CM.String(label)).toString()
|
||||
)
|
||||
const t = encodeURIComponent(this.props.type)
|
||||
|
||||
this.context.router.push(`/home?t=${t}&s=${s}`)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Tags
|
||||
labels={this.props.labels}
|
||||
onAdd={this.props.onAdd}
|
||||
onChange={this.props.onChange}
|
||||
onClick={this._onClick}
|
||||
onDelete={this.props.onDelete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,24 @@
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import { Portal } from 'react-overlays'
|
||||
import { forEach, isEmpty, keys, map, noop } from 'lodash'
|
||||
|
||||
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 propTypes from './prop-types-decorator'
|
||||
import SortedTable from './sorted-table'
|
||||
import TabButton from './tab-button'
|
||||
import { connectStore } from './utils'
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
createFilter,
|
||||
createSelector
|
||||
createSelector,
|
||||
} from './selectors'
|
||||
import {
|
||||
getHostMissingPatches,
|
||||
installAllHostPatches
|
||||
installAllHostPatches,
|
||||
installAllPatchesOnPool,
|
||||
subscribeHostMissingPatches,
|
||||
} from './xo'
|
||||
|
||||
// ===================================================================
|
||||
@@ -27,18 +26,22 @@ import {
|
||||
const MISSING_PATCHES_COLUMNS = [
|
||||
{
|
||||
name: _('srHost'),
|
||||
itemRenderer: host => <Link to={`/hosts/${host.id}`}>{host.name_label}</Link>,
|
||||
sortCriteria: host => host.name_label
|
||||
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
|
||||
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]
|
||||
itemRenderer: (host, { missingPatches }) => (
|
||||
<Link to={`/hosts/${host.id}/patches`}>{missingPatches[host.id]}</Link>
|
||||
),
|
||||
sortCriteria: (host, { missingPatches }) => missingPatches[host.id],
|
||||
},
|
||||
{
|
||||
name: _('patchUpdateButton'),
|
||||
@@ -49,21 +52,33 @@ const MISSING_PATCHES_COLUMNS = [
|
||||
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>
|
||||
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,
|
||||
},
|
||||
sortCriteria: (host, { pools }) => pools[host.$pool].name_label
|
||||
}].concat(MISSING_PATCHES_COLUMNS)
|
||||
].concat(MISSING_PATCHES_COLUMNS)
|
||||
|
||||
// Small component to homogenize Button usage in HostsPatchesTable
|
||||
const ActionButton_ = ({ children, labelId, ...props }) => (
|
||||
<ActionButton {...props} tooltip={_(labelId)}>
|
||||
{children}
|
||||
</ActionButton>
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@connectStore({
|
||||
hostsById: createGetObjectsOfType('host').groupBy('id'),
|
||||
})
|
||||
class HostsPatchesTable extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
@@ -78,105 +93,102 @@ class HostsPatchesTable extends Component {
|
||||
)
|
||||
)
|
||||
|
||||
_refreshMissingPatches = () => (
|
||||
Promise.all(
|
||||
map(this.props.hosts, this._refreshHostMissingPatches)
|
||||
_subscribeMissingPatches = (hosts = this.props.hosts) => {
|
||||
const { hostsById } = this.props
|
||||
|
||||
const unsubs = map(
|
||||
hosts,
|
||||
host =>
|
||||
hostsById
|
||||
? subscribeHostMissingPatches(hostsById[host.id][0], patches =>
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[host.id]: patches.length,
|
||||
},
|
||||
})
|
||||
)
|
||||
: noop
|
||||
)
|
||||
)
|
||||
|
||||
_installAllMissingPatches = () => (
|
||||
Promise.all(map(this._getHosts(), this._installAllHostPatches))
|
||||
)
|
||||
if (this.unsubscribeMissingPatches !== undefined) {
|
||||
this.unsubscribeMissingPatches()
|
||||
}
|
||||
|
||||
_refreshHostMissingPatches = host => (
|
||||
getHostMissingPatches(host).then(patches => {
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[host.id]: patches.length
|
||||
}
|
||||
})
|
||||
this.unsubscribeMissingPatches = () => forEach(unsubs, unsub => unsub())
|
||||
}
|
||||
|
||||
_installAllMissingPatches = () => {
|
||||
const pools = {}
|
||||
forEach(this._getHosts(), host => {
|
||||
pools[host.$pool] = true
|
||||
})
|
||||
)
|
||||
|
||||
_installAllHostPatches = host => (
|
||||
installAllHostPatches(host).then(() =>
|
||||
this._refreshHostMissingPatches(host)
|
||||
)
|
||||
)
|
||||
|
||||
componentWillMount () {
|
||||
this._refreshMissingPatches()
|
||||
return Promise.all(map(keys(pools), installAllPatchesOnPool))
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
// Force one Portal refresh.
|
||||
// Because Portal cannot see the container reference at first rendering.
|
||||
this.forceUpdate()
|
||||
this._subscribeMissingPatches()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
forEach(nextProps.hosts, host => {
|
||||
const { id } = host
|
||||
if (nextProps.hosts !== this.props.hosts) {
|
||||
this._subscribeMissingPatches(nextProps.hosts)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.missingPatches[id] !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[id]: 0
|
||||
}
|
||||
})
|
||||
|
||||
this._refreshHostMissingPatches(host)
|
||||
})
|
||||
componentWillUnmount () {
|
||||
this.unsubscribeMissingPatches()
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
buttonsGroupContainer,
|
||||
container,
|
||||
displayPools,
|
||||
pools,
|
||||
useTabButton,
|
||||
} = this.props
|
||||
|
||||
const hosts = this._getHosts()
|
||||
const noPatches = isEmpty(hosts)
|
||||
const { props } = this
|
||||
|
||||
const Container = props.container || 'div'
|
||||
const Button = props.useTabButton ? TabButton : ActionButton
|
||||
const Container = container || 'div'
|
||||
|
||||
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>
|
||||
)
|
||||
const Button = useTabButton ? TabButton : ActionButton_
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!noPatches
|
||||
? (
|
||||
{!noPatches ? (
|
||||
<SortedTable
|
||||
collection={hosts}
|
||||
columns={props.displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
|
||||
columns={
|
||||
displayPools
|
||||
? POOLS_MISSING_PATCHES_COLUMNS
|
||||
: MISSING_PATCHES_COLUMNS
|
||||
}
|
||||
userData={{
|
||||
installAllHostPatches: this._installAllHostPatches,
|
||||
installAllHostPatches,
|
||||
missingPatches: this.state.missingPatches,
|
||||
pools: props.pools
|
||||
pools,
|
||||
}}
|
||||
/>
|
||||
) : <p>{_('patchNothing')}</p>
|
||||
}
|
||||
<Portal container={() => props.buttonsGroupContainer()}>
|
||||
{Buttons}
|
||||
) : (
|
||||
<p>{_('patchNothing')}</p>
|
||||
)}
|
||||
<Portal container={() => buttonsGroupContainer()}>
|
||||
<Container>
|
||||
<Button
|
||||
btnStyle='primary'
|
||||
disabled={noPatches}
|
||||
handler={this._installAllMissingPatches}
|
||||
icon='host-patch-update'
|
||||
labelId='installPoolPatches'
|
||||
/>
|
||||
</Container>
|
||||
</Portal>
|
||||
</div>
|
||||
)
|
||||
@@ -189,7 +201,7 @@ class HostsPatchesTable extends Component {
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
|
||||
return {
|
||||
pools: getPools
|
||||
pools: getPools,
|
||||
}
|
||||
})
|
||||
class HostsPatchesTableByPool extends Component {
|
||||
@@ -207,10 +219,14 @@ export default propTypes({
|
||||
displayPools: propTypes.bool,
|
||||
hosts: propTypes.oneOfType([
|
||||
propTypes.arrayOf(propTypes.object),
|
||||
propTypes.objectOf(propTypes.object)
|
||||
propTypes.objectOf(propTypes.object),
|
||||
]).isRequired,
|
||||
useTabButton: propTypes.bool
|
||||
})(props => props.displayPools
|
||||
? <HostsPatchesTableByPool {...props} />
|
||||
: <HostsPatchesTable {...props} />
|
||||
useTabButton: propTypes.bool,
|
||||
})(
|
||||
props =>
|
||||
props.displayPools ? (
|
||||
<HostsPatchesTableByPool {...props} />
|
||||
) : (
|
||||
<HostsPatchesTable {...props} />
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import classNames from 'classnames'
|
||||
import isInteger from 'lodash/isInteger'
|
||||
import React, { PropTypes } from 'react'
|
||||
import React 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
|
||||
])
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const Icon = ({ icon, size = 1, color, fixedWidth, ...props }) => {
|
||||
props.className = classNames(
|
||||
props.className,
|
||||
icon !== undefined ? `xo-icon-${icon}` : 'fa', // Without icon prop, is a placeholder.
|
||||
isInteger(size) ? `fa-${size}x` : `fa-${size}`,
|
||||
color,
|
||||
fixedWidth && 'fa-fw'
|
||||
)
|
||||
|
||||
return <i {...props} />
|
||||
}
|
||||
propTypes(Icon)({
|
||||
color: propTypes.string,
|
||||
fixedWidth: propTypes.bool,
|
||||
icon: propTypes.string,
|
||||
size: propTypes.oneOfType([propTypes.string, propTypes.number]),
|
||||
})
|
||||
export default Icon
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
|
||||
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 PropTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { FormattedMessage, IntlProvider as IntlProvider_ } from 'react-intl'
|
||||
|
||||
import messages from './messages'
|
||||
import locales from './locales'
|
||||
@@ -44,10 +36,17 @@ const getMessage = (props, messageId, values, render) => {
|
||||
values = undefined
|
||||
}
|
||||
|
||||
return <FormattedMessage {...props} {...message} values={values}>
|
||||
{render}
|
||||
</FormattedMessage>
|
||||
return (
|
||||
<FormattedMessage {...props} {...message} values={values}>
|
||||
{render}
|
||||
</FormattedMessage>
|
||||
)
|
||||
}
|
||||
getMessage.keyValue = (key, value) =>
|
||||
getMessage('keyValue', {
|
||||
key: <strong>{key}</strong>,
|
||||
value,
|
||||
})
|
||||
|
||||
export { getMessage as default }
|
||||
|
||||
@@ -57,27 +56,35 @@ export { messages }
|
||||
export class IntlProvider extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
lang: PropTypes.string.isRequired
|
||||
};
|
||||
lang: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { lang, children } = this.props
|
||||
return <IntlProvider_
|
||||
locale={lang}
|
||||
messages={locales[lang]}
|
||||
>
|
||||
{children}
|
||||
</IntlProvider_>
|
||||
// Adding a key prop is a work-around suggested by react-intl documentation
|
||||
// to make sure changes to the locale trigger a re-render of the child components
|
||||
// https://github.com/yahoo/react-intl/wiki/Components#dynamic-language-selection
|
||||
//
|
||||
// FIXME: remove the key prop when React context propagation is fixed (https://github.com/facebook/react/issues/2517)
|
||||
return (
|
||||
<IntlProvider_ key={lang} 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>
|
||||
const { duration, lang } = this.props
|
||||
return (
|
||||
<span>
|
||||
{moment
|
||||
.duration(duration)
|
||||
.locale(lang)
|
||||
.humanize()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
1
src/common/intl/locales/.gitignore
vendored
1
src/common/intl/locales/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/index.js
|
||||
0
src/common/intl/locales/.index-modules
Normal file
0
src/common/intl/locales/.index-modules
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3658
src/common/intl/locales/hu.js
Normal file
3658
src/common/intl/locales/hu.js
Normal file
File diff suppressed because it is too large
Load Diff
3186
src/common/intl/locales/pl.js
Normal file
3186
src/common/intl/locales/pl.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
136
src/common/ip.js
Normal file
136
src/common/ip.js
Normal file
@@ -0,0 +1,136 @@
|
||||
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) {
|
||||
const 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) {
|
||||
const 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
|
||||
}
|
||||
@@ -1,50 +1,58 @@
|
||||
import React from 'react'
|
||||
|
||||
import _ from 'intl'
|
||||
import ActionButton from './action-button'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import Icon from 'icon'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import Tooltip from 'tooltip'
|
||||
import { alert } from 'modal'
|
||||
import { connectStore } from './utils'
|
||||
import { SelectVdi } from './select-objects'
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
createFinder,
|
||||
createGetObject,
|
||||
createSelector
|
||||
createSelector,
|
||||
} from './selectors'
|
||||
import {
|
||||
ejectCd,
|
||||
insertCd
|
||||
} from './xo'
|
||||
import { ejectCd, insertCd } from './xo'
|
||||
|
||||
@propTypes({
|
||||
vm: propTypes.object.isRequired
|
||||
vm: propTypes.object.isRequired,
|
||||
})
|
||||
@connectStore(() => {
|
||||
const getCdDrive = createFinder(
|
||||
createGetObjectsOfType('VBD').pick(
|
||||
(_, { vm }) => vm.$VBDs
|
||||
),
|
||||
[ vbd => vbd.is_cd_drive ]
|
||||
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
|
||||
}
|
||||
const getMountedIso = createGetObject((state, props) => {
|
||||
const cdDrive = getCdDrive(state, props)
|
||||
if (cdDrive) {
|
||||
return cdDrive.VDI
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return {
|
||||
cdDrive: getCdDrive,
|
||||
mountedIso: getMountedIso
|
||||
mountedIso: getMountedIso,
|
||||
}
|
||||
})
|
||||
export default class IsoDevice extends Component {
|
||||
_getPredicate = createSelector(
|
||||
() => this.props.vm.$pool,
|
||||
poolId => sr => sr.$pool === poolId && sr.SR_type === 'iso'
|
||||
() => this.props.vm.$container,
|
||||
(vmPool, vmContainer) => sr => {
|
||||
const vmRunning = vmContainer !== vmPool
|
||||
const sameHost = vmContainer === sr.$container
|
||||
const samePool = vmPool === sr.$pool
|
||||
|
||||
return (
|
||||
samePool &&
|
||||
(vmRunning ? sr.shared || sameHost : true) &&
|
||||
(sr.SR_type === 'iso' || (sr.SR_type === 'udev' && sr.size))
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
_handleInsert = iso => {
|
||||
@@ -59,25 +67,36 @@ export default class IsoDevice extends Component {
|
||||
|
||||
_handleEject = () => ejectCd(this.props.vm)
|
||||
|
||||
_showWarning = () => alert(_('cdDriveNotInstalled'), _('cdDriveInstallation'))
|
||||
|
||||
render () {
|
||||
const { mountedIso } = this.props
|
||||
const { cdDrive, 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>
|
||||
{mountedIso &&
|
||||
!cdDrive.device && (
|
||||
<Tooltip content={_('cdDriveNotInstalled')}>
|
||||
<a
|
||||
className='text-warning btn btn-link'
|
||||
onClick={this._showWarning}
|
||||
>
|
||||
<Icon icon='alarm' size='lg' />
|
||||
</a>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,15 @@
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
import map from 'lodash/map'
|
||||
import filter from 'lodash/filter'
|
||||
import React from 'react'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { filter, map } from 'lodash'
|
||||
|
||||
import _ from '../intl'
|
||||
import propTypes from '../prop-types'
|
||||
import { propsEqual } from '../utils'
|
||||
import Button from '../button'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import { EMPTY_ARRAY } 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
import { descriptionRender, forceDisplayOptionalAttr } from './helpers'
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
@@ -48,138 +18,108 @@ class ArrayItem extends Component {
|
||||
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
|
||||
@uncontrollableInput()
|
||||
export default class ObjectInput extends Component {
|
||||
state = {
|
||||
use: this.props.required || forceDisplayOptionalAttr(this.props),
|
||||
}
|
||||
|
||||
get value () {
|
||||
if (this.state.use) {
|
||||
return map(this.refs, 'value')
|
||||
}
|
||||
_onAddItem = () => {
|
||||
const { props } = this
|
||||
props.onChange((props.value || EMPTY_ARRAY).concat(undefined))
|
||||
}
|
||||
|
||||
set value (value = []) {
|
||||
this.setState({
|
||||
children: this._makeChildren({ ...this.props, value })
|
||||
})
|
||||
_onChangeItem = (value, name) => {
|
||||
const key = Number(name)
|
||||
|
||||
const { props } = this
|
||||
const newValue = (props.value || EMPTY_ARRAY).slice()
|
||||
newValue[key] = value
|
||||
props.onChange(newValue)
|
||||
}
|
||||
|
||||
_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)
|
||||
})
|
||||
}
|
||||
_onRemoveItem = key => {
|
||||
const { props } = this
|
||||
props.onChange(filter(props.value, (_, i) => i !== key))
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
props,
|
||||
state
|
||||
props: {
|
||||
depth = 0,
|
||||
disabled,
|
||||
label,
|
||||
required,
|
||||
schema,
|
||||
uiSchema,
|
||||
value = EMPTY_ARRAY,
|
||||
},
|
||||
state: { use },
|
||||
} = this
|
||||
const {
|
||||
disabled,
|
||||
schema
|
||||
} = props
|
||||
const { use } = state
|
||||
const depth = props.depth || 0
|
||||
|
||||
const childDepth = depth + 2
|
||||
const itemSchema = schema.items
|
||||
const itemUiSchema = uiSchema && uiSchema.items
|
||||
|
||||
const itemLabel = itemSchema.title || _('item')
|
||||
|
||||
return (
|
||||
<div style={{'paddingLeft': `${depth}em`}}>
|
||||
<legend>{props.label}</legend>
|
||||
<div style={{ paddingLeft: `${depth}em` }}>
|
||||
<legend>{label}</legend>
|
||||
{descriptionRender(schema.description)}
|
||||
<hr />
|
||||
{!props.required &&
|
||||
{!required && (
|
||||
<div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={disabled}
|
||||
onChange={this._handleOptionalChange}
|
||||
onChange={this.linkState('use')}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
/>{' '}
|
||||
{_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
{use &&
|
||||
<div className={'card-block'}>
|
||||
<ul style={{'paddingLeft': 0}} >
|
||||
{map(this.state.children, (child, index) =>
|
||||
cloneElement(child, { ref: index })
|
||||
)}
|
||||
)}
|
||||
{use && (
|
||||
<div className='card-block'>
|
||||
<ul style={{ paddingLeft: 0 }}>
|
||||
{map(value, (value, key) => (
|
||||
<li className='list-group-item clearfix' key={key}>
|
||||
<GenericInput
|
||||
depth={childDepth}
|
||||
disabled={disabled}
|
||||
label={itemLabel}
|
||||
name={key}
|
||||
onChange={this._onChangeItem}
|
||||
required
|
||||
schema={itemSchema}
|
||||
uiSchema={itemUiSchema}
|
||||
value={value}
|
||||
/>
|
||||
<Button
|
||||
btnStyle='danger'
|
||||
className='pull-right'
|
||||
disabled={disabled}
|
||||
name={key}
|
||||
onClick={() => this._onRemoveItem(key)}
|
||||
>
|
||||
{_('remove')}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button disabled={disabled} className='btn btn-primary pull-xs-right m-t-1 m-r-1' type='button' onClick={this._handleAdd}>
|
||||
<Button
|
||||
btnStyle='primary'
|
||||
className='pull-right mt-1 mr-1'
|
||||
disabled={disabled}
|
||||
onClick={this._onAddItem}
|
||||
>
|
||||
{_('add')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,33 +1,22 @@
|
||||
import React from 'react'
|
||||
import { Toggle } from 'form'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Component from '../base-component'
|
||||
import { Toggle } from '../form'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@uncontrollableInput()
|
||||
export default class BooleanInput extends Component {
|
||||
render () {
|
||||
const { props } = this
|
||||
const { disabled, onChange, value, ...props } = this.props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<div className='checkbox form-control'>
|
||||
<Toggle
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
/>
|
||||
<Toggle disabled={disabled} onChange={onChange} value={value} />
|
||||
</div>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -1,34 +1,54 @@
|
||||
import React from 'react'
|
||||
import _ from 'intl'
|
||||
import map from 'lodash/map'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import { createSelector } from 'reselect'
|
||||
import { findIndex, map } from 'lodash'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class EnumInput extends AbstractInput {
|
||||
@uncontrollableInput()
|
||||
export default class EnumInput extends Component {
|
||||
_getSelectedIndex = createSelector(
|
||||
() => this.props.schema.enum,
|
||||
() => {
|
||||
const { schema, value = schema.default } = this.props
|
||||
return value
|
||||
},
|
||||
(enumValues, value) => {
|
||||
const index = findIndex(enumValues, current => current === value)
|
||||
return index === -1 ? '' : index
|
||||
}
|
||||
)
|
||||
|
||||
_onChange = event => {
|
||||
this.props.onChange(this.props.schema.enum[event.target.value])
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const {
|
||||
onChange,
|
||||
required
|
||||
} = props
|
||||
disabled,
|
||||
schema: { enum: enumValues, enumNames = enumValues },
|
||||
required,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<PrimitiveInputWrapper {...this.props}>
|
||||
<select
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue || ''}
|
||||
disabled={props.disabled}
|
||||
onChange={onChange && (event => onChange(event.target.value))}
|
||||
ref='input'
|
||||
disabled={disabled}
|
||||
onChange={this._onChange}
|
||||
required={required}
|
||||
value={this._getSelectedIndex()}
|
||||
>
|
||||
{_('noSelectedValue', message => <option value=''>{message}</option>)}
|
||||
{map(props.schema.enum, (value, index) =>
|
||||
<option value={value} key={index}>{value}</option>
|
||||
)}
|
||||
{map(enumNames, (name, index) => (
|
||||
<option value={index} key={index}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { Component } from 'react'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { EMPTY_OBJECT } from '../utils'
|
||||
|
||||
import ArrayInput from './array-input'
|
||||
@@ -21,7 +23,7 @@ const InputByType = {
|
||||
integer: IntegerInput,
|
||||
number: NumberInput,
|
||||
object: ObjectInput,
|
||||
string: StringInput
|
||||
string: StringInput,
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
@@ -30,35 +32,31 @@ const InputByType = {
|
||||
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
|
||||
})
|
||||
@uncontrollableInput()
|
||||
export default class GenericInput extends Component {
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
_onChange = event => {
|
||||
const { name, onChange } = this.props
|
||||
onChange && onChange(getEventValue(event), name)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
schema,
|
||||
defaultValue = schema.default,
|
||||
value = schema.default,
|
||||
uiSchema = EMPTY_OBJECT,
|
||||
...opts
|
||||
} = this.props
|
||||
|
||||
const props = {
|
||||
...opts,
|
||||
defaultValue,
|
||||
onChange: this._onChange,
|
||||
schema,
|
||||
uiSchema,
|
||||
ref: 'input'
|
||||
value,
|
||||
}
|
||||
|
||||
// Enum, special case.
|
||||
|
||||
@@ -38,12 +38,21 @@ export const getXoType = schema => {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const descriptionRender = description =>
|
||||
<span className='text-muted' dangerouslySetInnerHTML={{__html: marked(description || '')}} />
|
||||
export const descriptionRender = description => (
|
||||
<span
|
||||
className='text-muted'
|
||||
dangerouslySetInnerHTML={{ __html: marked(description || '') }}
|
||||
/>
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const PrimitiveInputWrapper = ({ label, required = false, schema, children }) => (
|
||||
export const PrimitiveInputWrapper = ({
|
||||
label,
|
||||
required = false,
|
||||
schema,
|
||||
children,
|
||||
}) => (
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<div className='input-group'>
|
||||
@@ -54,27 +63,25 @@ export const PrimitiveInputWrapper = ({ label, required = false, schema, childre
|
||||
{children}
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
{descriptionRender(schema.description)}
|
||||
</Col>
|
||||
<Col mediumSize={6}>{descriptionRender(schema.description)}</Col>
|
||||
</Row>
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const forceDisplayOptionalAttr = ({ schema, defaultValue }) => {
|
||||
if (!schema || !defaultValue) {
|
||||
export const forceDisplayOptionalAttr = ({ schema, value }) => {
|
||||
if (!schema || !value) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Array
|
||||
if (schema.items && Array.isArray(defaultValue)) {
|
||||
if (schema.items && Array.isArray(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Object
|
||||
for (const key in schema.properties) {
|
||||
if (defaultValue[key]) {
|
||||
if (value[key]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Combobox from '../combobox'
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
|
||||
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
|
||||
@uncontrollableInput()
|
||||
export default class IntegerInput extends Component {
|
||||
_onChange = event => {
|
||||
const value = getEventValue(event)
|
||||
this.props.onChange(value ? +value : undefined)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { schema } = props
|
||||
const { required, schema } = this.props
|
||||
const {
|
||||
disabled,
|
||||
onChange, // eslint-disable-line no-unused-vars
|
||||
placeholder = schema.default,
|
||||
value,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
value={value === undefined ? '' : String(value)}
|
||||
disabled={disabled}
|
||||
max={schema.max}
|
||||
min={schema.min}
|
||||
onChange={this._onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
step={1}
|
||||
type='number'
|
||||
/>
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Combobox from '../combobox'
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
|
||||
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
|
||||
@uncontrollableInput()
|
||||
export default class NumberInput extends Component {
|
||||
_onChange = event => {
|
||||
const value = getEventValue(event)
|
||||
this.props.onChange(value ? +value : undefined)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { schema } = props
|
||||
const { required, schema } = this.props
|
||||
const {
|
||||
disabled,
|
||||
onChange, // eslint-disable-line no-unused-vars
|
||||
placeholder = schema.default,
|
||||
value,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
value={value === undefined ? '' : String(value)}
|
||||
disabled={disabled}
|
||||
max={schema.max}
|
||||
min={schema.min}
|
||||
onChange={this._onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
step='any'
|
||||
type='number'
|
||||
/>
|
||||
|
||||
@@ -1,44 +1,15 @@
|
||||
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 React from 'react'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { createSelector } from 'reselect'
|
||||
import { keyBy, map } from 'lodash'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
import { propsEqual } from '../utils'
|
||||
import _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import { EMPTY_OBJECT } 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
import { descriptionRender, forceDisplayOptionalAttr } from './helpers'
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
@@ -47,117 +18,80 @@ class ObjectItem extends Component {
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.object
|
||||
})
|
||||
@uncontrollableInput()
|
||||
export default class ObjectInput extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
use: Boolean(props.required) || forceDisplayOptionalAttr(props),
|
||||
children: this._makeChildren(props)
|
||||
}
|
||||
state = {
|
||||
use: this.props.required || forceDisplayOptionalAttr(this.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]
|
||||
_onChildChange = (value, key) => {
|
||||
this.props.onChange({
|
||||
...this.props.value,
|
||||
[key]: value,
|
||||
})
|
||||
}
|
||||
|
||||
_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)
|
||||
})
|
||||
}
|
||||
}
|
||||
_getRequiredProps = createSelector(
|
||||
() => this.props.schema.required,
|
||||
required => (required ? keyBy(required) : EMPTY_OBJECT)
|
||||
)
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const { use } = state
|
||||
const depth = props.depth || 0
|
||||
const {
|
||||
props: {
|
||||
depth = 0,
|
||||
disabled,
|
||||
label,
|
||||
required,
|
||||
schema,
|
||||
uiSchema,
|
||||
value = EMPTY_OBJECT,
|
||||
},
|
||||
state: { use },
|
||||
} = this
|
||||
|
||||
const childDepth = depth + 2
|
||||
const properties = (uiSchema != null && uiSchema.properties) || EMPTY_OBJECT
|
||||
const requiredProps = this._getRequiredProps()
|
||||
|
||||
return (
|
||||
<div style={{'paddingLeft': `${depth}em`}}>
|
||||
<legend>{props.label}</legend>
|
||||
{descriptionRender(props.schema.description)}
|
||||
<div style={{ paddingLeft: `${depth}em` }}>
|
||||
<legend>{label}</legend>
|
||||
{descriptionRender(schema.description)}
|
||||
<hr />
|
||||
{!props.required &&
|
||||
{!required && (
|
||||
<div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={props.disabled}
|
||||
onChange={this._handleOptionalChange}
|
||||
disabled={disabled}
|
||||
onChange={this.linkState('use')}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
/>{' '}
|
||||
{_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
{use &&
|
||||
)}
|
||||
{use && (
|
||||
<div className='card-block'>
|
||||
{map(state.children, (child, index) =>
|
||||
cloneElement(child, { ref: index })
|
||||
)}
|
||||
{map(schema.properties, (childSchema, key) => (
|
||||
<div className='pb-1' key={key}>
|
||||
<GenericInput
|
||||
depth={childDepth}
|
||||
disabled={disabled}
|
||||
label={childSchema.title || key}
|
||||
name={key}
|
||||
onChange={this._onChildChange}
|
||||
required={Boolean(requiredProps[key])}
|
||||
schema={childSchema}
|
||||
uiSchema={properties[key]}
|
||||
value={value[key]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,31 +1,48 @@
|
||||
import React from 'react'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import Combobox from '../combobox'
|
||||
import propTypes from '../prop-types'
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
password: propTypes.bool
|
||||
password: propTypes.bool,
|
||||
})
|
||||
export default class StringInput extends AbstractInput {
|
||||
@uncontrollableInput()
|
||||
export default class StringInput extends Component {
|
||||
// the value of this input is undefined not '' when empty to make
|
||||
// it homogenous with when the user has never touched this input
|
||||
_onChange = event => {
|
||||
const value = getEventValue(event)
|
||||
this.props.onChange(value !== '' ? value : undefined)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { schema } = props
|
||||
const { required, schema } = this.props
|
||||
const {
|
||||
disabled,
|
||||
password,
|
||||
placeholder = schema.default,
|
||||
value,
|
||||
...props
|
||||
} = this.props
|
||||
delete props.onChange
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
value={value !== undefined ? value : ''}
|
||||
disabled={disabled}
|
||||
onChange={this._onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
type={props.password && 'password'}
|
||||
placeholder={placeholder || schema.default}
|
||||
required={required}
|
||||
type={password && 'password'}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import { routerShape } from 'react-router/lib/PropTypes'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -15,15 +15,16 @@ const _IGNORED_TAGNAMES = {
|
||||
A: true,
|
||||
BUTTON: true,
|
||||
INPUT: true,
|
||||
SELECT: true
|
||||
SELECT: true,
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
tagName: propTypes.string
|
||||
className: propTypes.string,
|
||||
tagName: propTypes.string,
|
||||
})
|
||||
export class BlockLink extends Component {
|
||||
static contextTypes = {
|
||||
router: routerShape
|
||||
router: routerShape,
|
||||
}
|
||||
|
||||
_style = { cursor: 'pointer' }
|
||||
@@ -44,11 +45,22 @@ export class BlockLink extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
_addAuxClickListener = ref => {
|
||||
// FIXME: when https://github.com/facebook/react/issues/8529 is fixed,
|
||||
// remove and use onAuxClickCapture.
|
||||
// In Chrome ^55, middle-clicking triggers auxclick event instead of click
|
||||
if (ref !== null) {
|
||||
ref.addEventListener('auxclick', this._onClickCapture)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, tagName = 'div' } = this.props
|
||||
const { children, tagName = 'div', className } = this.props
|
||||
const Component = tagName
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
ref={this._addAuxClickListener}
|
||||
style={this._style}
|
||||
onClickCapture={this._onClickCapture}
|
||||
>
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import _ from 'intl'
|
||||
import Icon from 'icon'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isString from 'lodash/isString'
|
||||
import map from 'lodash/map'
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
import { Button, Modal as ReactModal } from 'react-bootstrap-4/lib'
|
||||
import { Modal as ReactModal } from 'react-bootstrap-4/lib'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import _ from './intl'
|
||||
import Button from './button'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import Tooltip from './tooltip'
|
||||
import {
|
||||
disable as disableShortcuts,
|
||||
enable as enableShortcuts,
|
||||
} from './shortcuts'
|
||||
|
||||
let instance
|
||||
|
||||
@@ -15,31 +22,100 @@ const modal = (content, onClose) => {
|
||||
} else if (instance.state.showModal) {
|
||||
throw new Error('Other modal still open.')
|
||||
}
|
||||
instance.setState({ content, onClose, showModal: true })
|
||||
instance.setState({ content, onClose, showModal: true }, disableShortcuts)
|
||||
}
|
||||
|
||||
export const alert = (title, body) => {
|
||||
return new Promise(resolve => {
|
||||
const { Body, Footer, Header, Title } = ReactModal
|
||||
modal(
|
||||
@propTypes({
|
||||
buttons: propTypes.arrayOf(
|
||||
propTypes.shape({
|
||||
btnStyle: propTypes.string,
|
||||
icon: propTypes.string,
|
||||
label: propTypes.node.isRequired,
|
||||
tooltip: propTypes.node,
|
||||
value: propTypes.any,
|
||||
})
|
||||
).isRequired,
|
||||
children: propTypes.node.isRequired,
|
||||
icon: propTypes.string,
|
||||
title: propTypes.node.isRequired,
|
||||
})
|
||||
class GenericModal extends Component {
|
||||
_getBodyValue = () => {
|
||||
const { body } = this.refs
|
||||
if (body !== undefined) {
|
||||
return body.getWrappedInstance === undefined
|
||||
? body.value
|
||||
: body.getWrappedInstance().value
|
||||
}
|
||||
}
|
||||
|
||||
_resolve = (value = this._getBodyValue()) => {
|
||||
this.props.resolve(value)
|
||||
instance.close()
|
||||
}
|
||||
|
||||
_reject = () => {
|
||||
this.props.reject()
|
||||
instance.close()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { buttons, icon, title } = this.props
|
||||
|
||||
const body = _addRef(this.props.children, 'body')
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header closeButton>
|
||||
<Title>{title}</Title>
|
||||
</Header>
|
||||
<Body>{body}</Body>
|
||||
<Footer>
|
||||
<Button bsStyle='primary' onClick={() => {
|
||||
resolve()
|
||||
instance.close()
|
||||
}}>
|
||||
{_('alertOk')}
|
||||
</Button>
|
||||
</Footer>
|
||||
</div>,
|
||||
<ReactModal.Header closeButton>
|
||||
<ReactModal.Title>
|
||||
{icon ? (
|
||||
<span>
|
||||
<Icon icon={icon} /> {title}
|
||||
</span>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</ReactModal.Title>
|
||||
</ReactModal.Header>
|
||||
<ReactModal.Body>{body}</ReactModal.Body>
|
||||
<ReactModal.Footer>
|
||||
{map(buttons, ({ label, tooltip, value, icon, ...props }, key) => {
|
||||
const button = (
|
||||
<Button onClick={() => this._resolve(value)} {...props}>
|
||||
{icon !== undefined && <Icon icon={icon} fixedWidth />}
|
||||
{label}
|
||||
</Button>
|
||||
)
|
||||
return (
|
||||
<span key={key}>
|
||||
{tooltip !== undefined ? (
|
||||
<Tooltip content={tooltip}>{button}</Tooltip>
|
||||
) : (
|
||||
button
|
||||
)}{' '}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
{this.props.reject !== undefined && (
|
||||
<Button onClick={this._reject}>{_('genericCancel')}</Button>
|
||||
)}
|
||||
</ReactModal.Footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const ALERT_BUTTONS = [{ label: _('alertOk'), value: 'ok' }]
|
||||
|
||||
export const alert = (title, body) =>
|
||||
new Promise(resolve => {
|
||||
modal(
|
||||
<GenericModal buttons={ALERT_BUTTONS} resolve={resolve} title={title}>
|
||||
{body}
|
||||
</GenericModal>,
|
||||
resolve
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const _addRef = (component, ref) => {
|
||||
if (isString(component) || isArray(component)) {
|
||||
@@ -52,75 +128,28 @@ const _addRef = (component, ref) => {
|
||||
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.value || body.getWrappedInstance && body.getWrappedInstance().value)
|
||||
instance.close()
|
||||
}
|
||||
_reject = () => {
|
||||
this.props.reject()
|
||||
instance.close()
|
||||
}
|
||||
const CONFIRM_BUTTONS = [{ btnStyle: 'primary', label: _('confirmOk') }]
|
||||
|
||||
_style = { marginRight: '0.5em' }
|
||||
export const confirm = ({ body, icon = 'alarm', title }) =>
|
||||
chooseAction({
|
||||
body,
|
||||
buttons: CONFIRM_BUTTONS,
|
||||
icon,
|
||||
title,
|
||||
})
|
||||
|
||||
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'
|
||||
}) => {
|
||||
export const chooseAction = ({ body, buttons, icon, title }) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
modal(
|
||||
<Confirm
|
||||
title={title}
|
||||
resolve={resolve}
|
||||
reject={reject}
|
||||
<GenericModal
|
||||
buttons={buttons}
|
||||
icon={icon}
|
||||
reject={reject}
|
||||
resolve={resolve}
|
||||
title={title}
|
||||
>
|
||||
{body}
|
||||
</Confirm>,
|
||||
</GenericModal>,
|
||||
reject
|
||||
)
|
||||
})
|
||||
@@ -130,18 +159,22 @@ export default class Modal extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.state = { showModal: false }
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (instance) {
|
||||
throw new Error('Modal is a singleton!')
|
||||
}
|
||||
instance = this
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.setState({ showModal: false })
|
||||
componentWillUnmount () {
|
||||
instance = undefined
|
||||
}
|
||||
|
||||
close () {
|
||||
this.setState({ showModal: false })
|
||||
this.setState({ showModal: false }, enableShortcuts)
|
||||
}
|
||||
|
||||
_onHide = () => {
|
||||
@@ -152,14 +185,8 @@ export default class Modal extends Component {
|
||||
}
|
||||
|
||||
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}>
|
||||
<ReactModal show={this.state.showModal} onHide={this._onHide}>
|
||||
{this.state.content}
|
||||
</ReactModal>
|
||||
)
|
||||
|
||||
41
src/common/no-objects.js
Normal file
41
src/common/no-objects.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
// This component returns :
|
||||
// - A loading icon when the objects are not fetched
|
||||
// - A default message if the objects are fetched and the collection is empty
|
||||
// - The children if the objects are fetched and the collection is not empty
|
||||
//
|
||||
// ```js
|
||||
// <NoObjects collection={collection} emptyMessage={message}>
|
||||
// {children}
|
||||
// </NoObjects>
|
||||
// ````
|
||||
const NoObjects = props => {
|
||||
const { collection } = props
|
||||
|
||||
if (collection == null) {
|
||||
return <img src='assets/loading.svg' alt='loading' />
|
||||
}
|
||||
|
||||
if (isEmpty(collection)) {
|
||||
return <p>{props.emptyMessage}</p>
|
||||
}
|
||||
|
||||
const { children, component: Component, ...otherProps } = props
|
||||
return children !== undefined ? (
|
||||
children(otherProps)
|
||||
) : (
|
||||
<Component {...otherProps} />
|
||||
)
|
||||
}
|
||||
|
||||
propTypes(NoObjects)({
|
||||
children: propTypes.func,
|
||||
collection: propTypes.oneOfType([propTypes.array, propTypes.object]),
|
||||
component: propTypes.func,
|
||||
emptyMessage: propTypes.node.isRequired,
|
||||
})
|
||||
export default NoObjects
|
||||
@@ -1,5 +1,10 @@
|
||||
import _ from 'intl'
|
||||
import ButtonLink from 'button-link'
|
||||
import Icon from 'icon'
|
||||
import React, { Component } from 'react'
|
||||
import ReactNotify from 'react-notify'
|
||||
import { connectStore } from 'utils'
|
||||
import { isAdmin } from 'selectors'
|
||||
|
||||
let instance
|
||||
|
||||
@@ -7,31 +12,59 @@ export let error
|
||||
export let info
|
||||
export let success
|
||||
|
||||
@connectStore({
|
||||
isAdmin,
|
||||
})
|
||||
export class Notification extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
componentDidMount () {
|
||||
if (instance) {
|
||||
throw new Error('Notification is a singleton!')
|
||||
}
|
||||
instance = this
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
instance = undefined
|
||||
}
|
||||
|
||||
// This special component never have to rerender!
|
||||
shouldComponentUpdate () {
|
||||
return false
|
||||
}
|
||||
|
||||
render () {
|
||||
return <ReactNotify ref={notification => {
|
||||
if (!notification) {
|
||||
return
|
||||
}
|
||||
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)
|
||||
}} />
|
||||
error = (title, body) =>
|
||||
notification.error(
|
||||
title,
|
||||
this.props.isAdmin ? (
|
||||
<div>
|
||||
<div>{body}</div>
|
||||
<ButtonLink
|
||||
btnStyle='danger'
|
||||
className='mt-1'
|
||||
size='small'
|
||||
to='/settings/logs'
|
||||
>
|
||||
<Icon icon='logs' /> {_('showLogs')}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
) : (
|
||||
body
|
||||
),
|
||||
6e3
|
||||
)
|
||||
info = (title, body) => notification.info(title, body, 3e3)
|
||||
success = (title, body) => notification.success(title, body, 3e3)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, { Component } from 'react'
|
||||
|
||||
@connectStore(() => {
|
||||
const object = createGetObject()
|
||||
return (state, props) => ({object: object(state, props)})
|
||||
return (state, props) => ({ object: object(state, props) })
|
||||
})
|
||||
export default class ObjectName extends Component {
|
||||
render () {
|
||||
|
||||
125
src/common/pagination.js
Normal file
125
src/common/pagination.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const PageItem = ({ active, children, disabled, onClick, value }) =>
|
||||
active ? (
|
||||
<li className='active page-item'>
|
||||
<span className='page-link'>{children}</span>
|
||||
</li>
|
||||
) : disabled ? (
|
||||
<li className='disabled page-item'>
|
||||
<span className='page-link'>{children}</span>
|
||||
</li>
|
||||
) : (
|
||||
<li className='page-item'>
|
||||
<a className='page-link' href='#' onClick={onClick} data-value={value}>
|
||||
{children}
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
|
||||
export default class Pagination extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
ellipsis: true,
|
||||
maxButtons: 7,
|
||||
next: true,
|
||||
prev: true,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
ariaLabel: PropTypes.string,
|
||||
ellipsis: PropTypes.bool,
|
||||
maxButtons: PropTypes.number,
|
||||
next: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
pages: PropTypes.number.isRequired,
|
||||
prev: PropTypes.bool,
|
||||
value: PropTypes.number.isRequired,
|
||||
}
|
||||
|
||||
_onClick (event) {
|
||||
event.preventDefault()
|
||||
this.props.onChange(+event.currentTarget.dataset.value)
|
||||
}
|
||||
_onClick = this._onClick.bind(this)
|
||||
|
||||
render () {
|
||||
const {
|
||||
ariaLabel,
|
||||
ellipsis,
|
||||
maxButtons,
|
||||
next,
|
||||
pages,
|
||||
prev,
|
||||
value,
|
||||
} = this.props
|
||||
const onClick = this._onClick
|
||||
|
||||
let min, max
|
||||
if (pages <= maxButtons) {
|
||||
min = 1
|
||||
max = pages
|
||||
} else {
|
||||
min = Math.max(
|
||||
1,
|
||||
Math.min(value - Math.floor(maxButtons / 2), pages - maxButtons + 1)
|
||||
)
|
||||
max = min + maxButtons - 1
|
||||
}
|
||||
|
||||
const pageButtons = []
|
||||
if (ellipsis && min !== 1) {
|
||||
pageButtons.push(
|
||||
<PageItem disabled key='firstEllipsis'>
|
||||
…
|
||||
</PageItem>
|
||||
)
|
||||
}
|
||||
for (let page = min; page <= max; ++page) {
|
||||
pageButtons.push(
|
||||
<PageItem
|
||||
active={page === value}
|
||||
key={page}
|
||||
onClick={onClick}
|
||||
value={page}
|
||||
>
|
||||
{page}
|
||||
</PageItem>
|
||||
)
|
||||
}
|
||||
if (ellipsis && max !== pages) {
|
||||
pageButtons.push(
|
||||
<PageItem disabled key='lastEllipsis'>
|
||||
…
|
||||
</PageItem>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<nav aria-label={ariaLabel}>
|
||||
<ul className='pagination'>
|
||||
{prev && (
|
||||
<PageItem
|
||||
aria-label='Previous'
|
||||
disabled={value === 1}
|
||||
onClick={onClick}
|
||||
value={value - 1}
|
||||
>
|
||||
‹
|
||||
</PageItem>
|
||||
)}
|
||||
{pageButtons}
|
||||
{next && (
|
||||
<PageItem
|
||||
aria-label='Next'
|
||||
disabled={value === pages}
|
||||
onClick={onClick}
|
||||
value={value + 1}
|
||||
>
|
||||
›
|
||||
</PageItem>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
}
|
||||
45
src/common/prop-types-decorator.js
Normal file
45
src/common/prop-types-decorator.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import assign from 'lodash/assign'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// Deprecated because :
|
||||
// - unnecessary
|
||||
// - not standard in the React ecosystem
|
||||
if (__DEV__) {
|
||||
console.warn(`DEPRECATED: use prop-types directly:
|
||||
class MyComponent extends React.Component {
|
||||
static propTypes = {
|
||||
foo: PropTypes.string.isRequired
|
||||
}
|
||||
}`)
|
||||
}
|
||||
|
||||
// Decorators to help declaring properties and context types on React
|
||||
// components without using the tedious static properties syntax.
|
||||
//
|
||||
// ```js
|
||||
// @propTypes({
|
||||
// children: propTypes.node.isRequired
|
||||
// }, {
|
||||
// store: propTypes.object.isRequired
|
||||
// })
|
||||
// class MyComponent extends React.Component {}
|
||||
// ```
|
||||
const propTypes = (propTypes, contextTypes) => target => {
|
||||
if (propTypes !== undefined) {
|
||||
target.propTypes = {
|
||||
...target.propTypes,
|
||||
...propTypes,
|
||||
}
|
||||
}
|
||||
if (contextTypes !== undefined) {
|
||||
target.contextTypes = {
|
||||
...target.contextTypes,
|
||||
...contextTypes,
|
||||
}
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
assign(propTypes, PropTypes)
|
||||
|
||||
export { propTypes as default }
|
||||
@@ -1,22 +0,0 @@
|
||||
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 }
|
||||
91
src/common/react-novnc.js
vendored
91
src/common/react-novnc.js
vendored
@@ -1,19 +1,17 @@
|
||||
import React, { Component } from 'react'
|
||||
import RFB from '@nraynaud/novnc/lib/rfb'
|
||||
import URL from 'url-parse'
|
||||
import { createBackoff } from 'jsonrpc-websocket-client'
|
||||
import { RFB } from 'novnc-node'
|
||||
import {
|
||||
format as formatUrl,
|
||||
parse as parseUrl,
|
||||
resolve as resolveUrl
|
||||
} from 'url'
|
||||
enable as enableShortcuts,
|
||||
disable as disableShortcuts,
|
||||
} from 'shortcuts'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
|
||||
const parseRelativeUrl = url => parseUrl(resolveUrl(String(window.location), url))
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const PROTOCOL_ALIASES = {
|
||||
'http:': 'ws:',
|
||||
'https:': 'wss:'
|
||||
'https:': 'wss:',
|
||||
}
|
||||
const fixProtocol = url => {
|
||||
const protocol = PROTOCOL_ALIASES[url.protocol]
|
||||
@@ -24,7 +22,7 @@ const fixProtocol = url => {
|
||||
|
||||
@propTypes({
|
||||
onClipboardChange: propTypes.func,
|
||||
url: propTypes.string.isRequired
|
||||
url: propTypes.string.isRequired,
|
||||
})
|
||||
export default class NoVnc extends Component {
|
||||
constructor (props) {
|
||||
@@ -41,12 +39,15 @@ export default class NoVnc extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (state !== 'disconnected') {
|
||||
if (state !== 'disconnected' || this.refs.canvas == null) {
|
||||
return
|
||||
}
|
||||
|
||||
clearTimeout(this._retryTimeout)
|
||||
this._retryTimeout = setTimeout(this._connect, this._retryGen.next().value)
|
||||
this._retryTimeout = setTimeout(
|
||||
this._connect,
|
||||
this._retryGen.next().value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,28 +71,46 @@ export default class NoVnc extends Component {
|
||||
this._rfb = null
|
||||
rfb.disconnect()
|
||||
}
|
||||
enableShortcuts()
|
||||
}
|
||||
|
||||
_connect = () => {
|
||||
this._clean()
|
||||
|
||||
const url = parseRelativeUrl(this.props.url)
|
||||
const { canvas } = this.refs
|
||||
if (!canvas) {
|
||||
return
|
||||
}
|
||||
|
||||
const url = new URL(this.props.url)
|
||||
fixProtocol(url)
|
||||
|
||||
const isSecure = url.protocol === 'wss:'
|
||||
|
||||
const { onClipboardChange } = this.props
|
||||
const rfb = this._rfb = new RFB({
|
||||
const rfb = (this._rfb = new RFB({
|
||||
encrypt: isSecure,
|
||||
target: this.refs.canvas,
|
||||
wsProtocols: [ 'chat' ],
|
||||
onClipboard: onClipboardChange && ((_, text) => {
|
||||
onClipboardChange(text)
|
||||
}),
|
||||
onUpdateState: this._onUpdateState
|
||||
})
|
||||
onClipboard:
|
||||
onClipboardChange &&
|
||||
((_, text) => {
|
||||
onClipboardChange(text)
|
||||
}),
|
||||
onUpdateState: this._onUpdateState,
|
||||
}))
|
||||
|
||||
rfb.connect(formatUrl(url))
|
||||
// remove leading slashes from the path
|
||||
//
|
||||
// a leading slassh will be added by noVNC
|
||||
const clippedPath = url.pathname.replace(/^\/+/, '')
|
||||
|
||||
// a port is required
|
||||
//
|
||||
// if not available from the URL, use the default ones
|
||||
const port = url.port || (isSecure ? 443 : 80)
|
||||
|
||||
rfb.connect(url.hostname, port, null, clippedPath)
|
||||
disableShortcuts()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@@ -102,6 +121,14 @@ export default class NoVnc extends Component {
|
||||
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) {
|
||||
@@ -112,6 +139,8 @@ export default class NoVnc extends Component {
|
||||
|
||||
rfb.get_keyboard().grab()
|
||||
rfb.get_mouse().grab()
|
||||
|
||||
disableShortcuts()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,17 +149,21 @@ export default class NoVnc extends Component {
|
||||
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'
|
||||
/>
|
||||
return (
|
||||
<canvas
|
||||
className='center-block'
|
||||
height='480'
|
||||
onMouseEnter={this._focus}
|
||||
onMouseLeave={this._unfocus}
|
||||
ref='canvas'
|
||||
width='640'
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,88 +1,95 @@
|
||||
import React, { Component } from 'react'
|
||||
import _ from 'intl'
|
||||
import React from 'react'
|
||||
import { startsWith } from 'lodash'
|
||||
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import { createGetObject } from './selectors'
|
||||
import { isSrWritable } from './xo'
|
||||
import {
|
||||
connectStore,
|
||||
formatSize
|
||||
} from './utils'
|
||||
import { connectStore, formatSize } from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const OBJECT_TYPE_TO_ICON = {
|
||||
'VM-template': 'vm',
|
||||
host: 'host',
|
||||
network: 'network'
|
||||
network: 'network',
|
||||
}
|
||||
|
||||
// Host, Network, VM-template.
|
||||
export const PoolObjectItem = propTypes({
|
||||
object: propTypes.object.isRequired
|
||||
})(connectStore(() => {
|
||||
const getPool = createGetObject(
|
||||
(_, props) => props.object.$pool
|
||||
)
|
||||
const PoolObjectItem = propTypes({
|
||||
object: propTypes.object.isRequired,
|
||||
})(
|
||||
connectStore(() => {
|
||||
const getPool = createGetObject((_, props) => props.object.$pool)
|
||||
|
||||
return (state, props) => ({
|
||||
pool: getPool(state, props)
|
||||
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>
|
||||
)
|
||||
})
|
||||
})(({ 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
|
||||
)
|
||||
const SrItem = propTypes({
|
||||
sr: propTypes.object.isRequired,
|
||||
})(
|
||||
connectStore(() => {
|
||||
const getContainer = createGetObject((_, props) => props.sr.$container)
|
||||
|
||||
return (state, props) => ({
|
||||
container: getContainer(state, props)
|
||||
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>
|
||||
)
|
||||
})
|
||||
})(({ sr, container }) => {
|
||||
let label = `${sr.name_label || sr.id}`
|
||||
|
||||
if (isSrWritable(sr)) {
|
||||
label += ` (${formatSize(sr.size)})`
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Icon icon='sr' /> {label}
|
||||
{container && ` (${container.name_label || container.id})`}
|
||||
</span>
|
||||
)
|
||||
}))
|
||||
)
|
||||
|
||||
// VM.
|
||||
export const VmItem = propTypes({
|
||||
vm: propTypes.object.isRequired
|
||||
})(connectStore(() => {
|
||||
const getContainer = createGetObject(
|
||||
(_, props) => props.vm.$container
|
||||
)
|
||||
const VmItem = propTypes({
|
||||
vm: propTypes.object.isRequired,
|
||||
})(
|
||||
connectStore(() => {
|
||||
const getContainer = createGetObject((_, props) => props.vm.$container)
|
||||
|
||||
return (state, props) => ({
|
||||
container: getContainer(state, 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 VgpuItem = connectStore(() => ({
|
||||
vgpuType: createGetObject((_, props) => props.vgpu.vgpuType),
|
||||
}))(({ vgpu, vgpuType }) => (
|
||||
<span>
|
||||
<Icon icon={`vm-${vm.power_state.toLowerCase()}`} /> {vm.name_label || vm.id}
|
||||
{container && ` (${container.name_label || container.id})`}
|
||||
<Icon icon='vgpu' /> {vgpuType.modelName}
|
||||
</span>
|
||||
)))
|
||||
))
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -98,11 +105,7 @@ const xoItemToRender = {
|
||||
<Icon icon='remote' /> {remote.value.name}
|
||||
</span>
|
||||
),
|
||||
role: role => (
|
||||
<span>
|
||||
{role.name}
|
||||
</span>
|
||||
),
|
||||
role: role => <span>{role.name}</span>,
|
||||
user: user => (
|
||||
<span>
|
||||
<Icon icon='user' /> {user.email}
|
||||
@@ -113,6 +116,22 @@ const xoItemToRender = {
|
||||
<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 => (
|
||||
@@ -123,7 +142,8 @@ const xoItemToRender = {
|
||||
|
||||
VDI: vdi => (
|
||||
<span>
|
||||
<Icon icon='disk' /> {vdi.name_label} {vdi.name_description && <span> ({vdi.name_description})</span>}
|
||||
<Icon icon='disk' /> {vdi.name_label}{' '}
|
||||
{vdi.name_description && <span> ({vdi.name_description})</span>}
|
||||
</span>
|
||||
),
|
||||
|
||||
@@ -140,16 +160,18 @@ const xoItemToRender = {
|
||||
'VM-snapshot': vm => <VmItem vm={vm} />,
|
||||
'VM-controller': vm => (
|
||||
<span>
|
||||
<Icon icon='host' />
|
||||
{' '}
|
||||
<VmItem vm={vm} />
|
||||
<Icon icon='host' /> <VmItem vm={vm} />
|
||||
</span>
|
||||
),
|
||||
|
||||
// PIF.
|
||||
PIF: pif => (
|
||||
<span>
|
||||
<Icon icon='network' /> {pif.device}
|
||||
<Icon
|
||||
icon='network'
|
||||
color={pif.carrier ? 'text-success' : 'text-danger'}
|
||||
/>{' '}
|
||||
{pif.device} ({pif.deviceName})
|
||||
</span>
|
||||
),
|
||||
|
||||
@@ -158,15 +180,44 @@ const xoItemToRender = {
|
||||
<span>
|
||||
<Icon icon='tag' /> {tag.value}
|
||||
</span>
|
||||
)
|
||||
),
|
||||
|
||||
// GPUs
|
||||
|
||||
vgpu: vgpu => <VgpuItem vgpu={vgpu} />,
|
||||
|
||||
vgpuType: type => (
|
||||
<span>
|
||||
<Icon icon='gpu' /> {type.modelName} ({type.vendorName}){' '}
|
||||
{type.maxResolutionX}x{type.maxResolutionY}
|
||||
</span>
|
||||
),
|
||||
|
||||
gpuGroup: group => (
|
||||
<span>
|
||||
{startsWith(group.name_label, 'Group of ')
|
||||
? group.name_label.slice(9)
|
||||
: group.name_label}
|
||||
</span>
|
||||
),
|
||||
}
|
||||
|
||||
const renderXoItem = (item, {
|
||||
className
|
||||
} = {}) => {
|
||||
const renderXoItem = (item, { className } = {}) => {
|
||||
const { id, type, label } = item
|
||||
|
||||
if (!type && label) {
|
||||
if (item.removed) {
|
||||
return (
|
||||
<span key={id} className='text-danger'>
|
||||
{' '}
|
||||
<Icon icon='alarm' /> {id}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
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}
|
||||
@@ -195,11 +246,17 @@ const GenericXoItem = connectStore(() => {
|
||||
const getObject = createGetObject()
|
||||
|
||||
return (state, props) => ({
|
||||
xoItem: getObject(state, props)
|
||||
xoItem: getObject(state, props),
|
||||
})
|
||||
})(({ xoItem, ...props }) => xoItem
|
||||
? renderXoItem(xoItem, props)
|
||||
: <span className='text-muted'>no such item</span>
|
||||
})(
|
||||
({ xoItem, ...props }) =>
|
||||
xoItem ? renderXoItem(xoItem, props) : renderXoUnknownItem()
|
||||
)
|
||||
|
||||
export const renderXoItemFromId = (id, props) => <GenericXoItem {...props} id={id} />
|
||||
export const renderXoItemFromId = (id, props) => (
|
||||
<GenericXoItem {...props} id={id} />
|
||||
)
|
||||
|
||||
export const renderXoUnknownItem = () => (
|
||||
<span className='text-muted'>{_('errorNoSuchItem')}</span>
|
||||
)
|
||||
|
||||
@@ -1,40 +1,43 @@
|
||||
import includes from 'lodash/includes'
|
||||
import join from 'lodash/join'
|
||||
import classNames from 'classnames'
|
||||
import later from 'later'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import sortedIndex from 'lodash/sortedIndex'
|
||||
import { FormattedTime } from 'react-intl'
|
||||
import {
|
||||
Tab,
|
||||
Tabs
|
||||
} from 'react-bootstrap-4/lib'
|
||||
import { FormattedDate, FormattedTime } from 'react-intl'
|
||||
import { forEach, includes, isArray, map, sortedIndex } from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import TimezonePicker from './timezone-picker'
|
||||
import Icon from './icon'
|
||||
import Tooltip from './tooltip'
|
||||
import { Card, CardHeader, CardBlock } from './card'
|
||||
import { Col, Row } from './grid'
|
||||
import { Range } from './form'
|
||||
import { Range, Toggle } from './form'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// By default later use UTC but we use this line for futures versions.
|
||||
// By default, later uses UTC but we use this line for future versions.
|
||||
later.date.UTC()
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const NAV_EACH_SELECTED = 1
|
||||
const NAV_EVERY_N = 2
|
||||
const CLICKABLE = { cursor: 'pointer' }
|
||||
const PREVIEW_SLIDER_STYLE = { width: '400px' }
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const UNITS = ['minute', 'hour', 'monthDay', 'month', 'weekDay']
|
||||
|
||||
const MINUTES_RANGE = [2, 30]
|
||||
const HOURS_RANGE = [2, 12]
|
||||
const MONTH_DAYS_RANGE = [2, 15]
|
||||
const MONTHS_RANGE = [2, 6]
|
||||
|
||||
const MIN_PREVIEWS = 5
|
||||
const MAX_PREVIEWS = 20
|
||||
|
||||
const MONTHS = [
|
||||
[ 0, 1, 2, 3, 4, 5 ],
|
||||
[ 6, 7, 8, 9, 10, 11 ]
|
||||
]
|
||||
const MONTHS = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]]
|
||||
|
||||
const DAYS = (() => {
|
||||
const days = []
|
||||
@@ -52,16 +55,16 @@ const DAYS = (() => {
|
||||
return days
|
||||
})()
|
||||
|
||||
const WEEK_DAYS = [[ 0, 1, 2, 3, 4, 5, 6 ]]
|
||||
const WEEK_DAYS = [[0, 1, 2], [3, 4, 5], [6]]
|
||||
|
||||
const HOURS = (() => {
|
||||
const hours = []
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
hours[i] = []
|
||||
|
||||
for (let j = 0; j < 8; j++) {
|
||||
hours[i].push(8 * i + j)
|
||||
for (let j = 0; j < 6; j++) {
|
||||
hours[i].push(6 * i + j)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +90,7 @@ const PICKTIME_TO_ID = {
|
||||
hour: 1,
|
||||
monthDay: 2,
|
||||
month: 3,
|
||||
weekDay: 4
|
||||
weekDay: 4,
|
||||
}
|
||||
|
||||
const TIME_FORMAT = {
|
||||
@@ -104,44 +107,58 @@ const TIME_FORMAT = {
|
||||
|
||||
// Therefore we can use UTC everywhere and say to the user that the
|
||||
// previews are in the configured timezone.
|
||||
timeZone: 'UTC'
|
||||
timeZone: 'UTC',
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// monthNum: [ 0 : 11 ]
|
||||
const getMonthName = (monthNum) =>
|
||||
<FormattedTime value={new Date(1970, monthNum)} month='long' />
|
||||
const getMonthName = monthNum => (
|
||||
<FormattedDate value={Date.UTC(1970, monthNum)} month='long' timeZone='UTC' />
|
||||
)
|
||||
|
||||
// dayNum: [ 0 : 6 ]
|
||||
const getDayName = (dayNum) =>
|
||||
const getDayName = dayNum => (
|
||||
// January, 1970, 5th => Monday
|
||||
<FormattedTime value={new Date(1970, 0, 4 + dayNum)} weekday='long' />
|
||||
<FormattedDate
|
||||
value={Date.UTC(1970, 0, 4 + dayNum)}
|
||||
weekday='long'
|
||||
timeZone='UTC'
|
||||
/>
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
cronPattern: propTypes.string.isRequired
|
||||
cronPattern: propTypes.string.isRequired,
|
||||
})
|
||||
export class SchedulePreview extends Component {
|
||||
_handleChange = value => {
|
||||
this.setState({
|
||||
value
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { cronPattern } = this.props
|
||||
const { value } = this.state
|
||||
|
||||
const cronSched = later.parse.cron(cronPattern)
|
||||
const dates = later.schedule(cronSched).next(this.state.value || MIN_PREVIEWS)
|
||||
|
||||
// Due to implementation, the range used for months is 0-11
|
||||
// instead of 1-12
|
||||
forEach(cronSched.schedules[0].M, (v, i, a) => {
|
||||
a[i] = v + 1
|
||||
})
|
||||
|
||||
const dates = later.schedule(cronSched).next(value)
|
||||
|
||||
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 className='mb-1' style={PREVIEW_SLIDER_STYLE}>
|
||||
<Range
|
||||
min={MIN_PREVIEWS}
|
||||
max={MAX_PREVIEWS}
|
||||
onChange={this.linkState('value')}
|
||||
value={+value}
|
||||
/>
|
||||
</div>
|
||||
<ul className='list-group'>
|
||||
{map(dates, (date, id) => (
|
||||
@@ -162,7 +179,7 @@ export class SchedulePreview extends Component {
|
||||
children: propTypes.any.isRequired,
|
||||
onChange: propTypes.func.isRequired,
|
||||
tdId: propTypes.number.isRequired,
|
||||
value: propTypes.bool.isRequired
|
||||
value: propTypes.bool.isRequired,
|
||||
})
|
||||
class ToggleTd extends Component {
|
||||
_onClick = () => {
|
||||
@@ -173,7 +190,11 @@ class ToggleTd extends Component {
|
||||
render () {
|
||||
const { props } = this
|
||||
return (
|
||||
<td style={{ cursor: 'pointer' }} className={props.value ? 'table-success' : ''} onClick={this._onClick}>
|
||||
<td
|
||||
className={classNames('text-xs-center', props.value && 'table-success')}
|
||||
onClick={this._onClick}
|
||||
style={CLICKABLE}
|
||||
>
|
||||
{props.children}
|
||||
</td>
|
||||
)
|
||||
@@ -183,14 +204,15 @@ class ToggleTd extends Component {
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
labelId: propTypes.string.isRequired,
|
||||
options: propTypes.array.isRequired,
|
||||
optionsRenderer: propTypes.func,
|
||||
optionRenderer: propTypes.func,
|
||||
onChange: propTypes.func.isRequired,
|
||||
value: propTypes.array.isRequired
|
||||
value: propTypes.array.isRequired,
|
||||
})
|
||||
class TableSelect extends Component {
|
||||
static defaultProps = {
|
||||
optionsRenderer: value => value
|
||||
optionRenderer: value => value,
|
||||
}
|
||||
|
||||
_reset = () => {
|
||||
@@ -219,12 +241,7 @@ class TableSelect extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
options,
|
||||
optionsRenderer,
|
||||
value
|
||||
} = this.props
|
||||
const { length } = options[0]
|
||||
const { labelId, options, optionRenderer, value } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -232,25 +249,23 @@ class TableSelect extends Component {
|
||||
<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)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{map(line, tdOption => (
|
||||
<ToggleTd
|
||||
children={optionRenderer(tdOption)}
|
||||
tdId={tdOption}
|
||||
key={tdOption}
|
||||
onChange={this._handleChange}
|
||||
value={includes(value, tdOption)}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className='btn btn-secondary pull-xs-right' onClick={this._reset}>
|
||||
{_('selectTableReset')}
|
||||
</button>
|
||||
<Button className='pull-right' onClick={this._reset}>
|
||||
{_(`selectTableAll${labelId}`)}{' '}
|
||||
{value && !value.length && <Icon icon='success' />}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -258,219 +273,296 @@ class TableSelect extends Component {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// "2,7" => [2,7] "*/2" => 2 "*" => []
|
||||
const cronToValue = (cron, range) => {
|
||||
if (cron.indexOf('/') === 1) {
|
||||
return +cron.split('/')[1]
|
||||
}
|
||||
|
||||
if (cron === '*') {
|
||||
return []
|
||||
}
|
||||
|
||||
return map(cron.split(','), Number)
|
||||
}
|
||||
|
||||
// [2,7] => "2,7" 2 => "*/2" [] => "*"
|
||||
const valueToCron = value => {
|
||||
if (!isArray(value)) {
|
||||
return `*/${value}`
|
||||
}
|
||||
|
||||
if (!value.length) {
|
||||
return '*'
|
||||
}
|
||||
|
||||
return value.join(',')
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
optionsRenderer: propTypes.func,
|
||||
headerAddon: propTypes.node,
|
||||
optionRenderer: 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
|
||||
}
|
||||
_update = cron => {
|
||||
const { tableValue, rangeValue } = this.state
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {
|
||||
activeKey: NAV_EACH_SELECTED,
|
||||
tableValue: []
|
||||
}
|
||||
}
|
||||
const newValue = cronToValue(cron)
|
||||
const periodic = !isArray(newValue)
|
||||
|
||||
_update (props) {
|
||||
const { refs } = this
|
||||
const { value, valueRenderer } = props
|
||||
|
||||
if (value.indexOf('/') === 1) {
|
||||
this.setState({
|
||||
activeKey: NAV_EVERY_N
|
||||
})
|
||||
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)
|
||||
}
|
||||
periodic,
|
||||
tableValue: periodic ? tableValue : newValue,
|
||||
rangeValue: periodic ? newValue : rangeValue,
|
||||
})
|
||||
}
|
||||
|
||||
_handleTableValue = tableValue => {
|
||||
this.setState({
|
||||
tableValue
|
||||
}, () => this.props.onChange(tableValue))
|
||||
componentWillReceiveProps (props) {
|
||||
if (props.value !== this.props.value) {
|
||||
this._update(props.value)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
onChange,
|
||||
options,
|
||||
optionsRenderer,
|
||||
range,
|
||||
labelId
|
||||
} = this.props
|
||||
const { tableValue } = this.state
|
||||
componentDidMount () {
|
||||
this._update(this.props.value)
|
||||
}
|
||||
|
||||
const tableSelect = (
|
||||
<TableSelect
|
||||
onChange={this._handleTableValue}
|
||||
options={options}
|
||||
optionsRenderer={optionsRenderer}
|
||||
value={tableValue}
|
||||
/>
|
||||
)
|
||||
_onChange = value => {
|
||||
this.props.onChange(valueToCron(value))
|
||||
}
|
||||
|
||||
_tableTab = () => this._onChange(this.state.tableValue || [])
|
||||
_periodicTab = () =>
|
||||
this._onChange(this.state.rangeValue || this.props.range[0])
|
||||
|
||||
render () {
|
||||
const { headerAddon, labelId, options, optionRenderer, range } = this.props
|
||||
|
||||
const { periodic, tableValue, rangeValue } = this.state
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{_(`scheduling${labelId}`)}
|
||||
{headerAddon}
|
||||
</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
|
||||
}
|
||||
{range && (
|
||||
<ul className='nav nav-tabs mb-1'>
|
||||
<li className='nav-item'>
|
||||
<a
|
||||
onClick={this._tableTab}
|
||||
className={classNames('nav-link', !periodic && 'active')}
|
||||
style={CLICKABLE}
|
||||
>
|
||||
{_(`schedulingEachSelected${labelId}`)}
|
||||
</a>
|
||||
</li>
|
||||
<li className='nav-item'>
|
||||
<a
|
||||
onClick={this._periodicTab}
|
||||
className={classNames('nav-link', periodic && 'active')}
|
||||
style={CLICKABLE}
|
||||
>
|
||||
{_(`schedulingEveryN${labelId}`)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
{periodic ? (
|
||||
<Range
|
||||
ref='range'
|
||||
min={range[0]}
|
||||
max={range[1]}
|
||||
onChange={this._onChange}
|
||||
value={rangeValue}
|
||||
/>
|
||||
) : (
|
||||
<TableSelect
|
||||
labelId={labelId}
|
||||
onChange={this._onChange}
|
||||
options={options}
|
||||
optionRenderer={optionRenderer}
|
||||
value={tableValue || []}
|
||||
/>
|
||||
)}
|
||||
</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
|
||||
})
|
||||
const isWeekDayMode = ({ monthDayPattern, weekDayPattern }) => {
|
||||
if (monthDayPattern === '*' && weekDayPattern === '*') {
|
||||
return
|
||||
}
|
||||
|
||||
_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)
|
||||
return weekDayPattern !== '*'
|
||||
}
|
||||
|
||||
_onTimezoneChange = timezone => {
|
||||
const { props } = this
|
||||
props.onChange({
|
||||
cronPattern: props.cronPattern,
|
||||
timezone
|
||||
})
|
||||
@propTypes({
|
||||
monthDayPattern: propTypes.string.isRequired,
|
||||
weekDayPattern: propTypes.string.isRequired,
|
||||
})
|
||||
class DayPicker extends Component {
|
||||
state = {
|
||||
weekDayMode: isWeekDayMode(this.props),
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
const weekDayMode = isWeekDayMode(props)
|
||||
|
||||
if (weekDayMode !== undefined) {
|
||||
this.setState({ weekDayMode })
|
||||
}
|
||||
}
|
||||
|
||||
_setWeekDayMode = weekDayMode => {
|
||||
this.props.onChange(['*', '*'])
|
||||
this.setState({ weekDayMode })
|
||||
}
|
||||
|
||||
_onChange = cron => {
|
||||
const isMonthDayPattern = !this.state.weekDayMode || includes(cron, '/')
|
||||
|
||||
this.props.onChange([
|
||||
isMonthDayPattern ? cron : '*',
|
||||
isMonthDayPattern ? '*' : cron,
|
||||
])
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
cronPattern,
|
||||
timezone
|
||||
} = this.props
|
||||
const cronPatternArr = cronPattern.split(' ')
|
||||
const { monthDayPattern, weekDayPattern } = this.props
|
||||
const { weekDayMode } = this.state
|
||||
|
||||
const dayModeToggle = (
|
||||
<Tooltip
|
||||
content={_(
|
||||
weekDayMode ? 'schedulingSetMonthDayMode' : 'schedulingSetWeekDayMode'
|
||||
)}
|
||||
>
|
||||
<span className='pull-right'>
|
||||
<Toggle
|
||||
onChange={this._setWeekDayMode}
|
||||
iconSize={1}
|
||||
value={weekDayMode}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
return (
|
||||
<TimePicker
|
||||
headerAddon={dayModeToggle}
|
||||
key={weekDayMode ? 'week' : 'month'}
|
||||
labelId='Day'
|
||||
optionRenderer={weekDayMode ? getDayName : undefined}
|
||||
options={weekDayMode ? WEEK_DAYS : DAYS}
|
||||
onChange={this._onChange}
|
||||
range={MONTH_DAYS_RANGE}
|
||||
setWeekDayMode={this._setWeekDayMode}
|
||||
value={weekDayMode ? weekDayPattern : monthDayPattern}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
cronPattern: propTypes.string,
|
||||
onChange: propTypes.func,
|
||||
timezone: propTypes.string,
|
||||
value: propTypes.shape({
|
||||
cronPattern: propTypes.string.isRequired,
|
||||
timezone: propTypes.string,
|
||||
}),
|
||||
})
|
||||
export default class Scheduler extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this._onCronChange = newCrons => {
|
||||
const cronPattern = this._getCronPattern().split(' ')
|
||||
forEach(newCrons, (cron, unit) => {
|
||||
cronPattern[PICKTIME_TO_ID[unit]] = cron
|
||||
})
|
||||
|
||||
this.props.onChange({
|
||||
cronPattern: cronPattern.join(' '),
|
||||
timezone: this._getTimezone(),
|
||||
})
|
||||
}
|
||||
|
||||
forEach(UNITS, unit => {
|
||||
this[`_${unit}Change`] = cron => this._onCronChange({ [unit]: cron })
|
||||
})
|
||||
this._dayChange = ([monthDay, weekDay]) =>
|
||||
this._onCronChange({ monthDay, weekDay })
|
||||
}
|
||||
|
||||
_onTimezoneChange = timezone => {
|
||||
this.props.onChange({
|
||||
cronPattern: this._getCronPattern(),
|
||||
timezone,
|
||||
})
|
||||
}
|
||||
|
||||
_getCronPattern = () => {
|
||||
const { value, cronPattern = value.cronPattern } = this.props
|
||||
return cronPattern
|
||||
}
|
||||
|
||||
_getTimezone = () => {
|
||||
const { value, timezone = value && value.timezone } = this.props
|
||||
return timezone
|
||||
}
|
||||
|
||||
render () {
|
||||
const cronPatternArr = this._getCronPattern().split(' ')
|
||||
const timezone = this._getTimezone()
|
||||
|
||||
return (
|
||||
<div className='card-block'>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<Col largeSize={6}>
|
||||
<TimePicker
|
||||
labelId='Month'
|
||||
optionsRenderer={getMonthName}
|
||||
optionRenderer={getMonthName}
|
||||
options={MONTHS}
|
||||
onChange={this._onMonthChange}
|
||||
onChange={this._monthChange}
|
||||
range={MONTHS_RANGE}
|
||||
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}>
|
||||
<Col largeSize={6}>
|
||||
<DayPicker
|
||||
onChange={this._dayChange}
|
||||
monthDayPattern={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
|
||||
weekDayPattern={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col largeSize={6}>
|
||||
<TimePicker
|
||||
labelId='Hour'
|
||||
options={HOURS}
|
||||
range={HOURS_RANGE}
|
||||
onChange={this._onHourChange}
|
||||
onChange={this._hourChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['hour']]}
|
||||
/>
|
||||
</Col>
|
||||
<Col largeSize={6}>
|
||||
<TimePicker
|
||||
labelId='Minute'
|
||||
options={MINS}
|
||||
range={MINUTES_RANGE}
|
||||
onChange={this._onMinuteChange}
|
||||
onChange={this._minuteChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['minute']]}
|
||||
/>
|
||||
</Col>
|
||||
@@ -478,7 +570,10 @@ export default class Scheduler extends Component {
|
||||
<Row>
|
||||
<Col>
|
||||
<hr />
|
||||
<TimezonePicker value={timezone} onChange={this._onTimezoneChange} />
|
||||
<TimezonePicker
|
||||
value={timezone}
|
||||
onChange={this._onTimezoneChange}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
34
src/common/select-files.js
Normal file
34
src/common/select-files.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
@propTypes({
|
||||
multi: propTypes.bool,
|
||||
label: propTypes.node,
|
||||
onChange: propTypes.func.isRequired,
|
||||
})
|
||||
export default class SelectFiles extends Component {
|
||||
_onChange = e => {
|
||||
const { multi, onChange } = this.props
|
||||
const { files } = e.target
|
||||
|
||||
onChange(multi ? files : files[0])
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<label className='btn btn-secondary btn-file hidden'>
|
||||
<Icon icon='file' /> {this.props.label || _('browseFiles')}
|
||||
<input
|
||||
{...omit(this.props, ['hidden', 'label', 'onChange', 'multi'])}
|
||||
hidden
|
||||
onChange={this._onChange}
|
||||
type='file'
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,21 @@
|
||||
import add from 'lodash/add'
|
||||
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 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 {
|
||||
filter,
|
||||
find,
|
||||
forEach,
|
||||
groupBy,
|
||||
isArray,
|
||||
isArrayLike,
|
||||
isFunction,
|
||||
keys,
|
||||
map,
|
||||
orderBy,
|
||||
pickBy,
|
||||
size,
|
||||
slice,
|
||||
} from 'lodash'
|
||||
|
||||
import invoke from './invoke'
|
||||
import shallowEqual from './shallow-equal'
|
||||
@@ -22,9 +26,8 @@ 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
|
||||
createSelector as create,
|
||||
} from 'reselect'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@@ -36,12 +39,16 @@ export {
|
||||
// Use case: in connect, to avoid rerendering a component where the
|
||||
// objects are still the same.
|
||||
const _createCollectionWrapper = selector => {
|
||||
let cache
|
||||
let cache, previous
|
||||
|
||||
return (...args) => {
|
||||
const value = selector(...args)
|
||||
if (!shallowEqual(value, cache)) {
|
||||
cache = value
|
||||
if (value !== previous) {
|
||||
previous = value
|
||||
|
||||
if (!shallowEqual(value, cache)) {
|
||||
cache = value
|
||||
}
|
||||
}
|
||||
return cache
|
||||
}
|
||||
@@ -85,9 +92,7 @@ const _create2 = (...inputs) => {
|
||||
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
|
||||
args[i] = input === _SELECTOR_PLACEHOLDER ? arguments[j++] : input
|
||||
}
|
||||
|
||||
return resultFn.apply(this, args)
|
||||
@@ -98,107 +103,94 @@ const _create2 = (...inputs) => {
|
||||
// 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
|
||||
_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
|
||||
}
|
||||
)
|
||||
_create2(
|
||||
object,
|
||||
props,
|
||||
_createCollectionWrapper((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
|
||||
_create2(
|
||||
collection,
|
||||
predicate,
|
||||
_createCollectionWrapper(
|
||||
(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
|
||||
)
|
||||
_create2(collection, predicate, find)
|
||||
|
||||
export const createGroupBy = (collection, getter) =>
|
||||
_create2(
|
||||
collection,
|
||||
getter,
|
||||
groupBy
|
||||
)
|
||||
_create2(collection, getter, groupBy)
|
||||
|
||||
export const createPager = (array, page, n = 25) => _createCollectionWrapper(
|
||||
export const createPager = (array, page, n = 25) =>
|
||||
_create2(
|
||||
array,
|
||||
page,
|
||||
n,
|
||||
(array, page, n) => {
|
||||
_createCollectionWrapper((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 createSort = (collection, getter = 'name_label', order = 'asc') =>
|
||||
_create2(collection, getter, order, orderBy)
|
||||
|
||||
export const createSumBy = (itemsSelector, iterateeSelector) =>
|
||||
_create2(itemsSelector, iterateeSelector, (items, iteratee) =>
|
||||
map(items, iteratee).reduce(add, 0)
|
||||
)
|
||||
|
||||
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
|
||||
_create2(
|
||||
collection,
|
||||
iteratee,
|
||||
n,
|
||||
_createCollectionWrapper((objects, iteratee, n) => {
|
||||
const results = orderBy(objects, iteratee, 'desc')
|
||||
if (n < results.length) {
|
||||
results.length = n
|
||||
}
|
||||
)
|
||||
return results
|
||||
})
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
@@ -206,21 +198,53 @@ export const createTop = (collection, iteratee, n) =>
|
||||
|
||||
export const areObjectsFetched = state => state.objects.fetched
|
||||
|
||||
const _getId = (state, { routeParams, id }) => routeParams
|
||||
? routeParams.id
|
||||
: id
|
||||
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
|
||||
|
||||
export const getCheckPermissions = invoke(() => {
|
||||
const getPredicate = create(
|
||||
state => state.permissions,
|
||||
state => state.objects,
|
||||
(permissions, objects) => {
|
||||
objects = objects.all
|
||||
const getObject = id => objects[id] || EMPTY_OBJECT
|
||||
|
||||
return (id, permission) =>
|
||||
checkPermissions(permissions, getObject, id, permission)
|
||||
}
|
||||
)
|
||||
|
||||
const isTrue = () => true
|
||||
const isFalse = () => false
|
||||
|
||||
return state => {
|
||||
const user = getUser(state)
|
||||
|
||||
if (!user) {
|
||||
return isFalse
|
||||
}
|
||||
|
||||
if (user.permission === 'admin') {
|
||||
return isTrue
|
||||
}
|
||||
|
||||
return getPredicate(state)
|
||||
}
|
||||
})
|
||||
|
||||
const _getPermissionsPredicate = invoke(() => {
|
||||
const getPredicate = create(
|
||||
state => state.permissions,
|
||||
state => state.objects,
|
||||
(permissions, objects) => {
|
||||
objects = objects.all
|
||||
const getObject = id => (objects[id] || EMPTY_OBJECT)
|
||||
const getObject = id => objects[id] || EMPTY_OBJECT
|
||||
|
||||
return id => checkPermissions(permissions, getObject, id.id || id, 'view')
|
||||
}
|
||||
@@ -240,37 +264,46 @@ const _getPermissionsPredicate = invoke(() => {
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
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 = {
|
||||
@@ -281,30 +314,27 @@ export const createSortForType = invoke(() => {
|
||||
tag: tag => tag,
|
||||
VBD: vbd => vbd.position,
|
||||
'VDI-snapshot': snapshot => snapshot.snapshot_time,
|
||||
'VM-snapshot': snapshot => snapshot.snapshot_time
|
||||
'VM-snapshot': snapshot => snapshot.snapshot_time,
|
||||
}
|
||||
const defaultIteratees = [
|
||||
object => object.$pool,
|
||||
object => object.name_label
|
||||
]
|
||||
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'
|
||||
'VM-snapshot': 'desc',
|
||||
}
|
||||
const getOrders = type => ordersByType[type]
|
||||
|
||||
const autoSelector = (type, fn) => isFunction(type)
|
||||
? (state, props) => fn(type(state, props))
|
||||
: [ fn(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),
|
||||
)
|
||||
return (type, collection) =>
|
||||
createSort(
|
||||
collection,
|
||||
autoSelector(type, getIteratees),
|
||||
autoSelector(type, getOrders)
|
||||
)
|
||||
})
|
||||
|
||||
// Add utility methods to a collection selector.
|
||||
@@ -320,7 +350,11 @@ const _extendCollectionSelector = (selector, objectsType) => {
|
||||
return selector
|
||||
}
|
||||
_addGroupBy(selector)
|
||||
selector.find = predicate => createFinder(selector, predicate)
|
||||
const _addFind = selector => {
|
||||
selector.find = predicate => createFinder(selector, predicate)
|
||||
return selector
|
||||
}
|
||||
_addFind(selector)
|
||||
|
||||
// groupBy can be chained.
|
||||
const _addSort = selector => {
|
||||
@@ -332,17 +366,17 @@ const _extendCollectionSelector = (selector, objectsType) => {
|
||||
|
||||
// count, groupBy and sort can be chained.
|
||||
const _addFilter = selector => {
|
||||
selector.filter = predicate => _addCount(_addGroupBy(_addSort(
|
||||
createFilter(selector, predicate)
|
||||
)))
|
||||
selector.filter = predicate =>
|
||||
_addCount(_addGroupBy(_addSort(createFilter(selector, predicate))))
|
||||
return selector
|
||||
}
|
||||
_addFilter(selector)
|
||||
|
||||
// filter, groupBy and sort can be chained.
|
||||
selector.pick = idsSelector => _addFilter(_addGroupBy(_addSort(
|
||||
createPicker(selector, idsSelector)
|
||||
)))
|
||||
selector.pick = idsSelector =>
|
||||
_addFind(
|
||||
_addFilter(_addGroupBy(_addSort(createPicker(selector, idsSelector))))
|
||||
)
|
||||
|
||||
return selector
|
||||
}
|
||||
@@ -360,7 +394,7 @@ const _extendCollectionSelector = (selector, objectsType) => {
|
||||
// - 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, groupBy and sort can be chained)
|
||||
// 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 => {
|
||||
@@ -368,10 +402,10 @@ export const createGetObjectsOfType = type => {
|
||||
? (state, props) => state.objects.byType[type(state, props)] || EMPTY_OBJECT
|
||||
: state => state.objects.byType[type] || EMPTY_OBJECT
|
||||
|
||||
return _extendCollectionSelector(createFilter(
|
||||
getObjects,
|
||||
_getPermissionsPredicate
|
||||
), type)
|
||||
return _extendCollectionSelector(
|
||||
createFilter(getObjects, _getPermissionsPredicate),
|
||||
type
|
||||
)
|
||||
}
|
||||
|
||||
export const createGetTags = collectionSelectors => {
|
||||
@@ -379,34 +413,56 @@ export const createGetTags = collectionSelectors => {
|
||||
collectionSelectors = [
|
||||
createGetObjectsOfType('host'),
|
||||
createGetObjectsOfType('pool'),
|
||||
createGetObjectsOfType('VM')
|
||||
createGetObjectsOfType('VM'),
|
||||
]
|
||||
}
|
||||
|
||||
const getTags = create(
|
||||
collectionSelectors,
|
||||
(...collections) => {
|
||||
const tags = {}
|
||||
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)
|
||||
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 createGetVmLastShutdownTime = (
|
||||
getVmId = (_, { vm }) => (vm != null ? vm.id : undefined)
|
||||
) =>
|
||||
create(getVmId, createGetObjectsOfType('message'), (vmId, messages) => {
|
||||
let max = null
|
||||
forEach(messages, message => {
|
||||
if (
|
||||
message.$object === vmId &&
|
||||
message.name === 'VM_SHUTDOWN' &&
|
||||
(max === null || message.time > max)
|
||||
) {
|
||||
max = message.time
|
||||
}
|
||||
})
|
||||
return max
|
||||
})
|
||||
|
||||
export const createGetObjectMessages = objectSelector =>
|
||||
createGetObjectsOfType('message').filter(
|
||||
create(
|
||||
(...args) => objectSelector(...args).id,
|
||||
id => message => message.$object === id
|
||||
createGetObjectsOfType('message')
|
||||
.filter(
|
||||
create(
|
||||
(...args) => objectSelector(...args).id,
|
||||
id => message => message.$object === id
|
||||
)
|
||||
)
|
||||
).sort()
|
||||
.sort()
|
||||
|
||||
// Example of use:
|
||||
// import store from 'store'
|
||||
@@ -414,15 +470,54 @@ export const createGetObjectMessages = objectSelector =>
|
||||
// ...
|
||||
export const getObject = createGetObject((_, id) => id)
|
||||
|
||||
export const createGetHostMetrics = hostSelector => _createCollectionWrapper(
|
||||
export const createDoesHostNeedRestart = hostSelector => {
|
||||
// XS < 7.1
|
||||
const patchRequiresReboot = createGetObjectsOfType('pool_patch')
|
||||
.pick(
|
||||
// Returns the first patch of the host which requires it to be
|
||||
// restarted.
|
||||
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 create(
|
||||
hostSelector,
|
||||
(...args) => args,
|
||||
(host, args) => host.rebootRequired || !!patchRequiresReboot(...args)
|
||||
)
|
||||
}
|
||||
|
||||
export const createGetHostMetrics = hostSelector =>
|
||||
create(
|
||||
hostSelector,
|
||||
hosts => {
|
||||
_createCollectionWrapper(hosts => {
|
||||
const metrics = {
|
||||
count: 0,
|
||||
cpus: 0,
|
||||
memoryTotal: 0,
|
||||
memoryUsage: 0
|
||||
memoryUsage: 0,
|
||||
}
|
||||
forEach(hosts, host => {
|
||||
metrics.count++
|
||||
@@ -431,6 +526,17 @@ export const createGetHostMetrics = hostSelector => _createCollectionWrapper(
|
||||
metrics.memoryUsage += host.memory.usage
|
||||
})
|
||||
return metrics
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
export const createGetVmDisks = vmSelector =>
|
||||
createGetObjectsOfType('VDI').pick(
|
||||
create(
|
||||
createGetObjectsOfType('VBD').pick(
|
||||
(state, props) => vmSelector(state, props).$VBDs
|
||||
),
|
||||
_createCollectionWrapper(vbds =>
|
||||
map(vbds, vbd => (vbd.is_cd_drive ? undefined : vbd.VDI))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import kindOf from 'kindof'
|
||||
|
||||
// Tests that two collections (arrays or objects) have strictly equals
|
||||
// values (items or properties)
|
||||
const shallowEqual = (c1, c2) => {
|
||||
@@ -5,8 +7,8 @@ const shallowEqual = (c1, c2) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const type = typeof c1
|
||||
if (type !== typeof c2) {
|
||||
const type = kindOf(c1)
|
||||
if (type !== kindOf(c2)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -25,8 +27,13 @@ const shallowEqual = (c1, c2) => {
|
||||
return true
|
||||
}
|
||||
|
||||
if (type !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
let n = 0
|
||||
for (const _ in c2) { // eslint-disable-line no-unused-vars
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for (const _ in c2) {
|
||||
++n
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
import React, { cloneElement } from 'react'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
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>)
|
||||
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 }
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
.clickableColumn {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickableColumn:hover {
|
||||
color: #fff;
|
||||
background-color: #96b8d1;
|
||||
color: #fff;
|
||||
background-color: #96b8d1;
|
||||
}
|
||||
|
||||
.clickableRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
outline: 2px solid #366e98;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
46
src/common/state-button.js
Normal file
46
src/common/state-button.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
// do not forward `state` to ActionButton
|
||||
const Button = styled(p => <ActionButton {...omit(p, 'state')} />)`
|
||||
background-color: ${p =>
|
||||
p.theme[`${p.state ? 'enabled' : 'disabled'}StateBg`]};
|
||||
border: 2px solid
|
||||
${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};
|
||||
color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};
|
||||
`
|
||||
|
||||
const StateButton = ({
|
||||
disabledHandler,
|
||||
disabledHandlerParam,
|
||||
disabledLabel,
|
||||
disabledTooltip,
|
||||
|
||||
enabledLabel,
|
||||
enabledTooltip,
|
||||
enabledHandler,
|
||||
enabledHandlerParam,
|
||||
|
||||
state,
|
||||
...props
|
||||
}) => (
|
||||
<Button
|
||||
handler={state ? enabledHandler : disabledHandler}
|
||||
handlerParam={state ? enabledHandlerParam : disabledHandlerParam}
|
||||
tooltip={state ? enabledTooltip : disabledTooltip}
|
||||
{...props}
|
||||
icon={state ? 'running' : 'halted'}
|
||||
size='small'
|
||||
state={state}
|
||||
>
|
||||
{state ? enabledLabel : disabledLabel}
|
||||
</Button>
|
||||
)
|
||||
|
||||
export default propTypes({
|
||||
state: propTypes.bool.isRequired,
|
||||
})(StateButton)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user