Compare commits
534 Commits
xo-web/v5.
...
v5.15.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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',
|
||||
},
|
||||
}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
/dist/
|
||||
/node_modules/
|
||||
/src/common/intl/locales/index.js
|
||||
/src/common/themes/index.js
|
||||
|
||||
npm-debug.log
|
||||
npm-debug.log.*
|
||||
|
||||
4
.prettierrc.js
Normal file
4
.prettierrc.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
}
|
||||
@@ -4,9 +4,7 @@ node_js:
|
||||
#- '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/
|
||||
|
||||
368
CHANGELOG.md
368
CHANGELOG.md
@@ -1,19 +1,375 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.3.1** (2016-10-27)
|
||||
## **5.15.0** (2017-12-29)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Improve backup restore view [\#1609](https://github.com/vatesfr/xo-web/issues/1609)
|
||||
- Move location of NFS mount point [\#1405](https://github.com/vatesfr/xo-web/issues/1405)
|
||||
- Modify VLAN of an existing network [\#1092](https://github.com/vatesfr/xo-web/issues/1092)
|
||||
- Ability to export/import XO config [\#786](https://github.com/vatesfr/xo-web/issues/786)
|
||||
* 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)
|
||||
|
||||
@@ -118,7 +474,7 @@
|
||||
- Tooltip on OS icon in VM view [\#1416](https://github.com/vatesfr/xo-web/issues/1416)
|
||||
- Display pool master [\#1407](https://github.com/vatesfr/xo-web/issues/1407)
|
||||
- Missing tooltips in VM creation view [\#1402](https://github.com/vatesfr/xo-web/issues/1402)
|
||||
- Handle VDB disconnect and connect [\#1397](https://github.com/vatesfr/xo-web/issues/1397)
|
||||
- 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)
|
||||
|
||||
@@ -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)
|
||||
|
||||

|
||||
|
||||
|
||||
140
gulpfile.js
140
gulpfile.js
@@ -2,17 +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
|
||||
|
||||
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
|
||||
@@ -20,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)
|
||||
}
|
||||
@@ -37,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) {
|
||||
@@ -57,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)) {
|
||||
@@ -81,7 +79,7 @@ var pipe = lazyFn(function () {
|
||||
}
|
||||
})
|
||||
|
||||
var resolvePath = lazyFn(function () {
|
||||
const resolvePath = lazyFn(function () {
|
||||
return require('path').resolve
|
||||
})
|
||||
|
||||
@@ -89,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')()
|
||||
)
|
||||
@@ -129,17 +125,15 @@ 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 = {
|
||||
const opts = {
|
||||
sourcemaps: {
|
||||
path: '.'
|
||||
}
|
||||
path: '.',
|
||||
},
|
||||
}
|
||||
|
||||
return PRODUCTION
|
||||
@@ -147,7 +141,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
|
||||
}
|
||||
@@ -160,9 +154,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',
|
||||
@@ -170,12 +164,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])
|
||||
}
|
||||
|
||||
@@ -183,7 +177,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.
|
||||
@@ -192,11 +190,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) {
|
||||
@@ -204,11 +202,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,
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -240,9 +240,10 @@ gulp.task(function buildPages () {
|
||||
return pipe(
|
||||
src('index.pug', { sourcemaps: true }),
|
||||
require('gulp-pug')(),
|
||||
DEVELOPMENT && require('gulp-embedlr')({
|
||||
port: LIVERELOAD_PORT
|
||||
}),
|
||||
DEVELOPMENT &&
|
||||
require('gulp-embedlr')({
|
||||
port: LIVERELOAD_PORT,
|
||||
}),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
@@ -252,12 +253,16 @@ gulp.task(function buildScripts () {
|
||||
browserify('./index.js', {
|
||||
plugins: [
|
||||
// ['css-modulesify', {
|
||||
['modular-css/browserify', {
|
||||
css: DIST_DIR + '/modules.css'
|
||||
}]
|
||||
]
|
||||
[
|
||||
'modular-css/browserify',
|
||||
{
|
||||
css: DIST_DIR + '/modules.css',
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
PRODUCTION && require('gulp-uglify')(),
|
||||
require('gulp-sourcemaps').init({ loadMaps: true }),
|
||||
PRODUCTION && require('gulp-uglify/composer')(require('uglify-es'))(),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
@@ -266,10 +271,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()
|
||||
)
|
||||
@@ -280,22 +282,20 @@ 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')
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
|
||||
200
package.json
200
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.3.1",
|
||||
"version": "5.15.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -31,10 +31,12 @@
|
||||
"npm": ">=3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ansi_up": "^1.3.0",
|
||||
"asap": "^2.0.4",
|
||||
"ava": "^0.16.0",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"@nraynaud/novnc": "0.6.1",
|
||||
"ansi_up": "^2.0.2",
|
||||
"asap": "^2.0.6",
|
||||
"babel-eslint": "^8.0.3",
|
||||
"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",
|
||||
@@ -44,87 +46,111 @@
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"babel-register": "^6.16.3",
|
||||
"babel-runtime": "^6.6.1",
|
||||
"babelify": "^7.2.0",
|
||||
"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": "^0.9.4",
|
||||
"chartist-plugin-legend": "^0.5.0",
|
||||
"bootstrap": "4.0.0-alpha.5",
|
||||
"browserify": "^14.5.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",
|
||||
"complex-matcher": "^0.1.1",
|
||||
"cookies-js": "^1.2.2",
|
||||
"d3": "^4.2.8",
|
||||
"dependency-check": "^2.5.1",
|
||||
"event-to-promise": "^0.7.0",
|
||||
"font-awesome": "^4.5.0",
|
||||
"font-mfizz": "github:fizzed/font-mfizz",
|
||||
"get-stream": "^2.3.0",
|
||||
"ghooks": "^1.1.1",
|
||||
"globby": "^6.0.0",
|
||||
"d3": "^4.12.0",
|
||||
"dependency-check": "^2.9.2",
|
||||
"enzyme": "^3.1.1",
|
||||
"enzyme-adapter-react-15": "^1.0.5",
|
||||
"enzyme-to-json": "^3.3.0",
|
||||
"eslint": "^4.13.1",
|
||||
"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": "github:gulpjs/gulp#4.0",
|
||||
"gulp-autoprefixer": "^3.1.0",
|
||||
"gulp-csso": "^2.0.0",
|
||||
"gulp-autoprefixer": "^4.0.0",
|
||||
"gulp-csso": "^3.0.0",
|
||||
"gulp-embedlr": "^0.5.2",
|
||||
"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-sass": "^3.0.0",
|
||||
"gulp-sourcemaps": "^2.2.3",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"gulp-watch": "^4.3.5",
|
||||
"human-format": "^0.6.0",
|
||||
"index-modules": "0.0.0",
|
||||
"is-ip": "^1.0.0",
|
||||
"jsonrpc-websocket-client": "0.0.1-5",
|
||||
"human-format": "^0.9.2",
|
||||
"husky": "^0.14.3",
|
||||
"immutable": "^3.8.2",
|
||||
"index-modules": "^0.3.0",
|
||||
"is-ip": "^2.0.0",
|
||||
"jest": "^22.0.0",
|
||||
"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",
|
||||
"make-error": "^1.2.1",
|
||||
"marked": "^0.3.5",
|
||||
"modular-css": "^0.28.0",
|
||||
"moment": "^2.13.0",
|
||||
"moment-timezone": "^0.5.4",
|
||||
"notifyjs": "^2.0.1",
|
||||
"novnc-node": "^0.5.3",
|
||||
"promise-toolbox": "^0.7.0",
|
||||
"marked": "^0.3.7",
|
||||
"modular-css": "^7.2.0",
|
||||
"moment": "^2.20.0",
|
||||
"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.3.0",
|
||||
"react-notify": "^2.0.1",
|
||||
"react-overlays": "^0.6.0",
|
||||
"react-redux": "^4.4.0",
|
||||
"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.0.0-beta13",
|
||||
"react-shortcuts": "^1.0.7",
|
||||
"react-sparklines": "^1.5.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.0.6",
|
||||
"redux": "^3.3.1",
|
||||
"redux-devtools": "^3.1.1",
|
||||
"readable-stream": "^2.3.3",
|
||||
"redux": "^3.7.2",
|
||||
"redux-devtools": "^3.4.1",
|
||||
"redux-devtools-dock-monitor": "^1.1.0",
|
||||
"redux-devtools-log-monitor": "^1.0.5",
|
||||
"redux-devtools-log-monitor": "^1.4.0",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.2.1",
|
||||
"standard": "^8.4.0",
|
||||
"superagent": "^2.0.0",
|
||||
"tar-stream": "^1.5.2",
|
||||
"vinyl": "^2.0.0",
|
||||
"reselect": "^2.5.4",
|
||||
"semver": "^5.4.1",
|
||||
"styled-components": "^2.3.0",
|
||||
"tar-stream": "^1.5.5",
|
||||
"uglify-es": "^3.2.2",
|
||||
"uncontrollable-input": "^0.0.1",
|
||||
"url-parse": "^1.2.0",
|
||||
"vinyl": "^2.1.0",
|
||||
"watchify": "^3.7.0",
|
||||
"xml2js": "^0.4.17",
|
||||
"xo-acl-resolver": "^0.2.2",
|
||||
"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"
|
||||
},
|
||||
@@ -133,11 +159,13 @@
|
||||
"build": "npm run build-indexes && NODE_ENV=production gulp build",
|
||||
"build-indexes": "index-modules --auto src",
|
||||
"dev": "npm run build-indexes && NODE_ENV=development gulp build",
|
||||
"dev-test": "ava --watch",
|
||||
"lint": "standard",
|
||||
"posttest": "npm run lint",
|
||||
"prepublish": "npm run build",
|
||||
"test": "ava"
|
||||
"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/",
|
||||
"precommit": "lint-staged",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "jest"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
@@ -145,15 +173,6 @@
|
||||
"loose-envify"
|
||||
]
|
||||
},
|
||||
"ava": {
|
||||
"babel": "inherit",
|
||||
"files": [
|
||||
"src/**/*.spec.js"
|
||||
],
|
||||
"require": [
|
||||
"babel-register"
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"env": {
|
||||
"development": {
|
||||
@@ -170,6 +189,8 @@
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"dev",
|
||||
"lodash",
|
||||
"transform-decorators-legacy",
|
||||
"transform-runtime"
|
||||
],
|
||||
@@ -179,15 +200,20 @@
|
||||
"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() })
|
||||
@@ -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,46 +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 {
|
||||
noop
|
||||
} from 'utils'
|
||||
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, (button, index) => {
|
||||
if (!button) {
|
||||
{React.Children.map(children, (child, key) => {
|
||||
if (!child) {
|
||||
return
|
||||
}
|
||||
|
||||
const { handler, handlerParam = param, label, icon, redirectOnSuccess } = button
|
||||
return <Tooltip key={index} content={_(label)}>
|
||||
<ActionButton
|
||||
key={index}
|
||||
btnStyle='secondary'
|
||||
handler={handler || noop}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
redirectOnSuccess={redirectOnSuccess}
|
||||
size='large'
|
||||
/>
|
||||
</Tooltip>
|
||||
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,
|
||||
redirectOnSuccess: React.PropTypes.string
|
||||
})
|
||||
).isRequired,
|
||||
display: React.PropTypes.oneOf(['icon', 'text', 'both'])
|
||||
display: propTypes.oneOf(['icon', 'both']),
|
||||
handlerParam: propTypes.any,
|
||||
}
|
||||
export { ActionBar as default }
|
||||
|
||||
@@ -1,73 +1,94 @@
|
||||
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'
|
||||
]),
|
||||
tooltip: propTypes.node
|
||||
|
||||
// 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,
|
||||
})
|
||||
|
||||
// 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,41 +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,
|
||||
tooltip
|
||||
},
|
||||
state: { error, working }
|
||||
props: { children, icon, pending, tooltip, ...props },
|
||||
state: { error, working },
|
||||
} = this
|
||||
|
||||
const button = <Button
|
||||
bsStyle={error ? 'warning' : btnStyle}
|
||||
form={form}
|
||||
onClick={!form && this._execute}
|
||||
disabled={working || disabled}
|
||||
type={form ? 'submit' : 'button'}
|
||||
{...{ bsSize, className, style }}
|
||||
>
|
||||
<Icon icon={working ? 'loading' : icon} fixedWidth />
|
||||
{children && ' '}
|
||||
{children}
|
||||
</Button>
|
||||
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
|
||||
|
||||
return tooltip
|
||||
? <Tooltip content={tooltip}>{button}</Tooltip>
|
||||
: button
|
||||
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)
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
import clone from 'lodash/clone'
|
||||
import includes from 'lodash/includes'
|
||||
import isArray from 'lodash/isArray'
|
||||
import forEach from 'lodash/forEach'
|
||||
import map from 'lodash/map'
|
||||
import { Component } from 'react'
|
||||
import { 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'
|
||||
|
||||
// Should components logs every renders?
|
||||
//
|
||||
// Usually set to process.env.NODE_ENV !== 'production'.
|
||||
const VERBOSE = false
|
||||
|
||||
const cowSet = (object, path, value, depth) => {
|
||||
if (depth >= path.length) {
|
||||
return value
|
||||
}
|
||||
|
||||
object = clone(object)
|
||||
const prop = path[depth]
|
||||
object[prop] = cowSet(object[prop], path, value, depth + 1)
|
||||
return object
|
||||
}
|
||||
|
||||
const get = (object, path, depth) => {
|
||||
if (depth >= path.length) {
|
||||
return object
|
||||
@@ -36,7 +20,7 @@ const get = (object, path, depth) => {
|
||||
: get(object[prop], path, depth)
|
||||
}
|
||||
|
||||
export default class BaseComponent extends Component {
|
||||
export default class BaseComponent extends PureComponent {
|
||||
constructor (props, context) {
|
||||
super(props, context)
|
||||
|
||||
@@ -46,30 +30,28 @@ export default class BaseComponent extends Component {
|
||||
this._linkedState = null
|
||||
|
||||
if (VERBOSE) {
|
||||
this.render = invoke(this.render, render => () => {
|
||||
this.render = (render => () => {
|
||||
console.log('render', this.constructor.name)
|
||||
|
||||
return render.call(this)
|
||||
})
|
||||
})(this.render)
|
||||
}
|
||||
}
|
||||
|
||||
// See https://preactjs.com/guide/linked-state
|
||||
linkState (name, targetPath) {
|
||||
const key = targetPath
|
||||
? `${name}##${targetPath}`
|
||||
: name
|
||||
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[key])) {
|
||||
} else if ((cb = linkedState[key]) !== undefined) {
|
||||
return cb
|
||||
}
|
||||
|
||||
let getValue
|
||||
if (targetPath) {
|
||||
if (targetPath !== undefined) {
|
||||
const path = targetPath.split('.')
|
||||
getValue = event => get(getEventValue(event), path, 0)
|
||||
} else {
|
||||
@@ -85,7 +67,7 @@ export default class BaseComponent extends Component {
|
||||
|
||||
return (linkedState[key] = event => {
|
||||
this.setState({
|
||||
[name]: getValue(event)
|
||||
[name]: getValue(event),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -93,9 +75,9 @@ export default class BaseComponent extends Component {
|
||||
toggleState (name) {
|
||||
let linkedState = this._linkedState
|
||||
let cb
|
||||
if (!linkedState) {
|
||||
if (linkedState === null) {
|
||||
linkedState = this._linkedState = {}
|
||||
} else if ((cb = linkedState[name])) {
|
||||
} else if ((cb = linkedState[name]) !== undefined) {
|
||||
return cb
|
||||
}
|
||||
|
||||
@@ -108,17 +90,10 @@ export default class BaseComponent extends Component {
|
||||
|
||||
return (linkedState[name] = () => {
|
||||
this.setState({
|
||||
[name]: !this.state[name]
|
||||
[name]: !this.state[name],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
shouldComponentUpdate (newProps, newState) {
|
||||
return !(
|
||||
shallowEqual(this.props, newProps) &&
|
||||
shallowEqual(this.state, newState)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (VERBOSE) {
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
@@ -1,31 +1,34 @@
|
||||
import _ from 'intl'
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import classNames from 'classnames'
|
||||
import Tooltip from 'tooltip'
|
||||
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
|
||||
})(({ className, tagName = 'span', ...props }) => createElement(
|
||||
tagName,
|
||||
{
|
||||
...props,
|
||||
className: classNames(styles.container, className)
|
||||
},
|
||||
props.children,
|
||||
' ',
|
||||
<Tooltip content={_('copyToClipboard')}>
|
||||
<CopyToClipboard text={props.data || props.children}>
|
||||
<button className={classNames('btn btn-sm btn-secondary', styles.button)}>
|
||||
<Icon icon='clipboard' />
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
))
|
||||
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 { '}
|
||||
{status === 'rejected' && '<rejected> '}
|
||||
{toString(value)}
|
||||
{' }'}
|
||||
</pre>
|
||||
return (
|
||||
<pre>
|
||||
{'Promise { '}
|
||||
{status === 'rejected' && '<rejected> '}
|
||||
{toString(value)}
|
||||
{' }'}
|
||||
</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 }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Component from 'base-component'
|
||||
import propTypes from 'prop-types'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import ReactDropzone from 'react-dropzone'
|
||||
|
||||
@@ -7,14 +7,20 @@ import styles from './index.css'
|
||||
|
||||
@propTypes({
|
||||
onDrop: propTypes.func,
|
||||
message: propTypes.node
|
||||
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>
|
||||
return (
|
||||
<ReactDropzone
|
||||
onDrop={onDrop}
|
||||
className={styles.dropzone}
|
||||
activeClassName={styles.activeDropzone}
|
||||
>
|
||||
<div className={styles.dropzoneText}>{message}</div>
|
||||
</ReactDropzone>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ 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'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import Tooltip from '../tooltip'
|
||||
import { formatSize } from '../utils'
|
||||
import { SizeInput } from '../form'
|
||||
@@ -25,8 +25,9 @@ import {
|
||||
SelectSr,
|
||||
SelectSubject,
|
||||
SelectTag,
|
||||
SelectVgpuType,
|
||||
SelectVm,
|
||||
SelectVmTemplate
|
||||
SelectVmTemplate,
|
||||
} from '../select-objects'
|
||||
|
||||
import styles from './index.css'
|
||||
@@ -34,14 +35,14 @@ import styles from './index.css'
|
||||
const LONG_CLICK = 400
|
||||
|
||||
@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 })
|
||||
@@ -50,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 () {
|
||||
@@ -94,7 +88,7 @@ class Editable extends Component {
|
||||
this.setState({
|
||||
editing: true,
|
||||
error: null,
|
||||
saving: false
|
||||
saving: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -112,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) {
|
||||
@@ -138,7 +129,7 @@ class Editable extends Component {
|
||||
this.setState({
|
||||
// `error` may be undefined if the action has been cancelled
|
||||
error: error !== undefined && (isString(error) ? error : error.message),
|
||||
saving: false
|
||||
saving: false,
|
||||
})
|
||||
logError(error)
|
||||
}
|
||||
@@ -161,34 +152,59 @@ class Editable extends Component {
|
||||
const { useLongClick } = props
|
||||
|
||||
const success = <Icon icon='success' />
|
||||
return <span className={classNames(styles.clickToEdit, !useLongClick && styles.shortClick)}>
|
||||
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 && this._openEdition}
|
||||
onMouseDown={useLongClick && this.__startTimer}
|
||||
onMouseUp={useLongClick && this.__stopTimer}
|
||||
>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,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 () {
|
||||
@@ -217,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 () {
|
||||
@@ -247,25 +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`,
|
||||
maxWidth: '50ex'
|
||||
}}
|
||||
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'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,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 () {
|
||||
@@ -300,20 +314,19 @@ 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({
|
||||
options: propTypes.oneOfType([
|
||||
propTypes.array,
|
||||
propTypes.object
|
||||
]).isRequired,
|
||||
renderer: propTypes.func
|
||||
options: propTypes.oneOfType([propTypes.array, propTypes.object]).isRequired,
|
||||
renderer: propTypes.func,
|
||||
})
|
||||
export class Select extends Editable {
|
||||
componentWillReceiveProps (props) {
|
||||
@@ -321,7 +334,9 @@ export class Select extends Editable {
|
||||
props.value !== this.props.value ||
|
||||
props.options !== this.props.options
|
||||
) {
|
||||
this.setState({ valueKey: findKey(props.options, option => option === props.value) })
|
||||
this.setState({
|
||||
valueKey: findKey(props.options, option => option === props.value),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,12 +351,11 @@ export class Select extends Editable {
|
||||
_optionToJsx = (option, key) => {
|
||||
const { renderer } = this.props
|
||||
|
||||
return <option
|
||||
key={key}
|
||||
value={key}
|
||||
>
|
||||
{renderer ? renderer(option) : option}
|
||||
</option>
|
||||
return (
|
||||
<option key={key} value={key}>
|
||||
{renderer ? renderer(option) : option}
|
||||
</option>
|
||||
)
|
||||
}
|
||||
|
||||
_onEditionMount = ref => {
|
||||
@@ -352,26 +366,27 @@ export class Select extends Editable {
|
||||
_renderDisplay () {
|
||||
const { children, renderer, value } = this.props
|
||||
|
||||
return children ||
|
||||
<span>{renderer ? renderer(value) : value}</span>
|
||||
return children || <span>{renderer ? renderer(value) : value}</span>
|
||||
}
|
||||
|
||||
_renderEdition () {
|
||||
const { saving, valueKey } = this.state
|
||||
const { options } = this.props
|
||||
|
||||
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>
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,37 +400,31 @@ const MAP_TYPE_SELECT = {
|
||||
SR: SelectSr,
|
||||
subject: SelectSubject,
|
||||
tag: SelectTag,
|
||||
vgpuType: SelectVgpuType,
|
||||
VM: SelectVm,
|
||||
'VM-template': SelectVmTemplate
|
||||
'VM-template': SelectVmTemplate,
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
labelProp: propTypes.string.isRequired,
|
||||
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 {
|
||||
saving,
|
||||
xoType,
|
||||
...props
|
||||
} = this.props
|
||||
const { saving, xoType, ...props } = this.props
|
||||
|
||||
const Select = MAP_TYPE_SELECT[xoType]
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
@@ -426,20 +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
|
||||
{...props}
|
||||
autoFocus
|
||||
disabled={saving}
|
||||
onChange={this._onChange}
|
||||
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 () {
|
||||
@@ -457,27 +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
|
||||
// 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>
|
||||
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 () {
|
||||
@@ -47,109 +45,87 @@ export class Password extends Component {
|
||||
// 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) {
|
||||
this.setState({
|
||||
value: +value
|
||||
})
|
||||
}
|
||||
|
||||
_handleChange = event => {
|
||||
const { onChange } = this.props
|
||||
const { value } = event.target
|
||||
|
||||
if (value === this.state.value) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
value
|
||||
}, onChange && (() => onChange(value)))
|
||||
}
|
||||
_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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -168,16 +144,15 @@ const DEFAULT_UNIT = 'GiB'
|
||||
readOnly: propTypes.bool,
|
||||
required: propTypes.bool,
|
||||
style: propTypes.object,
|
||||
value: propTypes.oneOfType([
|
||||
propTypes.number,
|
||||
propTypes.oneOf([ null ])
|
||||
])
|
||||
value: propTypes.oneOfType([propTypes.number, propTypes.oneOf([null])]),
|
||||
})
|
||||
export class SizeInput extends BaseComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = this._createStateFromBytes(firstDefined(props.value, props.defaultValue, null))
|
||||
this.state = this._createStateFromBytes(
|
||||
defined(props.value, props.defaultValue, null)
|
||||
)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
@@ -191,21 +166,21 @@ export class SizeInput extends BaseComponent {
|
||||
if (bytes === this._bytes) {
|
||||
return {
|
||||
input: this._input,
|
||||
unit: this._unit
|
||||
unit: this._unit,
|
||||
}
|
||||
}
|
||||
|
||||
if (bytes === null) {
|
||||
return {
|
||||
input: '',
|
||||
unit: this.props.defaultUnit || DEFAULT_UNIT
|
||||
unit: this.props.defaultUnit || DEFAULT_UNIT,
|
||||
}
|
||||
}
|
||||
|
||||
const { prefix, value } = formatSizeRaw(bytes)
|
||||
return {
|
||||
input: String(round(value, 2)),
|
||||
unit: `${prefix}B`
|
||||
unit: `${prefix}B`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,9 +208,7 @@ export class SizeInput extends BaseComponent {
|
||||
const { onChange } = this.props
|
||||
|
||||
// Empty input equals null.
|
||||
const bytes = input
|
||||
? parseSize(`${+input} ${unit}`)
|
||||
: null
|
||||
const bytes = input ? parseSize(`${+input} ${unit}`) : null
|
||||
|
||||
const isControlled = this.props.value !== undefined
|
||||
if (isControlled) {
|
||||
@@ -265,8 +238,7 @@ export class SizeInput extends BaseComponent {
|
||||
|
||||
const number = +input
|
||||
|
||||
// NaN: do not ack this change.
|
||||
if (number !== number) { // eslint-disable-line no-self-compare
|
||||
if (Number.isNaN(number)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -297,38 +269,37 @@ export class SizeInput extends BaseComponent {
|
||||
readOnly,
|
||||
placeholder,
|
||||
required,
|
||||
style
|
||||
style,
|
||||
} = this.props
|
||||
|
||||
return <span className={classNames('input-group', className)} style={style}>
|
||||
<input
|
||||
autoFocus={autoFocus}
|
||||
className='form-control'
|
||||
disabled={readOnly}
|
||||
onChange={this._updateNumber}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
type='text'
|
||||
value={this.state.input}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<DropdownButton
|
||||
bsStyle='secondary'
|
||||
id='size'
|
||||
pullRight
|
||||
return (
|
||||
<span className={classNames('input-group', className)} style={style}>
|
||||
<input
|
||||
autoFocus={autoFocus}
|
||||
className='form-control'
|
||||
disabled={readOnly}
|
||||
title={this.state.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
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,39 +2,35 @@ import map from 'lodash/map'
|
||||
import React, { Component } from 'react'
|
||||
import ReactSelect from 'react-select'
|
||||
import sum from 'lodash/sum'
|
||||
import {
|
||||
AutoSizer,
|
||||
CellMeasurer,
|
||||
List
|
||||
} from 'react-virtualized'
|
||||
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
|
||||
maxHeight: propTypes.number,
|
||||
})
|
||||
export default class Select extends Component {
|
||||
static defaultProps = {
|
||||
maxHeight: 200,
|
||||
optionRenderer: (option, labelKey) => option[labelKey]
|
||||
optionRenderer: (option, labelKey) => option[labelKey],
|
||||
}
|
||||
|
||||
_renderMenu = ({
|
||||
focusedOption,
|
||||
options,
|
||||
...otherOptions
|
||||
}) => {
|
||||
_renderMenu = ({ focusedOption, options, ...otherOptions }) => {
|
||||
const { maxHeight } = this.props
|
||||
|
||||
const focusedOptionIndex = options.indexOf(focusedOption)
|
||||
@@ -48,15 +44,17 @@ export default class Select extends Component {
|
||||
key,
|
||||
option: options[index],
|
||||
options,
|
||||
style
|
||||
style,
|
||||
})
|
||||
|
||||
return (
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
{({ width }) =>
|
||||
width ? (
|
||||
<CellMeasurer
|
||||
cellRenderer={({ rowIndex }) => wrappedRowRenderer({ index: rowIndex })}
|
||||
cellRenderer={({ rowIndex }) =>
|
||||
wrappedRowRenderer({ index: rowIndex })
|
||||
}
|
||||
columnCount={1}
|
||||
rowCount={options.length}
|
||||
// FIXME: 16 px: ugly workaround to take into account the scrollbar
|
||||
@@ -66,21 +64,26 @@ export default class Select extends Component {
|
||||
>
|
||||
{({ getRowHeight }) => {
|
||||
if (options.length <= MAX_OPTIONS) {
|
||||
height = sum(map(options, (_, index) => getRowHeight({ index })))
|
||||
height = sum(
|
||||
map(options, (_, index) => getRowHeight({ index }))
|
||||
)
|
||||
}
|
||||
|
||||
return <List
|
||||
height={height}
|
||||
rowCount={options.length}
|
||||
rowHeight={getRowHeight}
|
||||
rowRenderer={wrappedRowRenderer}
|
||||
scrollToIndex={focusedOptionIndex}
|
||||
width={width}
|
||||
/>
|
||||
return (
|
||||
<List
|
||||
height={height}
|
||||
rowCount={options.length}
|
||||
rowHeight={getRowHeight}
|
||||
rowRenderer={wrappedRowRenderer}
|
||||
scrollToIndex={focusedOptionIndex}
|
||||
style={LIST_STYLE}
|
||||
width={width}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</CellMeasurer>
|
||||
) : null
|
||||
)}
|
||||
}
|
||||
</AutoSizer>
|
||||
)
|
||||
}
|
||||
@@ -92,7 +95,7 @@ export default class Select extends Component {
|
||||
labelKey,
|
||||
option,
|
||||
style,
|
||||
selectValue
|
||||
selectValue,
|
||||
}) => {
|
||||
let className = 'Select-option'
|
||||
|
||||
@@ -124,6 +127,7 @@ export default class Select extends Component {
|
||||
render () {
|
||||
return (
|
||||
<ReactSelect
|
||||
closeOnSelect={!this.props.multi}
|
||||
{...this.props}
|
||||
backspaceToRemoveMessage=''
|
||||
menuRenderer={this._renderMenu}
|
||||
|
||||
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,
|
||||
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,
|
||||
}
|
||||
|
||||
_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,20 +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 = {
|
||||
homeFilterTags: 'tags:'
|
||||
...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,27 +1,24 @@
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import { Portal } from 'react-overlays'
|
||||
import { 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,
|
||||
installAllPatchesOnPool
|
||||
installAllPatchesOnPool,
|
||||
subscribeHostMissingPatches,
|
||||
} from './xo'
|
||||
|
||||
// ===================================================================
|
||||
@@ -29,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'),
|
||||
@@ -51,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)
|
||||
@@ -80,11 +93,30 @@ 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
|
||||
)
|
||||
)
|
||||
|
||||
if (this.unsubscribeMissingPatches !== undefined) {
|
||||
this.unsubscribeMissingPatches()
|
||||
}
|
||||
|
||||
this.unsubscribeMissingPatches = () => forEach(unsubs, unsub => unsub())
|
||||
}
|
||||
|
||||
_installAllMissingPatches = () => {
|
||||
const pools = {}
|
||||
@@ -92,101 +124,71 @@ class HostsPatchesTable extends Component {
|
||||
pools[host.$pool] = true
|
||||
})
|
||||
|
||||
return Promise.all(map(
|
||||
keys(pools),
|
||||
installAllPatchesOnPool
|
||||
)).then(this._refreshMissingPatches)
|
||||
}
|
||||
|
||||
_refreshHostMissingPatches = host => (
|
||||
getHostMissingPatches(host).then(patches => {
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[host.id]: patches.length
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
_installAllHostPatches = host => (
|
||||
installAllHostPatches(host).then(() =>
|
||||
this._refreshHostMissingPatches(host)
|
||||
)
|
||||
)
|
||||
|
||||
componentWillMount () {
|
||||
this._refreshMissingPatches()
|
||||
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
|
||||
? (
|
||||
<SortedTable
|
||||
collection={hosts}
|
||||
columns={props.displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
|
||||
userData={{
|
||||
installAllHostPatches: this._installAllHostPatches,
|
||||
missingPatches: this.state.missingPatches,
|
||||
pools: props.pools
|
||||
}}
|
||||
{!noPatches ? (
|
||||
<SortedTable
|
||||
collection={hosts}
|
||||
columns={
|
||||
displayPools
|
||||
? POOLS_MISSING_PATCHES_COLUMNS
|
||||
: MISSING_PATCHES_COLUMNS
|
||||
}
|
||||
userData={{
|
||||
installAllHostPatches,
|
||||
missingPatches: this.state.missingPatches,
|
||||
pools,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<p>{_('patchNothing')}</p>
|
||||
)}
|
||||
<Portal container={() => buttonsGroupContainer()}>
|
||||
<Container>
|
||||
<Button
|
||||
btnStyle='primary'
|
||||
disabled={noPatches}
|
||||
handler={this._installAllMissingPatches}
|
||||
icon='host-patch-update'
|
||||
labelId='installPoolPatches'
|
||||
/>
|
||||
) : <p>{_('patchNothing')}</p>
|
||||
}
|
||||
<Portal container={() => props.buttonsGroupContainer()}>
|
||||
{Buttons}
|
||||
</Container>
|
||||
</Portal>
|
||||
</div>
|
||||
)
|
||||
@@ -199,7 +201,7 @@ class HostsPatchesTable extends Component {
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
|
||||
return {
|
||||
pools: getPools
|
||||
pools: getPools,
|
||||
}
|
||||
})
|
||||
class HostsPatchesTableByPool extends Component {
|
||||
@@ -217,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
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -204,7 +204,7 @@ export default {
|
||||
editUserProfile: undefined,
|
||||
|
||||
// Original text: "Fetching data…"
|
||||
homeFetchingData: 'מקבל נתונים, נא להמתין...',
|
||||
homeFetchingData: 'מקבל נתונים, נא להמתין…',
|
||||
|
||||
// Original text: "Welcome on Xen Orchestra!"
|
||||
homeWelcome: 'ברוכים הבאים',
|
||||
@@ -228,7 +228,7 @@ export default {
|
||||
homeNoVms: 'אין מכונות',
|
||||
|
||||
// Original text: "Or…"
|
||||
homeNoVmsOr: 'או...',
|
||||
homeNoVmsOr: 'או…',
|
||||
|
||||
// Original text: "Import VM"
|
||||
homeImportVm: 'ההלעה של מכונה',
|
||||
@@ -330,7 +330,7 @@ export default {
|
||||
homeMore: 'עוד',
|
||||
|
||||
// Original text: "Migrate to…"
|
||||
homeMigrateTo: 'העבר ל...',
|
||||
homeMigrateTo: 'העבר ל…',
|
||||
|
||||
// Original text: 'Missing patches'
|
||||
homeMissingPaths: undefined,
|
||||
@@ -627,7 +627,7 @@ export default {
|
||||
editBackupReportTitle: undefined,
|
||||
|
||||
// Original text: 'Enable immediately after creation'
|
||||
editBackupReportEnable: undefined,
|
||||
editBackupScheduleEnabled: undefined,
|
||||
|
||||
// Original text: 'Depth'
|
||||
editBackupDepthTitle: undefined,
|
||||
@@ -1172,7 +1172,7 @@ export default {
|
||||
// Original text: 'Reboot'
|
||||
rebootHostLabel: undefined,
|
||||
|
||||
// Original text: 'Reboot for applying updates'
|
||||
// Original text: 'Reboot to apply updates'
|
||||
rebootUpdateHostLabel: undefined,
|
||||
|
||||
// Original text: 'Emergency mode'
|
||||
@@ -1605,10 +1605,10 @@ export default {
|
||||
vdiRemove: undefined,
|
||||
|
||||
// Original text: 'Boot flag'
|
||||
vdbBootableStatus: undefined,
|
||||
vbdBootableStatus: undefined,
|
||||
|
||||
// Original text: 'Status'
|
||||
vdbStatus: undefined,
|
||||
vbdStatus: undefined,
|
||||
|
||||
// Original text: 'Connected'
|
||||
vbdStatusConnected: undefined,
|
||||
@@ -1626,19 +1626,19 @@ export default {
|
||||
vbdDisconnect: undefined,
|
||||
|
||||
// Original text: 'Bootable'
|
||||
vdbBootable: undefined,
|
||||
vbdBootable: undefined,
|
||||
|
||||
// Original text: 'Readonly'
|
||||
vdbReadonly: undefined,
|
||||
vbdReadonly: undefined,
|
||||
|
||||
// Original text: 'Create'
|
||||
vdbCreate: undefined,
|
||||
vbdCreate: undefined,
|
||||
|
||||
// Original text: 'Disk name'
|
||||
vdbNamePlaceHolder: undefined,
|
||||
vbdNamePlaceHolder: undefined,
|
||||
|
||||
// Original text: 'Size'
|
||||
vdbSizePlaceHolder: undefined,
|
||||
vbdSizePlaceHolder: undefined,
|
||||
|
||||
// Original text: 'Save'
|
||||
saveBootOption: undefined,
|
||||
@@ -3132,5 +3132,5 @@ export default {
|
||||
settingsAclsButtonTooltipSR: undefined,
|
||||
|
||||
// Original text: 'Network'
|
||||
settingsAclsButtonTooltipnetwork: undefined
|
||||
settingsAclsButtonTooltipnetwork: undefined,
|
||||
}
|
||||
|
||||
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
@@ -204,7 +204,7 @@ export default {
|
||||
editUserProfile: undefined,
|
||||
|
||||
// Original text: "Fetching data…"
|
||||
homeFetchingData: 'Obtendo dados...',
|
||||
homeFetchingData: 'Obtendo dados…',
|
||||
|
||||
// Original text: "Welcome on Xen Orchestra!"
|
||||
homeWelcome: 'Bem-vindo ao Xen Orchestra',
|
||||
@@ -228,7 +228,7 @@ export default {
|
||||
homeNoVms: 'Não foram encontradas VMs!',
|
||||
|
||||
// Original text: "Or…"
|
||||
homeNoVmsOr: 'Ou...',
|
||||
homeNoVmsOr: 'Ou…',
|
||||
|
||||
// Original text: "Import VM"
|
||||
homeImportVm: 'Importar VM',
|
||||
@@ -330,7 +330,7 @@ export default {
|
||||
homeMore: 'Mais',
|
||||
|
||||
// Original text: "Migrate to…"
|
||||
homeMigrateTo: 'Migrar para...',
|
||||
homeMigrateTo: 'Migrar para…',
|
||||
|
||||
// Original text: 'Missing patches'
|
||||
homeMissingPaths: undefined,
|
||||
@@ -360,28 +360,28 @@ export default {
|
||||
selectSubjects: 'Escolha um usuário(s) e/ou grupo(s)',
|
||||
|
||||
// Original text: "Select Object(s)…"
|
||||
selectObjects: 'Selecionar Objeto(s)...',
|
||||
selectObjects: 'Selecionar Objeto(s)…',
|
||||
|
||||
// Original text: "Choose a role"
|
||||
selectRole: 'Escolha uma função',
|
||||
|
||||
// Original text: "Select Host(s)…"
|
||||
selectHosts: 'Selecionar Host(s)...',
|
||||
selectHosts: 'Selecionar Host(s)…',
|
||||
|
||||
// Original text: "Select object(s)…"
|
||||
selectHostsVms: 'Selecionar Objeto(s)...',
|
||||
selectHostsVms: 'Selecionar Objeto(s)…',
|
||||
|
||||
// Original text: "Select Network(s)…"
|
||||
selectNetworks: 'Selecionar Rede(s)...',
|
||||
selectNetworks: 'Selecionar Rede(s)…',
|
||||
|
||||
// Original text: "Select PIF(s)…"
|
||||
selectPifs: 'Selecionar PIF(s)...',
|
||||
selectPifs: 'Selecionar PIF(s)…',
|
||||
|
||||
// Original text: "Select Pool(s)…"
|
||||
selectPools: 'Selecionar Pool(s)...',
|
||||
selectPools: 'Selecionar Pool(s)…',
|
||||
|
||||
// Original text: "Select Remote(s)…"
|
||||
selectRemotes: 'Selecionar Remote(s)...',
|
||||
selectRemotes: 'Selecionar Remote(s)…',
|
||||
|
||||
// Original text: 'Select resource set(s)…'
|
||||
selectResourceSets: undefined,
|
||||
@@ -402,19 +402,19 @@ export default {
|
||||
selectSshKey: undefined,
|
||||
|
||||
// Original text: "Select SR(s)…"
|
||||
selectSrs: 'Selecionar SR(s)...',
|
||||
selectSrs: 'Selecionar SR(s)…',
|
||||
|
||||
// Original text: "Select VM(s)…"
|
||||
selectVms: 'Selecionar VM(s)...',
|
||||
selectVms: 'Selecionar VM(s)…',
|
||||
|
||||
// Original text: "Select VM template(s)…"
|
||||
selectVmTemplates: 'Selecionar VM(s) modelo(s)...',
|
||||
selectVmTemplates: 'Selecionar VM(s) modelo(s)…',
|
||||
|
||||
// Original text: "Select tag(s)…"
|
||||
selectTags: 'Selecionar etiqueta(s)...',
|
||||
selectTags: 'Selecionar etiqueta(s)…',
|
||||
|
||||
// Original text: "Select disk(s)…"
|
||||
selectVdis: 'Selecionar disco(s)...',
|
||||
selectVdis: 'Selecionar disco(s)…',
|
||||
|
||||
// Original text: 'Select timezone…'
|
||||
selectTimezone: undefined,
|
||||
@@ -543,7 +543,8 @@ export default {
|
||||
runJob: 'Iniciar tarefa',
|
||||
|
||||
// Original text: "One shot running started. See overview for logs."
|
||||
runJobVerbose: 'O backup manual foi executado. Clique em Visão Geral para ver os Logs',
|
||||
runJobVerbose:
|
||||
'O backup manual foi executado. Clique em Visão Geral para ver os Logs',
|
||||
|
||||
// Original text: "Started"
|
||||
jobStarted: 'Iniciado',
|
||||
@@ -558,16 +559,19 @@ export default {
|
||||
deleteBackupSchedule: 'Remover tarefa de backup',
|
||||
|
||||
// Original text: "Are you sure you want to delete this backup job?"
|
||||
deleteBackupScheduleQuestion: 'Você tem certeza que você quer deletar esta tarefa de backup?',
|
||||
deleteBackupScheduleQuestion:
|
||||
'Você tem certeza que você quer deletar esta tarefa de backup?',
|
||||
|
||||
// Original text: "Enable immediately after creation"
|
||||
scheduleEnableAfterCreation: 'Ativar imediatamente após criação',
|
||||
|
||||
// Original text: "You are editing Schedule {name} ({id}). Saving will override previous schedule state."
|
||||
scheduleEditMessage: 'Você esta editando o Agendamento {name} ({id}). Este procedimento irá substituir o agendamento atual.',
|
||||
scheduleEditMessage:
|
||||
'Você esta editando o Agendamento {name} ({id}). Este procedimento irá substituir o agendamento atual.',
|
||||
|
||||
// Original text: "You are editing job {name} ({id}). Saving will override previous job state."
|
||||
jobEditMessage: 'Você esta editando a Tarefa {name} ({id}). Este procedimento irá substituir a tarefa atual.',
|
||||
jobEditMessage:
|
||||
'Você esta editando a Tarefa {name} ({id}). Este procedimento irá substituir a tarefa atual.',
|
||||
|
||||
// Original text: "No scheduled jobs."
|
||||
noScheduledJobs: 'Sem agendamentos',
|
||||
@@ -627,7 +631,7 @@ export default {
|
||||
editBackupReportTitle: undefined,
|
||||
|
||||
// Original text: 'Enable immediately after creation'
|
||||
editBackupReportEnable: undefined,
|
||||
editBackupScheduleEnabled: undefined,
|
||||
|
||||
// Original text: 'Depth'
|
||||
editBackupDepthTitle: undefined,
|
||||
@@ -942,7 +946,8 @@ export default {
|
||||
purgePluginConfiguration: 'Configuração de limpeza do plugin',
|
||||
|
||||
// Original text: "Are you sure you want to purge this configuration ?"
|
||||
purgePluginConfigurationQuestion: 'Você tem certeza que deseja executar esta configuração?',
|
||||
purgePluginConfigurationQuestion:
|
||||
'Você tem certeza que deseja executar esta configuração?',
|
||||
|
||||
// Original text: "Edit"
|
||||
editPluginConfiguration: 'Editar',
|
||||
@@ -954,7 +959,8 @@ export default {
|
||||
pluginConfigurationSuccess: 'Configuração do Plugin',
|
||||
|
||||
// Original text: "Plugin configuration successfully saved!"
|
||||
pluginConfigurationChanges: 'Configuração do plugin foi efetuada com sucesso!',
|
||||
pluginConfigurationChanges:
|
||||
'Configuração do plugin foi efetuada com sucesso!',
|
||||
|
||||
// Original text: 'Predefined configuration'
|
||||
pluginConfigurationPresetTitle: undefined,
|
||||
@@ -1172,7 +1178,7 @@ export default {
|
||||
// Original text: "Reboot"
|
||||
rebootHostLabel: 'Reinicializar',
|
||||
|
||||
// Original text: 'Reboot for applying updates'
|
||||
// Original text: 'Reboot to apply updates'
|
||||
rebootUpdateHostLabel: undefined,
|
||||
|
||||
// Original text: "Emergency mode"
|
||||
@@ -1512,7 +1518,8 @@ export default {
|
||||
tipLabel: 'Dica',
|
||||
|
||||
// Original text: "non-US keyboard could have issues with console: switch your own layout to US."
|
||||
tipConsoleLabel: 'Teclados fora do padrão US-Keyboard podem apresentar problemas com o console: Altere seu teclado e verifique!',
|
||||
tipConsoleLabel:
|
||||
'Teclados fora do padrão US-Keyboard podem apresentar problemas com o console: Altere seu teclado e verifique!',
|
||||
|
||||
// Original text: 'Hide infos'
|
||||
hideHeaderTooltip: undefined,
|
||||
@@ -1605,10 +1612,10 @@ export default {
|
||||
vdiRemove: undefined,
|
||||
|
||||
// Original text: "Boot flag"
|
||||
vdbBootableStatus: 'Indicador de inicialização',
|
||||
vbdBootableStatus: 'Indicador de inicialização',
|
||||
|
||||
// Original text: "Status"
|
||||
vdbStatus: 'Status',
|
||||
vbdStatus: 'Status',
|
||||
|
||||
// Original text: "Connected"
|
||||
vbdStatusConnected: 'Conectado',
|
||||
@@ -1626,19 +1633,19 @@ export default {
|
||||
vbdDisconnect: undefined,
|
||||
|
||||
// Original text: 'Bootable'
|
||||
vdbBootable: undefined,
|
||||
vbdBootable: undefined,
|
||||
|
||||
// Original text: 'Readonly'
|
||||
vdbReadonly: undefined,
|
||||
vbdReadonly: undefined,
|
||||
|
||||
// Original text: 'Create'
|
||||
vdbCreate: undefined,
|
||||
vbdCreate: undefined,
|
||||
|
||||
// Original text: 'Disk name'
|
||||
vdbNamePlaceHolder: undefined,
|
||||
vbdNamePlaceHolder: undefined,
|
||||
|
||||
// Original text: 'Size'
|
||||
vdbSizePlaceHolder: undefined,
|
||||
vbdSizePlaceHolder: undefined,
|
||||
|
||||
// Original text: 'Save'
|
||||
saveBootOption: undefined,
|
||||
@@ -1842,7 +1849,8 @@ export default {
|
||||
vmHomeNamePlaceholder: 'Faça um longo clique para adicionar um nome',
|
||||
|
||||
// Original text: "Long click to add a description"
|
||||
vmHomeDescriptionPlaceholder: 'Faça um longo clique para adicionar uma descrição',
|
||||
vmHomeDescriptionPlaceholder:
|
||||
'Faça um longo clique para adicionar uma descrição',
|
||||
|
||||
// Original text: "Click to add a name"
|
||||
vmViewNamePlaceholder: 'Clique para adicionar um nome',
|
||||
@@ -1968,7 +1976,7 @@ export default {
|
||||
statsDashboardSelectObjects: 'Selecionar',
|
||||
|
||||
// Original text: "Loading…"
|
||||
metricsLoading: 'Carregando...',
|
||||
metricsLoading: 'Carregando…',
|
||||
|
||||
// Original text: "Coming soon!"
|
||||
comingSoon: 'Em breve!',
|
||||
@@ -2235,7 +2243,8 @@ export default {
|
||||
noHostsAvailable: 'Sem hosts disponiveis',
|
||||
|
||||
// Original text: "VMs created from this resource set shall run on the following hosts."
|
||||
availableHostsDescription: 'VMs criadas a partir desse conjunto de recursos deve ser executado nos hosts indicados.',
|
||||
availableHostsDescription:
|
||||
'VMs criadas a partir desse conjunto de recursos deve ser executado nos hosts indicados.',
|
||||
|
||||
// Original text: "Maximum CPUs"
|
||||
maxCpus: 'Limite de CPUs',
|
||||
@@ -2268,7 +2277,8 @@ export default {
|
||||
resourceSetNew: undefined,
|
||||
|
||||
// Original text: "Try dropping some VMs files here, or click to select VMs to upload. Accept only .xva/.ova files."
|
||||
importVmsList: 'Tente soltar alguns backups aqui, ou clique para selecionar os backups para que seja feito o upload. Apenas arquivos .xva são aceitos.',
|
||||
importVmsList:
|
||||
'Tente soltar alguns backups aqui, ou clique para selecionar os backups para que seja feito o upload. Apenas arquivos .xva são aceitos.',
|
||||
|
||||
// Original text: "No selected VMs."
|
||||
noSelectedVms: 'Nenhuma VM selecionada',
|
||||
@@ -2292,10 +2302,10 @@ export default {
|
||||
vmImportFailed: 'Falha na importação',
|
||||
|
||||
// Original text: "Import starting…"
|
||||
startVmImport: 'Iniciando importação...',
|
||||
startVmImport: 'Iniciando importação…',
|
||||
|
||||
// Original text: "Export starting…"
|
||||
startVmExport: 'Iniciando exportação...',
|
||||
startVmExport: 'Iniciando exportação…',
|
||||
|
||||
// Original text: 'N CPUs'
|
||||
nCpus: undefined,
|
||||
@@ -2406,7 +2416,8 @@ export default {
|
||||
stopHostModalTitle: 'Desligar host',
|
||||
|
||||
// Original text: "This will shutdown your host. Do you want to continue? If it's the pool master, your connection to the pool will be lost"
|
||||
stopHostModalMessage: 'O host será desligado. Você tem certeza que deseja continuar?',
|
||||
stopHostModalMessage:
|
||||
'O host será desligado. Você tem certeza que deseja continuar?',
|
||||
|
||||
// Original text: 'Add host'
|
||||
addHostModalTitle: undefined,
|
||||
@@ -2418,7 +2429,8 @@ export default {
|
||||
restartHostModalTitle: 'Reiniciar host',
|
||||
|
||||
// Original text: "This will restart your host. Do you want to continue?"
|
||||
restartHostModalMessage: 'O host será reiniciado. Você tem certeza que deseja continuar?',
|
||||
restartHostModalMessage:
|
||||
'O host será reiniciado. Você tem certeza que deseja continuar?',
|
||||
|
||||
// Original text: 'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}'
|
||||
restartHostsAgentsModalTitle: undefined,
|
||||
@@ -2436,7 +2448,8 @@ export default {
|
||||
startVmsModalTitle: 'Iniciar VM{vms, plural, one {} other {s}}',
|
||||
|
||||
// Original text: "Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?"
|
||||
startVmsModalMessage: 'Você tem certeza que deseja iniciar {vms} VM{vms, plural, one {} other {s}}?',
|
||||
startVmsModalMessage:
|
||||
'Você tem certeza que deseja iniciar {vms} VM{vms, plural, one {} other {s}}?',
|
||||
|
||||
// Original text: 'Stop Host{nHosts, plural, one {} other {s}}'
|
||||
stopHostsModalTitle: undefined,
|
||||
@@ -2448,7 +2461,8 @@ export default {
|
||||
stopVmsModalTitle: 'Parar VM{vms, plural, one {} other {s}}',
|
||||
|
||||
// Original text: "Are you sure you want to stop {vms} VM{vms, plural, one {} other {s}}?"
|
||||
stopVmsModalMessage: 'Você tem certeza que deseja parar {vms} VM{vms, plural, one {} other {s}}?',
|
||||
stopVmsModalMessage:
|
||||
'Você tem certeza que deseja parar {vms} VM{vms, plural, one {} other {s}}?',
|
||||
|
||||
// Original text: "Restart VM"
|
||||
restartVmModalTitle: 'Reiniciar VM',
|
||||
@@ -2466,25 +2480,29 @@ export default {
|
||||
restartVmsModalTitle: 'Reiniciar VM{vms, plural, one {} other {s}}',
|
||||
|
||||
// Original text: "Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?"
|
||||
restartVmsModalMessage: 'Você tem certeza que deseja reiniciar {vms} VM{vms, plural, one {} other {s}}?',
|
||||
restartVmsModalMessage:
|
||||
'Você tem certeza que deseja reiniciar {vms} VM{vms, plural, one {} other {s}}?',
|
||||
|
||||
// Original text: "Snapshot VM{vms, plural, one {} other {s}}"
|
||||
snapshotVmsModalTitle: 'Snapshot VM{vms, plural, one {} other {s}}',
|
||||
|
||||
// Original text: "Are you sure you want to snapshot {vms} VM{vms, plural, one {} other {s}}?"
|
||||
snapshotVmsModalMessage: 'Você tem certeza que deseja executar snapshop para {vms} VM{vms, plural, one {} other {s}}?',
|
||||
snapshotVmsModalMessage:
|
||||
'Você tem certeza que deseja executar snapshop para {vms} VM{vms, plural, one {} other {s}}?',
|
||||
|
||||
// Original text: "Delete VM{vms, plural, one {} other {s}}"
|
||||
deleteVmsModalTitle: 'Deletar VM{vms, plural, one {} other {s}}',
|
||||
|
||||
// Original text: "Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED"
|
||||
deleteVmsModalMessage: 'Você tem certeza que deseja deletar {vms} VM{vms, plural, one {} other {s}}? Todos os discos de VM serão removidos',
|
||||
deleteVmsModalMessage:
|
||||
'Você tem certeza que deseja deletar {vms} VM{vms, plural, one {} other {s}}? Todos os discos de VM serão removidos',
|
||||
|
||||
// Original text: "Delete VM"
|
||||
deleteVmModalTitle: 'Deletar VM',
|
||||
|
||||
// Original text: "Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED"
|
||||
deleteVmModalMessage: 'Você tem certeza que deseja deletar esta VM? Todos os discos de VM serão removidos',
|
||||
deleteVmModalMessage:
|
||||
'Você tem certeza que deseja deletar esta VM? Todos os discos de VM serão removidos',
|
||||
|
||||
// Original text: "Migrate VM"
|
||||
migrateVmModalTitle: 'Migrar VM',
|
||||
@@ -2559,16 +2577,18 @@ export default {
|
||||
importBackupModalStart: 'Iniciar VM após restauração',
|
||||
|
||||
// Original text: "Select your backup…"
|
||||
importBackupModalSelectBackup: 'Selecionar backup...',
|
||||
importBackupModalSelectBackup: 'Selecionar backup…',
|
||||
|
||||
// Original text: "Are you sure you want to remove all orphaned snapshot VDIs?"
|
||||
removeAllOrphanedModalWarning: 'Você tem certeza que deseja remover todos as VDIs orfãs?',
|
||||
removeAllOrphanedModalWarning:
|
||||
'Você tem certeza que deseja remover todos as VDIs orfãs?',
|
||||
|
||||
// Original text: "Remove all logs"
|
||||
removeAllLogsModalTitle: 'Remover todos os logs',
|
||||
|
||||
// Original text: "Are you sure you want to remove all logs?"
|
||||
removeAllLogsModalWarning: 'Você tem certeza que deseja remover todos os logs?',
|
||||
removeAllLogsModalWarning:
|
||||
'Você tem certeza que deseja remover todos os logs?',
|
||||
|
||||
// Original text: "This operation is definitive."
|
||||
definitiveMessageModal: 'Esta operação é definitiva.',
|
||||
@@ -2577,25 +2597,29 @@ export default {
|
||||
existingSrModalTitle: 'Uso anterior SR',
|
||||
|
||||
// Original text: "This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation."
|
||||
existingSrModalText: 'Este caminho foi previamente utilizado como um dispositivo de armazenamento por um host XenServer. Todos os dados serão perdidos se você optar por continuar a criação do SR.',
|
||||
existingSrModalText:
|
||||
'Este caminho foi previamente utilizado como um dispositivo de armazenamento por um host XenServer. Todos os dados serão perdidos se você optar por continuar a criação do SR.',
|
||||
|
||||
// Original text: "Previous LUN Usage"
|
||||
existingLunModalTitle: 'Uso anterior LUN',
|
||||
|
||||
// Original text: "This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation."
|
||||
existingLunModalText: 'Este LUN foi previamente utilizado como um dispositivo de armazenamento por um host XenServer. Todos os dados serão perdidos se você optar por continuar a criação do SR.',
|
||||
existingLunModalText:
|
||||
'Este LUN foi previamente utilizado como um dispositivo de armazenamento por um host XenServer. Todos os dados serão perdidos se você optar por continuar a criação do SR.',
|
||||
|
||||
// Original text: "Replace current registration?"
|
||||
alreadyRegisteredModal: 'Deseja substituir o registro atual?',
|
||||
|
||||
// Original text: "Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?"
|
||||
alreadyRegisteredModalText: 'O seu XO appliance já foi registrado com o e-mail {email}, você tem certeza que gostaria de substituir este registro?',
|
||||
alreadyRegisteredModalText:
|
||||
'O seu XO appliance já foi registrado com o e-mail {email}, você tem certeza que gostaria de substituir este registro?',
|
||||
|
||||
// Original text: "Ready for trial?"
|
||||
trialReadyModal: 'Pronto para iniciar o teste (trial)?',
|
||||
|
||||
// Original text: "During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!"
|
||||
trialReadyModalText: 'Durante o período experimental, XOA precisa de uma conexão internet. Esta limitação não se aplica em nossos planos pagos!',
|
||||
trialReadyModalText:
|
||||
'Durante o período experimental, XOA precisa de uma conexão internet. Esta limitação não se aplica em nossos planos pagos!',
|
||||
|
||||
// Original text: "Host"
|
||||
serverHost: 'Host',
|
||||
@@ -2844,13 +2868,16 @@ export default {
|
||||
upgrade: 'Atualização (Upgrade)',
|
||||
|
||||
// Original text: "No updater available for Community Edition"
|
||||
noUpdaterCommunity: 'Nenhuma atualização disponível para a versão Community Edition',
|
||||
noUpdaterCommunity:
|
||||
'Nenhuma atualização disponível para a versão Community Edition',
|
||||
|
||||
// Original text: "Please consider subscribe and try it with all features for free during 15 days on"
|
||||
noUpdaterSubscribe: 'Oi, inscreva-se e venha testar todos nossos recursos e serviços gratuitamente por 15 dias!',
|
||||
noUpdaterSubscribe:
|
||||
'Oi, inscreva-se e venha testar todos nossos recursos e serviços gratuitamente por 15 dias!',
|
||||
|
||||
// Original text: "Manual update could break your current installation due to dependencies issues, do it with caution"
|
||||
noUpdaterWarning: 'Atualização feita de forma manual pode corromper sua instalação atual devido a problema de dependências, tenha cuidado!',
|
||||
noUpdaterWarning:
|
||||
'Atualização feita de forma manual pode corromper sua instalação atual devido a problema de dependências, tenha cuidado!',
|
||||
|
||||
// Original text: "Current version:"
|
||||
currentVersion: 'Versão atual:',
|
||||
@@ -2862,19 +2889,23 @@ export default {
|
||||
editRegistration: undefined,
|
||||
|
||||
// Original text: "Please, take time to register in order to enjoy your trial."
|
||||
trialRegistration: 'Por favor, tome seu tempo para se registrar a fim de desfrutar do seu período de teste (trial)',
|
||||
trialRegistration:
|
||||
'Por favor, tome seu tempo para se registrar a fim de desfrutar do seu período de teste (trial)',
|
||||
|
||||
// Original text: "Start trial"
|
||||
trialStartButton: 'Iniciar teste (trial)',
|
||||
|
||||
// Original text: "You can use a trial version until {date, date, medium}. Upgrade your appliance to get it."
|
||||
trialAvailableUntil: 'Sua versao de teste é válida até {date, date, medium}. Após esta data escolha um de nossos planos e continue a desfrutar de nosso software e serviços!',
|
||||
trialAvailableUntil:
|
||||
'Sua versao de teste é válida até {date, date, medium}. Após esta data escolha um de nossos planos e continue a desfrutar de nosso software e serviços!',
|
||||
|
||||
// Original text: "Your trial has been ended. Contact us or downgrade to Free version"
|
||||
trialConsumed: 'Seu período de teste chegou ao fim. Entre em contato conosco ou faça o downgrade para a versão grátis',
|
||||
trialConsumed:
|
||||
'Seu período de teste chegou ao fim. Entre em contato conosco ou faça o downgrade para a versão grátis',
|
||||
|
||||
// Original text: "Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service."
|
||||
trialLocked: 'Seu serviço de atualização XOA parece não funcionar. Seu XOA não pode funcionar corretamente sem este serviço.',
|
||||
trialLocked:
|
||||
'Seu serviço de atualização XOA parece não funcionar. Seu XOA não pode funcionar corretamente sem este serviço.',
|
||||
|
||||
// Original text: 'No update information available'
|
||||
noUpdateInfo: undefined,
|
||||
@@ -2904,13 +2935,16 @@ export default {
|
||||
disclaimerTitle: 'Xen Orchestra versão Open-Source',
|
||||
|
||||
// Original text: "You are using XO from the sources! That's great for a personal/non-profit usage."
|
||||
disclaimerText1: 'Você está usando XO Open-Source! Isso é ótimo para um uso pessoal / sem fins lucrativos.',
|
||||
disclaimerText1:
|
||||
'Você está usando XO Open-Source! Isso é ótimo para um uso pessoal / sem fins lucrativos.',
|
||||
|
||||
// Original text: "If you are a company, it's better to use it with our appliance + pro support included:"
|
||||
disclaimerText2: 'Se você é uma empresa, é melhor usá-lo com o nosso sistema appliance + suporte pro inclusos:',
|
||||
disclaimerText2:
|
||||
'Se você é uma empresa, é melhor usá-lo com o nosso sistema appliance + suporte pro inclusos:',
|
||||
|
||||
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
|
||||
disclaimerText3: 'Esta versão não está vinculada a qualquer tipo de suporte nem atualizações. Use-a com cuidado em se tratando de tarefas críticas.',
|
||||
disclaimerText3:
|
||||
'Esta versão não está vinculada a qualquer tipo de suporte nem atualizações. Use-a com cuidado em se tratando de tarefas críticas.',
|
||||
|
||||
// Original text: "Connect PIF"
|
||||
connectPif: 'Conectar PIF',
|
||||
@@ -3132,5 +3166,5 @@ export default {
|
||||
settingsAclsButtonTooltipSR: undefined,
|
||||
|
||||
// Original text: 'Network'
|
||||
settingsAclsButtonTooltipnetwork: undefined
|
||||
settingsAclsButtonTooltipnetwork: undefined,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -13,22 +13,24 @@ export const isIpV6 = isIp.v6
|
||||
const ipv4 = /^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(?:\.(?!$)|$)){4}$/
|
||||
|
||||
function ip2hex (ip) {
|
||||
let parts = ip.split('.').map(str => parseInt(str, 10))
|
||||
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[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) }
|
||||
if (!ipv4.test(str)) {
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
function *range (ip1, ip2) {
|
||||
function * range (ip1, ip2) {
|
||||
assertIpv4(ip1, 'argument "ip1" must be a valid IPv4 address')
|
||||
assertIpv4(ip2, 'argument "ip2" must be a valid IPv4 address')
|
||||
|
||||
@@ -36,13 +38,14 @@ function *range (ip1, ip2) {
|
||||
let hex2 = ip2hex(ip2)
|
||||
|
||||
if (hex > hex2) {
|
||||
let tmp = hex
|
||||
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}`
|
||||
yield `${(i >> 24) & 0xff}.${(i >> 16) & 0xff}.${(i >> 8) & 0xff}.${i &
|
||||
0xff}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +53,10 @@ function *range (ip1, ip2) {
|
||||
|
||||
export const getNextIpV4 = ip => {
|
||||
const splitIp = ip.split('.')
|
||||
if (splitIp.length !== 4 || some(splitIp, value => value < 0 || value > 255)) {
|
||||
if (
|
||||
splitIp.length !== 4 ||
|
||||
some(splitIp, value => value < 0 || value > 255)
|
||||
) {
|
||||
return
|
||||
}
|
||||
let index
|
||||
@@ -85,10 +91,13 @@ export const formatIps = ips => {
|
||||
if (splitIp2.length !== 4) {
|
||||
return -1
|
||||
}
|
||||
return splitIp1[3] - splitIp2[3] +
|
||||
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 = []
|
||||
@@ -96,7 +105,8 @@ export const formatIps = ips => {
|
||||
forEach(sortedIps, ip => {
|
||||
if (ip !== getNextIpV4(range.last)) {
|
||||
if (range.first) {
|
||||
formattedIps[index] = range.first === range.last ? range.first : { ...range }
|
||||
formattedIps[index] =
|
||||
range.first === range.last ? range.first : { ...range }
|
||||
index++
|
||||
}
|
||||
range.first = range.last = ip
|
||||
|
||||
@@ -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-right' type='button' onClick={this.props.onDelete}>
|
||||
{_('remove')}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
import { descriptionRender, forceDisplayOptionalAttr } from './helpers'
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
@@ -48,135 +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._nextChildKey = 0
|
||||
|
||||
this.state = {
|
||||
use: props.required || forceDisplayOptionalAttr(props),
|
||||
children: this._makeChildren(props)
|
||||
}
|
||||
@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, defaultValue) {
|
||||
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={defaultValue}
|
||||
/>
|
||||
</ArrayItem>
|
||||
)
|
||||
}
|
||||
|
||||
_makeChildren (props) {
|
||||
return map(props.defaultValue, defaultValue =>
|
||||
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-right mt-1 mr-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='pb-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,14 +1,17 @@
|
||||
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
|
||||
enable as enableShortcuts,
|
||||
} from './shortcuts'
|
||||
|
||||
let instance
|
||||
@@ -22,28 +25,97 @@ const modal = (content, onClose) => {
|
||||
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)) {
|
||||
@@ -56,79 +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.getWrappedInstance
|
||||
? body.getWrappedInstance().value
|
||||
: body.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
|
||||
)
|
||||
})
|
||||
@@ -138,14 +159,18 @@ 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 () {
|
||||
@@ -160,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>
|
||||
)
|
||||
|
||||
31
src/common/no-objects.js
Normal file
31
src/common/no-objects.js
Normal file
@@ -0,0 +1,31 @@
|
||||
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 = ({ children, collection, emptyMessage }) =>
|
||||
collection == null ? (
|
||||
<img src='assets/loading.svg' alt='loading' />
|
||||
) : isEmpty(collection) ? (
|
||||
<p>{emptyMessage}</p>
|
||||
) : (
|
||||
<div>{children}</div>
|
||||
)
|
||||
|
||||
propTypes(NoObjects)({
|
||||
children: propTypes.node.isRequired,
|
||||
collection: propTypes.oneOfType([propTypes.array, propTypes.object])
|
||||
.isRequired,
|
||||
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 () {
|
||||
|
||||
33
src/common/prop-types-decorator.js
Normal file
33
src/common/prop-types-decorator.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import assign from 'lodash/assign'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// 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 }
|
||||
78
src/common/react-novnc.js
vendored
78
src/common/react-novnc.js
vendored
@@ -1,20 +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'
|
||||
import { enable as enableShortcuts, disable as disableShortcuts } from 'shortcuts'
|
||||
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]
|
||||
@@ -25,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) {
|
||||
@@ -42,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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,23 +77,39 @@ export default class NoVnc extends Component {
|
||||
_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()
|
||||
}
|
||||
|
||||
@@ -139,13 +155,15 @@ export default class NoVnc extends Component {
|
||||
}
|
||||
|
||||
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 _ 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 - sr.physical_usage)} free)`
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Icon icon='sr' /> {label}
|
||||
</span>
|
||||
)
|
||||
}))
|
||||
)
|
||||
|
||||
// VM.
|
||||
export const VmItem = propTypes({
|
||||
vm: propTypes.object.isRequired
|
||||
})(connectStore(() => {
|
||||
const getContainer = createGetObject(
|
||||
(_, props) => props.vm.$container
|
||||
)
|
||||
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}
|
||||
@@ -123,7 +126,7 @@ const xoItemToRender = {
|
||||
<Icon icon='ip' /> {ipPool.name}
|
||||
</span>
|
||||
),
|
||||
ipAddress: ({label, used}) => {
|
||||
ipAddress: ({ label, used }) => {
|
||||
if (used) {
|
||||
return <strong className='text-warning'>{label}</strong>
|
||||
}
|
||||
@@ -139,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>
|
||||
),
|
||||
|
||||
@@ -156,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>
|
||||
),
|
||||
|
||||
@@ -174,14 +180,40 @@ 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 (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`)
|
||||
@@ -214,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'>{_('errorNoSuchItem')}</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,42 +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 { FormattedDate, FormattedTime } from 'react-intl'
|
||||
import {
|
||||
Tab,
|
||||
Tabs
|
||||
} from 'react-bootstrap-4/lib'
|
||||
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 = []
|
||||
@@ -54,20 +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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +90,7 @@ const PICKTIME_TO_ID = {
|
||||
hour: 1,
|
||||
monthDay: 2,
|
||||
month: 3,
|
||||
weekDay: 4
|
||||
weekDay: 4,
|
||||
}
|
||||
|
||||
const TIME_FORMAT = {
|
||||
@@ -110,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) =>
|
||||
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
|
||||
<FormattedDate value={Date.UTC(1970, 0, 4 + dayNum)} weekday='long' timeZone='UTC' />
|
||||
<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 pb-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) => (
|
||||
@@ -168,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 = () => {
|
||||
@@ -179,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>
|
||||
)
|
||||
@@ -189,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 = () => {
|
||||
@@ -225,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>
|
||||
@@ -238,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-right' onClick={this._reset}>
|
||||
{_('selectTableReset')}
|
||||
</button>
|
||||
<Button className='pull-right' onClick={this._reset}>
|
||||
{_(`selectTableAll${labelId}`)}{' '}
|
||||
{value && !value.length && <Icon icon='success' />}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -264,217 +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 { value, valueRenderer } = props
|
||||
|
||||
if (value.indexOf('/') === 1) {
|
||||
this.setState({
|
||||
activeKey: NAV_EVERY_N
|
||||
}, () => { this.refs.range.value = value.split('/')[1] })
|
||||
} else {
|
||||
this.setState({
|
||||
activeKey: NAV_EACH_SELECTED,
|
||||
tableValue: value === '*'
|
||||
? []
|
||||
: map(value.split(','), valueRenderer)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this._update(this.props)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
this._update(props)
|
||||
}
|
||||
|
||||
_selectTab = activeKey => {
|
||||
this.setState({
|
||||
activeKey
|
||||
}, () => {
|
||||
const { activeKey, tableValue } = this.state
|
||||
const { onChange } = this.props
|
||||
const { refs } = this
|
||||
|
||||
if (activeKey === NAV_EACH_SELECTED) {
|
||||
onChange(tableValue)
|
||||
} else {
|
||||
onChange(refs.range.value)
|
||||
}
|
||||
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>
|
||||
@@ -482,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,18 +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 map from 'lodash/map'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import pickBy from 'lodash/pickBy'
|
||||
import size from 'lodash/size'
|
||||
import slice from 'lodash/slice'
|
||||
import { createSelector as create } from 'reselect'
|
||||
import {
|
||||
filter,
|
||||
find,
|
||||
forEach,
|
||||
groupBy,
|
||||
isArray,
|
||||
isArrayLike,
|
||||
isFunction,
|
||||
keys,
|
||||
map,
|
||||
orderBy,
|
||||
pickBy,
|
||||
size,
|
||||
slice,
|
||||
} from 'lodash'
|
||||
|
||||
import invoke from './invoke'
|
||||
import shallowEqual from './shallow-equal'
|
||||
@@ -23,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'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@@ -37,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
|
||||
}
|
||||
@@ -86,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)
|
||||
@@ -99,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
|
||||
})
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
@@ -207,9 +198,8 @@ 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
|
||||
|
||||
@@ -217,13 +207,44 @@ 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')
|
||||
}
|
||||
@@ -253,33 +274,36 @@ export const isAdmin = (...args) => {
|
||||
// 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 = {
|
||||
@@ -290,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.
|
||||
@@ -345,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 => _addFind(_addFilter(_addGroupBy(_addSort(
|
||||
createPicker(selector, idsSelector)
|
||||
))))
|
||||
selector.pick = idsSelector =>
|
||||
_addFind(
|
||||
_addFilter(_addGroupBy(_addSort(createPicker(selector, idsSelector))))
|
||||
)
|
||||
|
||||
return selector
|
||||
}
|
||||
@@ -381,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 => {
|
||||
@@ -392,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'
|
||||
@@ -428,40 +471,53 @@ export const createGetObjectMessages = objectSelector =>
|
||||
export const getObject = createGetObject((_, id) => id)
|
||||
|
||||
export const createDoesHostNeedRestart = hostSelector => {
|
||||
// Returns the first patch of the host which requires it to be
|
||||
// restarted.
|
||||
const restartPoolPatch = createGetObjectsOfType('pool_patch').pick(
|
||||
create(
|
||||
createGetObjectsOfType('host_patch').pick(
|
||||
(state, props) => {
|
||||
const host = hostSelector(state, props)
|
||||
return host && host.patches
|
||||
}
|
||||
).filter(create(
|
||||
(state, props) => {
|
||||
const host = hostSelector(state, props)
|
||||
return host && host.startTime
|
||||
},
|
||||
startTime => patch => patch.time > startTime
|
||||
)),
|
||||
hostPatches => map(hostPatches, hostPatch => hostPatch.pool_patch)
|
||||
// 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'
|
||||
) ])
|
||||
.find([
|
||||
({ guidance }) =>
|
||||
find(
|
||||
guidance,
|
||||
action => action === 'restartHost' || action === 'restartXapi'
|
||||
),
|
||||
])
|
||||
|
||||
return (state, props) => restartPoolPatch(state, props) !== undefined
|
||||
return create(
|
||||
hostSelector,
|
||||
(...args) => args,
|
||||
(host, args) => host.rebootRequired || !!patchRequiresReboot(...args)
|
||||
)
|
||||
}
|
||||
|
||||
export const createGetHostMetrics = hostSelector => _createCollectionWrapper(
|
||||
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++
|
||||
@@ -470,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -10,3 +10,8 @@
|
||||
.clickableRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
outline: 2px solid #366e98;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,42 @@
|
||||
import * as CM from 'complex-matcher'
|
||||
import _ from 'intl'
|
||||
import ceil from 'lodash/ceil'
|
||||
import classNames from 'classnames'
|
||||
import debounce from 'lodash/debounce'
|
||||
import findIndex from 'lodash/findIndex'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import { Dropdown, MenuItem, Pagination } from 'react-bootstrap-4/lib'
|
||||
import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
|
||||
import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
|
||||
import React from 'react'
|
||||
import Shortcuts from 'shortcuts'
|
||||
import { Portal } from 'react-overlays'
|
||||
import { routerShape } from 'react-router/lib/PropTypes'
|
||||
import { Set } from 'immutable'
|
||||
import { Dropdown, MenuItem, Pagination } from 'react-bootstrap-4/lib'
|
||||
import {
|
||||
ceil,
|
||||
filter,
|
||||
findIndex,
|
||||
forEach,
|
||||
isEmpty,
|
||||
isFunction,
|
||||
map,
|
||||
} from 'lodash'
|
||||
|
||||
import ActionRowButton from '../action-row-button'
|
||||
import Button from '../button'
|
||||
import ButtonGroup from '../button-group'
|
||||
import Component from '../base-component'
|
||||
import defined, { get } from '../xo-defined'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import SingleLineRow from '../single-line-row'
|
||||
import Tooltip from '../tooltip'
|
||||
import { BlockLink } from '../link'
|
||||
import { Container, Col } from '../grid'
|
||||
import { create as createMatcher } from '../complex-matcher'
|
||||
import { Input as DebouncedInput } from '../debounce-component-decorator'
|
||||
import {
|
||||
createCounter,
|
||||
createFilter,
|
||||
createPager,
|
||||
createSelector,
|
||||
createSort
|
||||
createSort,
|
||||
} from '../selectors'
|
||||
|
||||
import styles from './index.css'
|
||||
@@ -33,15 +45,14 @@ import styles from './index.css'
|
||||
|
||||
@propTypes({
|
||||
filters: propTypes.object,
|
||||
nFilteredItems: propTypes.number.isRequired,
|
||||
nItems: propTypes.number.isRequired,
|
||||
onChange: propTypes.func.isRequired
|
||||
onChange: propTypes.func.isRequired,
|
||||
value: propTypes.string.isRequired,
|
||||
})
|
||||
class TableFilter extends Component {
|
||||
_cleanFilter = () => this._setFilter('')
|
||||
|
||||
_setFilter = filterValue => {
|
||||
const { filter } = this.refs
|
||||
const filter = this.refs.filter.getWrappedInstance()
|
||||
filter.value = filterValue
|
||||
filter.focus()
|
||||
this.props.onChange(filterValue)
|
||||
@@ -51,39 +62,55 @@ class TableFilter extends Component {
|
||||
this.props.onChange(event.target.value)
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.refs.filter.getWrappedInstance().focus()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
|
||||
return (
|
||||
<div className='input-group'>
|
||||
<span className='input-group-addon'>{props.nFilteredItems} / {props.nItems}</span>
|
||||
{isEmpty(props.filters)
|
||||
? <span className='input-group-addon'><Icon icon='search' /></span>
|
||||
: <div className='input-group-btn'>
|
||||
{isEmpty(props.filters) ? (
|
||||
<span className='input-group-addon'>
|
||||
<Icon icon='search' />
|
||||
</span>
|
||||
) : (
|
||||
<span className='input-group-btn'>
|
||||
<Dropdown id='filter'>
|
||||
<DropdownToggle bsStyle='info'>
|
||||
<Icon icon='search' />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
{map(props.filters, (filter, label) =>
|
||||
{map(props.filters, (filter, label) => (
|
||||
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
|
||||
{_(label)}
|
||||
</MenuItem>
|
||||
)}
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>}
|
||||
<input
|
||||
type='text'
|
||||
ref='filter'
|
||||
onChange={this._onChange}
|
||||
</span>
|
||||
)}
|
||||
<DebouncedInput
|
||||
className='form-control'
|
||||
onChange={this._onChange}
|
||||
ref='filter'
|
||||
value={props.value}
|
||||
/>
|
||||
<div className='input-group-btn'>
|
||||
<button className='btn btn-secondary' onClick={this._cleanFilter}>
|
||||
<Tooltip content={_('filterSyntaxLinkTooltip')}>
|
||||
<a
|
||||
className='input-group-addon'
|
||||
href='https://xen-orchestra.com/docs/search.html#filter-syntax'
|
||||
target='_blank'
|
||||
>
|
||||
<Icon icon='info' />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<span className='input-group-btn'>
|
||||
<Button onClick={this._cleanFilter}>
|
||||
<Icon icon='clear-search' />
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -93,9 +120,9 @@ class TableFilter extends Component {
|
||||
|
||||
@propTypes({
|
||||
columnId: propTypes.number.isRequired,
|
||||
name: propTypes.any.isRequired,
|
||||
name: propTypes.node,
|
||||
sort: propTypes.func,
|
||||
sortIcon: propTypes.string
|
||||
sortIcon: propTypes.string,
|
||||
})
|
||||
class ColumnHead extends Component {
|
||||
_sort = () => {
|
||||
@@ -104,10 +131,10 @@ class ColumnHead extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { name, sortIcon } = this.props
|
||||
const { name, sortIcon, textAlign } = this.props
|
||||
|
||||
if (!this.props.sort) {
|
||||
return <th>{name}</th>
|
||||
return <th className={textAlign && `text-xs-${textAlign}`}>{name}</th>
|
||||
}
|
||||
|
||||
const isSelected = sortIcon === 'asc' || sortIcon === 'desc'
|
||||
@@ -115,6 +142,7 @@ class ColumnHead extends Component {
|
||||
return (
|
||||
<th
|
||||
className={classNames(
|
||||
textAlign && `text-xs-${textAlign}`,
|
||||
styles.clickableColumn,
|
||||
isSelected && classNames('text-white', 'bg-info')
|
||||
)}
|
||||
@@ -131,38 +159,136 @@ class ColumnHead extends Component {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const DEFAULT_ITEMS_PER_PAGE = 10
|
||||
|
||||
@propTypes({
|
||||
defaultColumn: propTypes.number,
|
||||
collection: propTypes.oneOfType([
|
||||
propTypes.array,
|
||||
propTypes.object
|
||||
]).isRequired,
|
||||
columns: propTypes.arrayOf(propTypes.shape({
|
||||
default: propTypes.bool,
|
||||
name: propTypes.node.isRequired,
|
||||
itemRenderer: propTypes.func.isRequired,
|
||||
sortCriteria: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
]),
|
||||
sortOrder: propTypes.string
|
||||
})).isRequired,
|
||||
filterContainer: propTypes.func,
|
||||
filters: propTypes.object,
|
||||
itemsPerPage: propTypes.number,
|
||||
paginationContainer: propTypes.func,
|
||||
rowAction: propTypes.func,
|
||||
rowLink: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
]),
|
||||
userData: propTypes.any
|
||||
indeterminate: propTypes.bool.isRequired,
|
||||
})
|
||||
class Checkbox extends Component {
|
||||
componentDidUpdate () {
|
||||
const { props: { indeterminate }, ref } = this
|
||||
if (ref !== null) {
|
||||
ref.indeterminate = indeterminate
|
||||
}
|
||||
}
|
||||
|
||||
_ref = ref => {
|
||||
this.ref = ref
|
||||
this.componentDidUpdate()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { indeterminate, ...props } = this.props
|
||||
props.ref = this._ref
|
||||
props.type = 'checkbox'
|
||||
return <input {...props} />
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const actionsShape = propTypes.arrayOf(
|
||||
propTypes.shape({
|
||||
// groupedActions: the function will be called with an array of the selected items in parameters
|
||||
// individualActions: the function will be called with the related item in parameters
|
||||
disabled: propTypes.oneOfType([propTypes.bool, propTypes.func]),
|
||||
handler: propTypes.func.isRequired,
|
||||
icon: propTypes.string.isRequired,
|
||||
label: propTypes.node.isRequired,
|
||||
level: propTypes.oneOf(['primary', 'warning', 'danger']),
|
||||
})
|
||||
)
|
||||
|
||||
class IndividualAction extends Component {
|
||||
_getIsDisabled = createSelector(
|
||||
() => this.props.disabled,
|
||||
() => this.props.item,
|
||||
() => this.props.userData,
|
||||
(disabled, item, userData) =>
|
||||
isFunction(disabled) ? disabled(item, userData) : disabled
|
||||
)
|
||||
|
||||
render () {
|
||||
const { icon, label, level, handler, item } = this.props
|
||||
|
||||
return (
|
||||
<ActionRowButton
|
||||
btnStyle={level}
|
||||
disabled={this._getIsDisabled()}
|
||||
handler={handler}
|
||||
handlerParam={item}
|
||||
icon={icon}
|
||||
tooltip={label}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class GroupedAction extends Component {
|
||||
_getIsDisabled = createSelector(
|
||||
() => this.props.disabled,
|
||||
() => this.props.selectedItems,
|
||||
() => this.props.userData,
|
||||
(disabled, selectedItems, userData) =>
|
||||
isFunction(disabled) ? disabled(selectedItems, userData) : disabled
|
||||
)
|
||||
|
||||
render () {
|
||||
const { icon, label, level, handler, selectedItems } = this.props
|
||||
|
||||
return (
|
||||
<ActionRowButton
|
||||
btnStyle={level}
|
||||
disabled={this._getIsDisabled()}
|
||||
handler={handler}
|
||||
handlerParam={selectedItems}
|
||||
icon={icon}
|
||||
tooltip={label}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes(
|
||||
{
|
||||
defaultColumn: propTypes.number,
|
||||
defaultFilter: propTypes.string,
|
||||
collection: propTypes.oneOfType([propTypes.array, propTypes.object])
|
||||
.isRequired,
|
||||
columns: propTypes.arrayOf(
|
||||
propTypes.shape({
|
||||
component: propTypes.func,
|
||||
default: propTypes.bool,
|
||||
name: propTypes.node,
|
||||
itemRenderer: propTypes.func,
|
||||
sortCriteria: propTypes.oneOfType([propTypes.func, propTypes.string]),
|
||||
sortOrder: propTypes.string,
|
||||
textAlign: propTypes.string,
|
||||
})
|
||||
).isRequired,
|
||||
filterContainer: propTypes.func,
|
||||
filters: propTypes.object,
|
||||
groupedActions: actionsShape,
|
||||
individualActions: actionsShape,
|
||||
itemsPerPage: propTypes.number,
|
||||
paginationContainer: propTypes.func,
|
||||
rowAction: propTypes.func,
|
||||
rowLink: propTypes.oneOfType([propTypes.func, propTypes.string]),
|
||||
// DOM node selector like body or .my-class
|
||||
// The shortcuts will be enabled when the node is focused
|
||||
shortcutsTarget: propTypes.string,
|
||||
stateUrlParam: propTypes.string,
|
||||
userData: propTypes.any,
|
||||
},
|
||||
{
|
||||
router: routerShape,
|
||||
}
|
||||
)
|
||||
export default class SortedTable extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
static defaultProps = {
|
||||
itemsPerPage: 10,
|
||||
}
|
||||
|
||||
constructor (props, context) {
|
||||
super(props, context)
|
||||
|
||||
let selectedColumn = props.defaultColumn
|
||||
if (selectedColumn == null) {
|
||||
@@ -173,53 +299,144 @@ export default class SortedTable extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
this.state = {
|
||||
const state = (this.state = {
|
||||
all: false, // whether all items are selected (accross pages)
|
||||
filter: defined(() => props.filters[props.defaultFilter], ''),
|
||||
page: 1,
|
||||
selectedColumn,
|
||||
itemsPerPage: props.itemsPerPage || DEFAULT_ITEMS_PER_PAGE
|
||||
sortOrder:
|
||||
props.columns[selectedColumn].sortOrder === 'desc' ? 'desc' : 'asc',
|
||||
})
|
||||
|
||||
const urlState = get(
|
||||
() => context.router.location.query[props.stateUrlParam]
|
||||
)
|
||||
if (urlState !== undefined) {
|
||||
const i = urlState.indexOf('-')
|
||||
if (i === -1) {
|
||||
state.filter = urlState
|
||||
} else {
|
||||
state.filter = urlState.slice(i + 1)
|
||||
state.page = +urlState.slice(0, i)
|
||||
}
|
||||
}
|
||||
|
||||
this._getSelectedColumn = () =>
|
||||
this.props.columns[this.state.selectedColumn]
|
||||
|
||||
this._getTotalNumberOfItems = createCounter(
|
||||
() => this.props.collection
|
||||
)
|
||||
this._getTotalNumberOfItems = createCounter(() => this.props.collection)
|
||||
|
||||
this._getAllItems = createSort(
|
||||
const createMatcher = str => CM.parse(str).createPredicate()
|
||||
this._getItems = createSort(
|
||||
createFilter(
|
||||
() => this.props.collection,
|
||||
createSelector(
|
||||
() => this.state.filter || '',
|
||||
createMatcher
|
||||
)
|
||||
createSelector(() => this.state.filter, createMatcher)
|
||||
),
|
||||
createSelector(
|
||||
() => this._getSelectedColumn().sortCriteria,
|
||||
() => this.props.userData,
|
||||
(sortCriteria, userData) =>
|
||||
(typeof sortCriteria === 'function')
|
||||
typeof sortCriteria === 'function'
|
||||
? object => sortCriteria(object, userData)
|
||||
: sortCriteria
|
||||
),
|
||||
() => this.state.sortOrder
|
||||
)
|
||||
|
||||
this.state.activePage = 1
|
||||
|
||||
this._getVisibleItems = createPager(
|
||||
this._getAllItems,
|
||||
() => this.state.activePage,
|
||||
this.state.itemsPerPage
|
||||
this._getItems,
|
||||
() => this.state.page,
|
||||
() => this.props.itemsPerPage
|
||||
)
|
||||
|
||||
state.selectedItemsIds = new Set()
|
||||
|
||||
this._getSelectedItems = createSelector(
|
||||
() => this.state.all,
|
||||
() => this.state.selectedItemsIds,
|
||||
this._getItems,
|
||||
(all, selectedItemsIds, items) =>
|
||||
all ? items : filter(items, item => selectedItemsIds.has(item.id))
|
||||
)
|
||||
|
||||
this._hasGroupedActions = createSelector(
|
||||
() => this.props.groupedActions,
|
||||
actions => !isEmpty(actions)
|
||||
)
|
||||
|
||||
this._getShortcutsHandler = createSelector(
|
||||
this._getVisibleItems,
|
||||
this._hasGroupedActions,
|
||||
() => this.state.highlighted,
|
||||
() => this.props.rowLink,
|
||||
() => this.props.rowAction,
|
||||
() => this.props.userData,
|
||||
(
|
||||
visibleItems,
|
||||
hasGroupedActions,
|
||||
itemIndex,
|
||||
rowLink,
|
||||
rowAction,
|
||||
userData
|
||||
) => (command, event) => {
|
||||
event.preventDefault()
|
||||
const item =
|
||||
itemIndex !== undefined ? visibleItems[itemIndex] : undefined
|
||||
|
||||
switch (command) {
|
||||
case 'SEARCH':
|
||||
this.refs.filterInput.focus()
|
||||
break
|
||||
case 'NAV_DOWN':
|
||||
if (
|
||||
hasGroupedActions ||
|
||||
rowAction !== undefined ||
|
||||
rowLink !== undefined
|
||||
) {
|
||||
this.setState({
|
||||
highlighted:
|
||||
(itemIndex + visibleItems.length + 1) % visibleItems.length ||
|
||||
0,
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'NAV_UP':
|
||||
if (
|
||||
hasGroupedActions ||
|
||||
rowAction !== undefined ||
|
||||
rowLink !== undefined
|
||||
) {
|
||||
this.setState({
|
||||
highlighted:
|
||||
(itemIndex + visibleItems.length - 1) % visibleItems.length ||
|
||||
0,
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'SELECT':
|
||||
if (itemIndex !== undefined && hasGroupedActions) {
|
||||
this._selectItem(itemIndex)
|
||||
}
|
||||
break
|
||||
case 'ROW_ACTION':
|
||||
if (item !== undefined) {
|
||||
if (rowLink !== undefined) {
|
||||
this.context.router.push(
|
||||
isFunction(rowLink) ? rowLink(item, userData) : rowLink
|
||||
)
|
||||
} else if (rowAction !== undefined) {
|
||||
rowAction(item, userData)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.setState({
|
||||
sortOrder: this.props.columns[this.state.selectedColumn].sortOrder === 'desc' ? 'desc' : 'asc'
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._checkUpdatePage()
|
||||
|
||||
// Force one Portal refresh.
|
||||
// Because Portal cannot see the container reference at first rendering.
|
||||
if (this.props.paginationContainer) {
|
||||
@@ -232,132 +449,419 @@ export default class SortedTable extends Component {
|
||||
let sortOrder
|
||||
|
||||
if (state.selectedColumn === columnId) {
|
||||
sortOrder = state.sortOrder === 'desc'
|
||||
? 'asc'
|
||||
: 'desc'
|
||||
sortOrder = state.sortOrder === 'desc' ? 'asc' : 'desc'
|
||||
} else {
|
||||
sortOrder = this.props.columns[columnId].sortOrder === 'desc'
|
||||
? 'desc'
|
||||
: 'asc'
|
||||
sortOrder =
|
||||
this.props.columns[columnId].sortOrder === 'desc' ? 'desc' : 'asc'
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedColumn: columnId,
|
||||
sortOrder
|
||||
sortOrder,
|
||||
})
|
||||
}
|
||||
|
||||
_onPageSelection = (_, event) => this.setState({
|
||||
activePage: event.eventKey
|
||||
})
|
||||
componentDidUpdate () {
|
||||
const { selectedItemsIds } = this.state
|
||||
|
||||
_onFilterChange = debounce(filter => {
|
||||
// Unselect items that are no longer visible
|
||||
if (
|
||||
(this._visibleItemsRecomputations || 0) <
|
||||
(this._visibleItemsRecomputations = this._getVisibleItems.recomputations())
|
||||
) {
|
||||
const newSelectedItems = selectedItemsIds.intersect(
|
||||
map(this._getVisibleItems(), 'id')
|
||||
)
|
||||
if (newSelectedItems.size < selectedItemsIds.size) {
|
||||
this.setState({ selectedItemsIds: newSelectedItems })
|
||||
}
|
||||
}
|
||||
|
||||
this._checkUpdatePage()
|
||||
}
|
||||
|
||||
_saveUrlState (filter, page) {
|
||||
const { stateUrlParam } = this.props
|
||||
if (stateUrlParam === undefined) {
|
||||
return
|
||||
}
|
||||
const { router } = this.context
|
||||
const { location } = router
|
||||
router.replace({
|
||||
...location,
|
||||
query: {
|
||||
...location.query,
|
||||
[stateUrlParam]: `${page}-${filter}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
_setFilter = filter => {
|
||||
this._saveUrlState(filter, 1)
|
||||
this.setState({
|
||||
filter,
|
||||
activePage: 1
|
||||
page: 1,
|
||||
highlighted: undefined,
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
|
||||
_checkUpdatePage () {
|
||||
const { page } = this.state
|
||||
if (page === 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const n = this._getItems().length
|
||||
const { itemsPerPage } = this.props
|
||||
if (n < itemsPerPage) {
|
||||
return this._setPage(1)
|
||||
}
|
||||
|
||||
if (page * itemsPerPage > n) {
|
||||
return this._setPage(ceil(n / itemsPerPage))
|
||||
}
|
||||
}
|
||||
|
||||
_setPage (page) {
|
||||
this._saveUrlState(this.state.filter, page)
|
||||
this.setState({ page })
|
||||
}
|
||||
|
||||
_onPageSelection = (_, event) => this._setPage(event.eventKey)
|
||||
|
||||
_selectAllVisibleItems = event => {
|
||||
this.setState({
|
||||
all: false,
|
||||
selectedItemsIds: event.target.checked
|
||||
? this.state.selectedItemsIds.union(map(this._getVisibleItems(), 'id'))
|
||||
: this.state.selectedItemsIds.clear(),
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: figure out why it's necessary
|
||||
_toggleNestedCheckboxGuard = false
|
||||
|
||||
_toggleNestedCheckbox = event => {
|
||||
const child = event.target.firstElementChild
|
||||
if (child != null && child.tagName === 'INPUT') {
|
||||
if (this._toggleNestedCheckboxGuard) {
|
||||
return
|
||||
}
|
||||
this._toggleNestedCheckboxGuard = true
|
||||
child.dispatchEvent(new window.MouseEvent('click', event.nativeEvent))
|
||||
this._toggleNestedCheckboxGuard = false
|
||||
}
|
||||
}
|
||||
|
||||
_selectAll = () => this.setState({ all: true })
|
||||
|
||||
_selectItem (current, selected, range = false) {
|
||||
const { all, selectedItemsIds } = this.state
|
||||
const visibleItems = this._getVisibleItems()
|
||||
const item = visibleItems[current]
|
||||
|
||||
if (all) {
|
||||
return this.setState({
|
||||
all: false,
|
||||
selectedItemsIds: new Set().withMutations(selectedItemsIds => {
|
||||
forEach(visibleItems, item => {
|
||||
selectedItemsIds.add(item.id)
|
||||
})
|
||||
selectedItemsIds.delete(item.id)
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const method = (selected === undefined
|
||||
? !selectedItemsIds.has(item.id)
|
||||
: selected)
|
||||
? 'add'
|
||||
: 'delete'
|
||||
|
||||
let previous
|
||||
this.setState({
|
||||
selectedItemsIds:
|
||||
range && (previous = this._previous) !== undefined
|
||||
? selectedItemsIds.withMutations(selectedItemsIds => {
|
||||
let i = previous
|
||||
let end = current
|
||||
if (previous > current) {
|
||||
i = current
|
||||
end = previous
|
||||
}
|
||||
for (; i <= end; ++i) {
|
||||
selectedItemsIds[method](visibleItems[i].id)
|
||||
}
|
||||
})
|
||||
: selectedItemsIds[method](item.id),
|
||||
})
|
||||
|
||||
this._previous = current
|
||||
}
|
||||
|
||||
_onSelectItemCheckbox = event => {
|
||||
const { target } = event
|
||||
this._selectItem(+target.name, target.checked, event.nativeEvent.shiftKey)
|
||||
}
|
||||
|
||||
_renderItem = (item, i) => {
|
||||
const { props, state } = this
|
||||
|
||||
const { individualActions, rowAction, rowLink, userData } = props
|
||||
|
||||
const hasGroupedActions = this._hasGroupedActions()
|
||||
const hasIndividualActions = !isEmpty(individualActions)
|
||||
|
||||
const columns = map(
|
||||
props.columns,
|
||||
({ component: Component, itemRenderer, textAlign }, key) => (
|
||||
<td className={textAlign && `text-xs-${textAlign}`} key={key}>
|
||||
{Component !== undefined ? (
|
||||
<Component item={item} userData={userData} />
|
||||
) : (
|
||||
itemRenderer(item, userData)
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
)
|
||||
|
||||
const { id = i } = item
|
||||
|
||||
const selectionColumn = hasGroupedActions && (
|
||||
<td className='text-xs-center' onClick={this._toggleNestedCheckbox}>
|
||||
<input
|
||||
checked={state.all || state.selectedItemsIds.has(id)}
|
||||
name={i} // position in visible items
|
||||
onChange={this._onSelectItemCheckbox}
|
||||
type='checkbox'
|
||||
/>
|
||||
</td>
|
||||
)
|
||||
const actionsColumn = hasIndividualActions && (
|
||||
<td>
|
||||
<div className='pull-right'>
|
||||
<ButtonGroup>
|
||||
{map(individualActions, (props, key) => (
|
||||
<IndividualAction
|
||||
{...props}
|
||||
item={item}
|
||||
key={key}
|
||||
userData={userData}
|
||||
/>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
|
||||
return rowLink != null ? (
|
||||
<BlockLink
|
||||
className={state.highlighted === i ? styles.highlight : undefined}
|
||||
key={id}
|
||||
tagName='tr'
|
||||
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
|
||||
>
|
||||
{selectionColumn}
|
||||
{columns}
|
||||
{actionsColumn}
|
||||
</BlockLink>
|
||||
) : (
|
||||
<tr
|
||||
className={classNames(
|
||||
rowAction && styles.clickableRow,
|
||||
state.highlighted === i && styles.highlight
|
||||
)}
|
||||
key={id}
|
||||
onClick={rowAction && (() => rowAction(item, userData))}
|
||||
>
|
||||
{selectionColumn}
|
||||
{columns}
|
||||
{actionsColumn}
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const {
|
||||
paginationContainer,
|
||||
filterContainer,
|
||||
filters,
|
||||
rowAction,
|
||||
rowLink,
|
||||
userData
|
||||
groupedActions,
|
||||
itemsPerPage,
|
||||
paginationContainer,
|
||||
shortcutsTarget,
|
||||
userData,
|
||||
} = props
|
||||
const { all } = state
|
||||
|
||||
const nFilteredItems = this._getAllItems().length
|
||||
const nAllItems = this._getTotalNumberOfItems()
|
||||
const nItems = this._getItems().length
|
||||
const nSelectedItems = state.selectedItemsIds.size
|
||||
const nVisibleItems = this._getVisibleItems().length
|
||||
|
||||
const paginationInstance = (
|
||||
const hasGroupedActions = this._hasGroupedActions()
|
||||
const hasIndividualActions = !isEmpty(props.individualActions)
|
||||
|
||||
const nColumns = props.columns.length + (hasIndividualActions ? 2 : 1)
|
||||
|
||||
const displayPagination =
|
||||
paginationContainer === undefined && itemsPerPage < nAllItems
|
||||
const displayFilter = filterContainer === undefined && nAllItems !== 0
|
||||
|
||||
const paginationInstance = displayPagination && (
|
||||
<Pagination
|
||||
first
|
||||
last
|
||||
prev
|
||||
next
|
||||
ellipsis
|
||||
boundaryLinks
|
||||
maxButtons={10}
|
||||
items={ceil(nFilteredItems / state.itemsPerPage)}
|
||||
activePage={this.state.activePage}
|
||||
maxButtons={7}
|
||||
items={ceil(nItems / itemsPerPage)}
|
||||
activePage={state.page}
|
||||
onSelect={this._onPageSelection}
|
||||
/>
|
||||
)
|
||||
|
||||
const filterInstance = (
|
||||
const filterInstance = displayFilter && (
|
||||
<TableFilter
|
||||
filters={filters}
|
||||
nFilteredItems={nFilteredItems}
|
||||
nItems={this._getTotalNumberOfItems()}
|
||||
onChange={this._onFilterChange}
|
||||
filters={props.filters}
|
||||
onChange={this._setFilter}
|
||||
ref='filterInput'
|
||||
value={state.filter}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{shortcutsTarget !== undefined && (
|
||||
<Shortcuts
|
||||
handler={this._getShortcutsHandler()}
|
||||
name='SortedTable'
|
||||
stopPropagation
|
||||
targetNodeSelector={shortcutsTarget}
|
||||
/>
|
||||
)}
|
||||
<table className='table'>
|
||||
<thead className='thead-default'>
|
||||
<tr>
|
||||
<th colSpan={nColumns}>
|
||||
{nItems === nAllItems
|
||||
? _('sortedTableNumberOfItems', { nTotal: nItems })
|
||||
: _('sortedTableNumberOfFilteredItems', {
|
||||
nFiltered: nItems,
|
||||
nTotal: nAllItems,
|
||||
})}
|
||||
{all ? (
|
||||
<span>
|
||||
{' '}
|
||||
-{' '}
|
||||
<span className='text-danger'>
|
||||
{_('sortedTableAllItemsSelected')}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
nSelectedItems !== 0 && (
|
||||
<span>
|
||||
{' '}
|
||||
-{' '}
|
||||
{_('sortedTableNumberOfSelectedItems', {
|
||||
nSelected: nSelectedItems,
|
||||
})}
|
||||
{nSelectedItems === nVisibleItems &&
|
||||
nSelectedItems < nItems && (
|
||||
<Button
|
||||
btnStyle='info'
|
||||
className='ml-1'
|
||||
onClick={this._selectAll}
|
||||
size='small'
|
||||
>
|
||||
{_('sortedTableSelectAllItems')}
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
{nSelectedItems !== 0 && (
|
||||
<div className='pull-right'>
|
||||
<ButtonGroup>
|
||||
{map(groupedActions, (props, key) => (
|
||||
<GroupedAction
|
||||
{...props}
|
||||
key={key}
|
||||
selectedItems={this._getSelectedItems()}
|
||||
userData={userData}
|
||||
/>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{hasGroupedActions && (
|
||||
<th
|
||||
className='text-xs-center'
|
||||
onClick={this._toggleNestedCheckbox}
|
||||
>
|
||||
<Checkbox
|
||||
onChange={this._selectAllVisibleItems}
|
||||
checked={all || nSelectedItems !== 0}
|
||||
indeterminate={
|
||||
!all &&
|
||||
nSelectedItems !== 0 &&
|
||||
nSelectedItems !== nVisibleItems
|
||||
}
|
||||
/>
|
||||
</th>
|
||||
)}
|
||||
{map(props.columns, (column, key) => (
|
||||
<ColumnHead
|
||||
textAlign={column.textAlign}
|
||||
columnId={key}
|
||||
key={key}
|
||||
name={column.name}
|
||||
sort={column.sortCriteria && this._sort}
|
||||
sortIcon={state.selectedColumn === key ? state.sortOrder : 'sort'}
|
||||
/>
|
||||
sortIcon={
|
||||
state.selectedColumn === key ? state.sortOrder : 'sort'
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{hasIndividualActions && <th />}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(this._getVisibleItems(), (item, i) => {
|
||||
const columns = map(props.columns, (column, key) => (
|
||||
<td key={key}>
|
||||
{column.itemRenderer(item, userData)}
|
||||
{nVisibleItems !== 0 ? (
|
||||
map(this._getVisibleItems(), this._renderItem)
|
||||
) : (
|
||||
<tr>
|
||||
<td className='text-info text-xs-center' colSpan={nColumns}>
|
||||
{_('sortedTableNoItems')}
|
||||
</td>
|
||||
))
|
||||
|
||||
const { id = i } = item
|
||||
|
||||
return rowLink
|
||||
? <BlockLink
|
||||
key={id}
|
||||
tagName='tr'
|
||||
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
|
||||
>{columns}</BlockLink>
|
||||
: <tr
|
||||
className={rowAction && styles.clickableRow}
|
||||
key={id}
|
||||
onClick={rowAction && (() => rowAction(item, userData))}
|
||||
>
|
||||
{columns}
|
||||
</tr>
|
||||
})}
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{(!paginationContainer || !filterContainer) && (
|
||||
{(displayFilter || displayPagination) && (
|
||||
<Container>
|
||||
<SingleLineRow>
|
||||
<Col mediumSize={8}>
|
||||
{paginationContainer
|
||||
? (
|
||||
{displayPagination &&
|
||||
(paginationContainer !== undefined ? (
|
||||
// Rebuild container function to refresh Portal component.
|
||||
<Portal container={() => paginationContainer()}>
|
||||
{paginationInstance}
|
||||
</Portal>
|
||||
) : paginationInstance
|
||||
}
|
||||
) : (
|
||||
paginationInstance
|
||||
))}
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
{filterContainer
|
||||
? (
|
||||
{displayFilter &&
|
||||
(filterContainer ? (
|
||||
<Portal container={() => filterContainer()}>
|
||||
{filterInstance}
|
||||
</Portal>
|
||||
) : filterInstance
|
||||
}
|
||||
) : (
|
||||
filterInstance
|
||||
))}
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</Container>
|
||||
|
||||
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)
|
||||
@@ -1,33 +1,24 @@
|
||||
import isFunction from 'lodash/isFunction'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const createAction = (() => {
|
||||
const { defineProperty } = Object
|
||||
const noop = function () {
|
||||
if (arguments.length) {
|
||||
throw new Error('this action expects no payload!')
|
||||
}
|
||||
}
|
||||
|
||||
return (type, payloadCreator = noop) => {
|
||||
const createActionObject = payload => {
|
||||
// Thunks
|
||||
if (isFunction(payload)) {
|
||||
return payload
|
||||
}
|
||||
return (type, payloadCreator) =>
|
||||
defineProperty(
|
||||
payloadCreator
|
||||
? (...args) => ({
|
||||
type,
|
||||
payload: payloadCreator(...args),
|
||||
})
|
||||
: (action =>
|
||||
function () {
|
||||
if (arguments.length) {
|
||||
throw new Error('this action expects no payload!')
|
||||
}
|
||||
|
||||
return payload === undefined
|
||||
? { type }
|
||||
: { type, payload }
|
||||
}
|
||||
|
||||
return defineProperty(
|
||||
(...args) => createActionObject(payloadCreator(...args)),
|
||||
return action
|
||||
})({ type }),
|
||||
'toString',
|
||||
{ value: () => type }
|
||||
)
|
||||
}
|
||||
})()
|
||||
|
||||
// ===================================================================
|
||||
@@ -40,7 +31,10 @@ export const connected = createAction('CONNECTED')
|
||||
export const disconnected = createAction('DISCONNECTED')
|
||||
|
||||
export const updateObjects = createAction('UPDATE_OBJECTS', updates => updates)
|
||||
export const updatePermissions = createAction('UPDATE_PERMISSIONS', permissions => permissions)
|
||||
export const updatePermissions = createAction(
|
||||
'UPDATE_PERMISSIONS',
|
||||
permissions => permissions
|
||||
)
|
||||
|
||||
export const signedIn = createAction('SIGNED_IN', user => user)
|
||||
export const signedOut = createAction('SIGNED_OUT')
|
||||
@@ -48,5 +42,11 @@ export const signedOut = createAction('SIGNED_OUT')
|
||||
export const xoaUpdaterState = createAction('XOA_UPDATER_STATE', state => state)
|
||||
export const xoaTrialState = createAction('XOA_TRIAL_STATE', state => state)
|
||||
export const xoaUpdaterLog = createAction('XOA_UPDATER_LOG', log => log)
|
||||
export const xoaRegisterState = createAction('XOA_REGISTER_STATE', registration => registration)
|
||||
export const xoaConfiguration = createAction('XOA_CONFIGURATION', configuration => configuration)
|
||||
export const xoaRegisterState = createAction(
|
||||
'XOA_REGISTER_STATE',
|
||||
registration => registration
|
||||
)
|
||||
export const xoaConfiguration = createAction(
|
||||
'XOA_CONFIGURATION',
|
||||
configuration => configuration
|
||||
)
|
||||
|
||||
@@ -4,10 +4,7 @@ import React from 'react'
|
||||
import { createDevTools } from 'redux-devtools'
|
||||
|
||||
export default createDevTools(
|
||||
<DockMonitor
|
||||
changePositionKey='ctrl-q'
|
||||
toggleVisibilityKey='ctrl-h'
|
||||
>
|
||||
<DockMonitor changePositionKey='ctrl-q' toggleVisibilityKey='ctrl-h'>
|
||||
<LogMonitor />
|
||||
</DockMonitor>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import reduxThunk from 'redux-thunk'
|
||||
import {
|
||||
applyMiddleware,
|
||||
combineReducers,
|
||||
compose,
|
||||
createStore
|
||||
} from 'redux'
|
||||
import { applyMiddleware, combineReducers, compose, createStore } from 'redux'
|
||||
|
||||
import { connectStore as connectXo } from '../xo'
|
||||
|
||||
@@ -13,9 +8,7 @@ import reducer from './reducer'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const enhancers = [
|
||||
applyMiddleware(reduxThunk)
|
||||
]
|
||||
const enhancers = [applyMiddleware(reduxThunk)]
|
||||
DevTools && enhancers.push(DevTools.instrument())
|
||||
|
||||
const store = createStore(
|
||||
|
||||
@@ -54,19 +54,16 @@ const combineActionHandlers = invoke(
|
||||
const actionType = firstProp(handlers)
|
||||
const handler = handlers[actionType]
|
||||
|
||||
return (state = initialState, action) => (
|
||||
return (state = initialState, action) =>
|
||||
action.type === actionType
|
||||
? handler(state, action.payload, action)
|
||||
: state
|
||||
)
|
||||
}
|
||||
|
||||
return (state = initialState, action) => {
|
||||
const handler = handlers[action.type]
|
||||
|
||||
return handler
|
||||
? handler(state, action.payload, action)
|
||||
: state
|
||||
return handler ? handler(state, action.payload, action) : state
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -79,74 +76,91 @@ export default {
|
||||
cookies.set('lang', lang)
|
||||
|
||||
return lang
|
||||
},
|
||||
}),
|
||||
|
||||
permissions: combineActionHandlers(
|
||||
{},
|
||||
{
|
||||
[actions.updatePermissions]: (_, permissions) => permissions,
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
permissions: combineActionHandlers({}, {
|
||||
[actions.updatePermissions]: (_, permissions) => permissions
|
||||
}),
|
||||
objects: combineActionHandlers(
|
||||
{
|
||||
all: {}, // Mutable for performance!
|
||||
byType: {},
|
||||
},
|
||||
{
|
||||
[actions.updateObjects]: ({ all, byType: prevByType }, updates) => {
|
||||
const byType = { ...prevByType }
|
||||
const get = type => {
|
||||
const curr = byType[type]
|
||||
const prev = prevByType[type]
|
||||
return curr === prev ? (byType[type] = { ...prev }) : curr
|
||||
}
|
||||
|
||||
objects: combineActionHandlers({
|
||||
all: {}, // Mutable for performance!
|
||||
byType: {}
|
||||
}, {
|
||||
[actions.updateObjects]: ({ all, byType: prevByType }, updates) => {
|
||||
const byType = { ...prevByType }
|
||||
const get = type => {
|
||||
const curr = byType[type]
|
||||
const prev = prevByType[type]
|
||||
return curr === prev
|
||||
? (byType[type] = { ...prev })
|
||||
: curr
|
||||
}
|
||||
|
||||
for (const id in updates) {
|
||||
const object = updates[id]
|
||||
|
||||
if (object) {
|
||||
all[id] = object
|
||||
get(object.type)[id] = object
|
||||
} else {
|
||||
for (const id in updates) {
|
||||
const object = updates[id]
|
||||
const previous = all[id]
|
||||
if (previous) {
|
||||
|
||||
if (object) {
|
||||
const { type } = object
|
||||
|
||||
all[id] = object
|
||||
get(type)[id] = object
|
||||
|
||||
if (previous && previous.type !== type) {
|
||||
delete get(previous.type)[id]
|
||||
}
|
||||
} else if (previous) {
|
||||
delete all[id]
|
||||
delete get(previous.type)[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { all, byType, fetched: true }
|
||||
return { all, byType, fetched: true }
|
||||
},
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
user: combineActionHandlers(null, {
|
||||
[actions.signedIn]: {
|
||||
next: (_, user) => user
|
||||
}
|
||||
next: (_, user) => user,
|
||||
},
|
||||
}),
|
||||
|
||||
status: combineActionHandlers('disconnected', {
|
||||
[actions.connected]: () => 'connected',
|
||||
[actions.disconnected]: () => 'disconnected'
|
||||
[actions.disconnected]: () => 'disconnected',
|
||||
}),
|
||||
|
||||
xoaUpdaterState: combineActionHandlers('disconnected', {
|
||||
[actions.xoaUpdaterState]: (_, state) => state
|
||||
[actions.xoaUpdaterState]: (_, state) => state,
|
||||
}),
|
||||
xoaTrialState: combineActionHandlers({}, {
|
||||
[actions.xoaTrialState]: (_, state) => state
|
||||
}),
|
||||
xoaUpdaterLog: combineActionHandlers([], {
|
||||
[actions.xoaUpdaterLog]: (_, log) => log
|
||||
}),
|
||||
xoaRegisterState: combineActionHandlers({state: '?'}, {
|
||||
[actions.xoaRegisterState]: (_, registration) => registration
|
||||
}),
|
||||
xoaConfiguration: combineActionHandlers({proxyHost: '', proxyPort: '', proxyUser: ''}, { // defined values for controlled inputs
|
||||
[actions.xoaConfiguration]: (_, configuration) => {
|
||||
delete configuration.password
|
||||
return configuration
|
||||
xoaTrialState: combineActionHandlers(
|
||||
{},
|
||||
{
|
||||
[actions.xoaTrialState]: (_, state) => state,
|
||||
}
|
||||
})
|
||||
|
||||
),
|
||||
xoaUpdaterLog: combineActionHandlers([], {
|
||||
[actions.xoaUpdaterLog]: (_, log) => log,
|
||||
}),
|
||||
xoaRegisterState: combineActionHandlers(
|
||||
{ state: '?' },
|
||||
{
|
||||
[actions.xoaRegisterState]: (_, registration) => registration,
|
||||
}
|
||||
),
|
||||
xoaConfiguration: combineActionHandlers(
|
||||
{ proxyHost: '', proxyPort: '', proxyUser: '' },
|
||||
{
|
||||
// defined values for controlled inputs
|
||||
[actions.xoaConfiguration]: (_, configuration) => {
|
||||
delete configuration.password
|
||||
return configuration
|
||||
},
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
@@ -7,36 +7,24 @@ import Link from './link'
|
||||
|
||||
const STYLE = {
|
||||
marginBottom: '1em',
|
||||
marginLeft: '1em'
|
||||
marginLeft: '1em',
|
||||
}
|
||||
|
||||
const TabButton = ({
|
||||
labelId,
|
||||
...props
|
||||
}) => (
|
||||
<ActionButton
|
||||
{...props}
|
||||
size='large'
|
||||
style={STYLE}
|
||||
><span className='hidden-md-down'>{_(labelId)}</span></ActionButton>
|
||||
const TabButton = ({ labelId, ...props }) => (
|
||||
<ActionButton {...props} size='large' style={STYLE}>
|
||||
{labelId !== undefined && (
|
||||
<span className='hidden-md-down'>{_(labelId)}</span>
|
||||
)}
|
||||
</ActionButton>
|
||||
)
|
||||
export { TabButton as default }
|
||||
|
||||
export const TabButtonLink = ({
|
||||
labelId,
|
||||
icon,
|
||||
...props
|
||||
}) => (
|
||||
<Link
|
||||
{...props}
|
||||
className='btn btn-lg btn-primary'
|
||||
style={STYLE}
|
||||
>
|
||||
export const TabButtonLink = ({ labelId, icon, ...props }) => (
|
||||
<Link {...props} className='btn btn-lg btn-primary' style={STYLE}>
|
||||
<span className='hidden-md-down'>
|
||||
{icon && (
|
||||
<span>
|
||||
<Icon icon={icon} />
|
||||
{' '}
|
||||
<Icon icon={icon} />{' '}
|
||||
</span>
|
||||
)}
|
||||
{_(labelId)}
|
||||
|
||||
@@ -5,11 +5,11 @@ import React from 'react'
|
||||
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const INPUT_STYLE = {
|
||||
margin: '2px',
|
||||
maxWidth: '4em'
|
||||
maxWidth: '4em',
|
||||
}
|
||||
const TAG_STYLE = {
|
||||
backgroundColor: '#2598d9',
|
||||
@@ -19,26 +19,30 @@ const TAG_STYLE = {
|
||||
margin: '0.2em',
|
||||
marginTop: '-0.1em',
|
||||
padding: '0.3em',
|
||||
verticalAlign: 'middle'
|
||||
verticalAlign: 'middle',
|
||||
}
|
||||
const LINK_STYLE = {
|
||||
cursor: 'pointer',
|
||||
}
|
||||
const ADD_TAG_STYLE = {
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8em',
|
||||
marginLeft: '0.2em'
|
||||
marginLeft: '0.2em',
|
||||
}
|
||||
const REMOVE_TAG_STYLE = {
|
||||
cursor: 'pointer'
|
||||
cursor: 'pointer',
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
labels: propTypes.arrayOf(React.PropTypes.string).isRequired,
|
||||
onAdd: propTypes.func,
|
||||
onChange: propTypes.func,
|
||||
onClick: propTypes.func,
|
||||
onDelete: propTypes.func,
|
||||
onAdd: propTypes.func
|
||||
})
|
||||
export default class Tags extends Component {
|
||||
componentWillMount () {
|
||||
this.setState({editing: false})
|
||||
this.setState({ editing: false })
|
||||
}
|
||||
|
||||
_startEdit = () => {
|
||||
@@ -53,7 +57,7 @@ export default class Tags extends Component {
|
||||
|
||||
if (!includes(labels, newTag)) {
|
||||
onAdd && onAdd(newTag)
|
||||
onChange && onChange([ ...labels, newTag ])
|
||||
onChange && onChange([...labels, newTag])
|
||||
}
|
||||
}
|
||||
_deleteTag = tag => {
|
||||
@@ -81,29 +85,29 @@ export default class Tags extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
labels,
|
||||
onAdd,
|
||||
onChange,
|
||||
onDelete
|
||||
} = this.props
|
||||
const { labels, onAdd, onChange, onClick, onDelete } = this.props
|
||||
|
||||
const deleteTag = (onDelete || onChange) && this._deleteTag
|
||||
|
||||
return (
|
||||
<span className='form-group' style={{ color: '#999' }}>
|
||||
<Icon icon='tags' />
|
||||
{' '}
|
||||
<Icon icon='tags' />{' '}
|
||||
<span>
|
||||
{map(labels.sort(), (label, index) =>
|
||||
<Tag label={label} onDelete={deleteTag} key={index} />
|
||||
)}
|
||||
{map(labels.sort(), (label, index) => (
|
||||
<Tag
|
||||
label={label}
|
||||
onDelete={deleteTag}
|
||||
key={index}
|
||||
onClick={onClick}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
{(onAdd || onChange) && !this.state.editing
|
||||
? <span onClick={this._startEdit} style={ADD_TAG_STYLE}>
|
||||
{(onAdd || onChange) && !this.state.editing ? (
|
||||
<span onClick={this._startEdit} style={ADD_TAG_STYLE}>
|
||||
<Icon icon='add-tag' />
|
||||
</span>
|
||||
: <span>
|
||||
) : (
|
||||
<span>
|
||||
<input
|
||||
type='text'
|
||||
autoFocus
|
||||
@@ -112,23 +116,32 @@ export default class Tags extends Component {
|
||||
onBlur={this._stopEdit}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const Tag = ({ label, onDelete }) => (
|
||||
export const Tag = ({ type, label, onDelete, onClick }) => (
|
||||
<span style={TAG_STYLE}>
|
||||
{label}{' '}
|
||||
{onDelete
|
||||
? <span onClick={onDelete && (() => onDelete(label))} style={REMOVE_TAG_STYLE}>
|
||||
<span
|
||||
onClick={onClick && (() => onClick(label))}
|
||||
style={onClick && LINK_STYLE}
|
||||
>
|
||||
{label}
|
||||
</span>{' '}
|
||||
{onDelete ? (
|
||||
<span
|
||||
onClick={onDelete && (() => onDelete(label))}
|
||||
style={REMOVE_TAG_STYLE}
|
||||
>
|
||||
<Icon icon='remove-tag' />
|
||||
</span>
|
||||
: []
|
||||
}
|
||||
) : (
|
||||
[]
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
Tag.propTypes = {
|
||||
label: React.PropTypes.string.isRequired
|
||||
label: React.PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
0
src/common/themes/.index-modules
Normal file
0
src/common/themes/.index-modules
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user