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

|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ function browserify (path, opts) {
|
||||
|
||||
var 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',
|
||||
@@ -257,7 +257,8 @@ gulp.task(function buildScripts () {
|
||||
}]
|
||||
]
|
||||
}),
|
||||
PRODUCTION && require('gulp-uglify')(),
|
||||
require('gulp-sourcemaps').init({ loadMaps: true }),
|
||||
PRODUCTION && require('gulp-uglify/composer')(require('uglify-es'))(),
|
||||
dest()
|
||||
)
|
||||
})
|
||||
|
||||
138
package.json
138
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.3.0",
|
||||
"version": "5.13.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -31,10 +31,12 @@
|
||||
"npm": ">=3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nraynaud/novnc": "^0.6.1-1",
|
||||
"ansi_up": "^1.3.0",
|
||||
"asap": "^2.0.4",
|
||||
"ava": "^0.16.0",
|
||||
"asap": "^2.0.6",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"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,85 +46,98 @@
|
||||
"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",
|
||||
"babel-register": "^6.26.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"babelify": "^7.2.0",
|
||||
"benchmark": "^2.1.0",
|
||||
"bootstrap": "github:twbs/bootstrap#v4-dev",
|
||||
"browserify": "^13.0.0",
|
||||
"bundle-collapser": "^1.2.1",
|
||||
"chartist-plugin-legend": "^0.5.0",
|
||||
"bootstrap": "4.0.0-alpha.5",
|
||||
"browserify": "^14.1.0",
|
||||
"bundle-collapser": "^1.3.0",
|
||||
"chartist": "^0.10.1",
|
||||
"chartist-plugin-legend": "^0.6.1",
|
||||
"chartist-plugin-tooltip": "0.0.11",
|
||||
"classnames": "^2.2.3",
|
||||
"cookies-js": "^1.2.2",
|
||||
"d3": "^4.0.0-alpha.50",
|
||||
"d3": "^4.10.2",
|
||||
"dependency-check": "^2.5.1",
|
||||
"event-to-promise": "^0.7.0",
|
||||
"font-awesome": "^4.5.0",
|
||||
"enzyme": "^2.9.1",
|
||||
"enzyme-to-json": "^1.4.4",
|
||||
"event-to-promise": "^0.8.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"font-mfizz": "github:fizzed/font-mfizz",
|
||||
"get-stream": "^2.3.0",
|
||||
"ghooks": "^1.1.1",
|
||||
"globby": "^6.0.0",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-autoprefixer": "^3.1.0",
|
||||
"gulp-csso": "^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",
|
||||
"human-format": "^0.8.0",
|
||||
"husky": "^0.14.3",
|
||||
"immutable": "^3.8.1",
|
||||
"index-modules": "^0.3.0",
|
||||
"is-ip": "^1.0.0",
|
||||
"jsonrpc-websocket-client": "0.0.1-5",
|
||||
"jest": "^21.0.2",
|
||||
"jsonrpc-websocket-client": "^0.1.1",
|
||||
"kindof": "^2.0.0",
|
||||
"later": "^1.2.0",
|
||||
"lodash": "^4.6.1",
|
||||
"loose-envify": "^1.1.0",
|
||||
"make-error": "^1.2.1",
|
||||
"marked": "^0.3.5",
|
||||
"modular-css": "^0.27.1",
|
||||
"modular-css": "^6.0.2",
|
||||
"moment": "^2.13.0",
|
||||
"moment-timezone": "^0.5.4",
|
||||
"notifyjs": "^2.0.1",
|
||||
"novnc-node": "^0.5.3",
|
||||
"promise-toolbox": "^0.6.0",
|
||||
"notifyjs": "^3.0.0",
|
||||
"promise-toolbox": "^0.9.5",
|
||||
"random-password": "^0.1.2",
|
||||
"react": "^15.0.0",
|
||||
"react": "^15.4.1",
|
||||
"react-addons-shallow-compare": "^15.1.0",
|
||||
"react-addons-test-utils": "^15.4.1",
|
||||
"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.0",
|
||||
"react-debounce-input": "^3.0.1",
|
||||
"react-dnd": "^2.5.1",
|
||||
"react-dnd-html5-backend": "^2.5.1",
|
||||
"react-document-title": "^2.0.2",
|
||||
"react-dom": "^15.0.0",
|
||||
"react-dropzone": "^3.5.0",
|
||||
"react-intl": "^2.0.1",
|
||||
"react-key-handler": "^0.3.0",
|
||||
"react-notify": "^2.0.1",
|
||||
"react-redux": "^4.4.0",
|
||||
"react-router": "^3.0.0-alpha.1",
|
||||
"react-select": "^1.0.0-beta13",
|
||||
"react-shortcuts": "^1.0.7",
|
||||
"react-sparklines": "^1.5.0",
|
||||
"react-dom": "^15.4.1",
|
||||
"react-dropzone": "^4.1.2",
|
||||
"react-intl": "^2.4.0",
|
||||
"react-key-handler": "^1.0.0",
|
||||
"react-notify": "^3.0.0",
|
||||
"react-overlays": "^0.8.0",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-router": "^3.0.0",
|
||||
"react-select": "^1.0.0-rc.8",
|
||||
"react-shortcuts": "^1.6.1",
|
||||
"react-sparklines": "1.6.0",
|
||||
"react-virtualized": "^8.0.8",
|
||||
"readable-stream": "^2.0.6",
|
||||
"redux": "^3.3.1",
|
||||
"readable-stream": "^2.3.3",
|
||||
"redux": "^3.7.2",
|
||||
"redux-devtools": "^3.1.1",
|
||||
"redux-devtools-dock-monitor": "^1.1.0",
|
||||
"redux-devtools-log-monitor": "^1.0.5",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.2.1",
|
||||
"standard": "^8.4.0",
|
||||
"superagent": "^2.0.0",
|
||||
"reselect": "^2.5.4",
|
||||
"semver": "^5.4.1",
|
||||
"standard": "^10.0.3",
|
||||
"styled-components": "^2.1.2",
|
||||
"superagent": "^3.6.0",
|
||||
"tar-stream": "^1.5.2",
|
||||
"vinyl": "^2.0.0",
|
||||
"uglify-es": "^3.1.0",
|
||||
"uncontrollable-input": "^0.0.1",
|
||||
"vinyl": "^2.1.0",
|
||||
"watchify": "^3.7.0",
|
||||
"xml2js": "^0.4.17",
|
||||
"xo-acl-resolver": "^0.2.2",
|
||||
"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"
|
||||
},
|
||||
@@ -130,12 +145,13 @@
|
||||
"benchmarks": "./tools/run-benchmarks.js 'src/**/*.bench.js'",
|
||||
"build": "npm run build-indexes && NODE_ENV=production gulp build",
|
||||
"build-indexes": "index-modules --auto src",
|
||||
"commitmsg": "npm test",
|
||||
"dev": "npm run build-indexes && NODE_ENV=development gulp build",
|
||||
"dev-test": "ava --watch",
|
||||
"dev-test": "jest --watch",
|
||||
"lint": "standard",
|
||||
"posttest": "npm run lint",
|
||||
"prepublish": "npm run build",
|
||||
"test": "ava"
|
||||
"test": "jest"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
@@ -143,15 +159,6 @@
|
||||
"loose-envify"
|
||||
]
|
||||
},
|
||||
"ava": {
|
||||
"babel": "inherit",
|
||||
"files": [
|
||||
"src/**/*.spec.js"
|
||||
],
|
||||
"require": [
|
||||
"babel-register"
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"env": {
|
||||
"development": {
|
||||
@@ -168,6 +175,8 @@
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"dev",
|
||||
"lodash",
|
||||
"transform-decorators-legacy",
|
||||
"transform-runtime"
|
||||
],
|
||||
@@ -177,12 +186,15 @@
|
||||
"stage-0"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"ghooks": {
|
||||
"commit-msg": "npm test"
|
||||
}
|
||||
"jest": {
|
||||
"snapshotSerializers": [
|
||||
"enzyme-to-json/serializer"
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"globals": [
|
||||
"__DEV__"
|
||||
],
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
|
||||
@@ -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,50 @@
|
||||
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,59 +1,82 @@
|
||||
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,
|
||||
|
||||
// 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
|
||||
]),
|
||||
size: propTypes.oneOf([
|
||||
'large',
|
||||
'small'
|
||||
]),
|
||||
|
||||
// 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 {
|
||||
children,
|
||||
handler,
|
||||
handlerParam
|
||||
handlerParam,
|
||||
tooltip
|
||||
} = this.props
|
||||
|
||||
try {
|
||||
this.setState({
|
||||
error: null,
|
||||
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({
|
||||
@@ -68,6 +91,7 @@ export default class ActionButton extends Component {
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,28 +121,30 @@ export default class ActionButton extends Component {
|
||||
render () {
|
||||
const {
|
||||
props: {
|
||||
btnStyle,
|
||||
children,
|
||||
className,
|
||||
disabled,
|
||||
form,
|
||||
icon,
|
||||
size: bsSize,
|
||||
style,
|
||||
tooltip
|
||||
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 />
|
||||
if (error !== undefined) {
|
||||
props.btnStyle = 'warning'
|
||||
}
|
||||
if (pending || working) {
|
||||
props.disabled = true
|
||||
}
|
||||
delete props.handler
|
||||
delete props.handlerParam
|
||||
if (props.form === undefined) {
|
||||
props.onClick = this._execute
|
||||
}
|
||||
delete props.redirectOnSuccess
|
||||
|
||||
const button = <Button {...props}>
|
||||
<Icon icon={pending || working ? 'loading' : icon} fixedWidth />
|
||||
{children && ' '}
|
||||
{children}
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 }) =>
|
||||
<ActionButton
|
||||
|
||||
@@ -1,30 +1,19 @@
|
||||
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 +25,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 +35,30 @@ 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
|
||||
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 {
|
||||
@@ -93,9 +82,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
|
||||
}
|
||||
|
||||
@@ -112,13 +101,6 @@ export default class BaseComponent extends Component {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
shouldComponentUpdate (newProps, newState) {
|
||||
return !(
|
||||
shallowEqual(this.props, newProps) &&
|
||||
shallowEqual(this.state, newState)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (VERBOSE) {
|
||||
|
||||
8
src/common/button-group.js
Normal file
8
src/common/button-group.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
|
||||
const ButtonGroup = ({ children }) =>
|
||||
<div className='btn-group' role='group'>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
export { ButtonGroup as default }
|
||||
56
src/common/button.js
Normal file
56
src/common/button.js
Normal file
@@ -0,0 +1,56 @@
|
||||
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,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const CARD_STYLE = {
|
||||
minHeight: '100%'
|
||||
@@ -17,16 +17,16 @@ const CARD_HEADER_STYLE = {
|
||||
}
|
||||
|
||||
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,
|
||||
...props
|
||||
}) => {
|
||||
props.className = 'card'
|
||||
props.style = shadow ? CARD_STYLE_WITH_SHADOW : CARD_STYLE
|
||||
|
||||
return <div {...props} />
|
||||
})
|
||||
|
||||
export const CardHeader = propTypes({
|
||||
className: propTypes.string
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
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
|
||||
@@ -22,9 +28,9 @@ export default class Collapse extends Component {
|
||||
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<button className='btn btn-lg btn-primary btn-block' onClick={this._onClick}>
|
||||
<Button block btnStyle='primary' size='large' onClick={this._onClick}>
|
||||
{props.buttonText} <Icon icon={`chevron-${isOpened ? 'up' : 'down'}`} />
|
||||
</button>
|
||||
</Button>
|
||||
{isOpened && props.children}
|
||||
</div>
|
||||
)
|
||||
|
||||
64
src/common/combobox.js
Normal file
64
src/common/combobox.js
Normal file
@@ -0,0 +1,64 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,11 @@ import {
|
||||
createOr,
|
||||
createNot,
|
||||
createProperty,
|
||||
createString
|
||||
createString,
|
||||
createTruthyProperty
|
||||
} from './'
|
||||
|
||||
export const pattern = 'foo !"\\\\ \\"" name:|(wonderwoman batman)'
|
||||
export const pattern = 'foo !"\\\\ \\"" name:|(wonderwoman batman) hasCape?'
|
||||
|
||||
export const ast = createAnd([
|
||||
createString('foo'),
|
||||
@@ -14,5 +15,6 @@ export const ast = createAnd([
|
||||
createProperty('name', createOr([
|
||||
createString('wonderwoman'),
|
||||
createString('batman')
|
||||
]))
|
||||
])),
|
||||
createTruthyProperty('hasCape')
|
||||
])
|
||||
|
||||
@@ -56,19 +56,22 @@ export const createProperty = (name, child) => ({ type: 'property', name, child
|
||||
|
||||
export const createString = value => ({ type: 'string', value })
|
||||
|
||||
export const createTruthyProperty = name => ({ type: 'truthyProperty', name })
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// *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
|
||||
// *and = terms
|
||||
// terms = term+
|
||||
// term = ws (groupedAnd | or | not | property | truthyProperty | string) ws
|
||||
// ws = ' '*
|
||||
// groupedAnd = "(" and ")"
|
||||
// *or = "|" ws "(" terms ")"
|
||||
// *not = "!" term
|
||||
// *property = string ws ":" term
|
||||
// *truthyProperty = string ws "?"
|
||||
// *string = quotedString | rawString
|
||||
// quotedString = "\"" ( /[^"\]/ | "\\\\" | "\\\"" )+
|
||||
// rawString = /[a-z0-9-_.]+/i
|
||||
export const parse = invoke(() => {
|
||||
let i
|
||||
let n
|
||||
@@ -108,6 +111,7 @@ export const parse = invoke(() => {
|
||||
parseOr() ||
|
||||
parseNot() ||
|
||||
parseProperty() ||
|
||||
parseTruthyProperty() ||
|
||||
parseString()
|
||||
)
|
||||
if (child) {
|
||||
@@ -203,6 +207,16 @@ export const parse = invoke(() => {
|
||||
return value
|
||||
}
|
||||
}
|
||||
const parseTruthyProperty = backtrace(() => {
|
||||
let name
|
||||
if (
|
||||
(name = parseString()) &&
|
||||
parseWs() &&
|
||||
input[i++] === '?'
|
||||
) {
|
||||
return createTruthyProperty(name.value)
|
||||
}
|
||||
})
|
||||
|
||||
return input_ => {
|
||||
if (!input_) {
|
||||
@@ -280,10 +294,10 @@ export const getPropertyClausesStrings = function () {
|
||||
|
||||
export const removePropertyClause = function (name) {
|
||||
let type
|
||||
if (
|
||||
!this ||
|
||||
(type = this.type) === 'property' && this.name === name
|
||||
) {
|
||||
if (!this || (
|
||||
(type = this.type) === 'property' &&
|
||||
this.name === name
|
||||
)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -321,7 +335,7 @@ export const setPropertyClause = function (name, child) {
|
||||
return _addAndClause(
|
||||
this,
|
||||
property,
|
||||
node => node.type === 'property' && node.name === name,
|
||||
node => node.type === 'property' && node.name === name
|
||||
)
|
||||
}
|
||||
|
||||
@@ -341,6 +355,7 @@ export const execute = invoke(() => {
|
||||
property: ({ name, child }, value) => (
|
||||
value != null && child::execute(value[name])
|
||||
),
|
||||
truthyProperty: ({ name }, value) => !!value[name],
|
||||
string: invoke(() => {
|
||||
const match = (pattern, value) => {
|
||||
if (isString(value)) {
|
||||
@@ -378,7 +393,8 @@ export const toString = invoke(() => {
|
||||
property: ({ name, child }) => `${toString(createString(name))}:${toString(child)}`,
|
||||
string: ({ value }) => isRawString(value)
|
||||
? value
|
||||
: `"${value.replace(/\\|"/g, match => `\\${match}`)}"`
|
||||
: `"${value.replace(/\\|"/g, match => `\\${match}`)}"`,
|
||||
truthyProperty: ({ name }) => `${toString(createString(name))}?`
|
||||
}
|
||||
|
||||
const toString = node => visitors[node.type](node)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import test from 'ava'
|
||||
/* eslint-env jest */
|
||||
|
||||
import {
|
||||
getPropertyClausesStrings,
|
||||
@@ -11,43 +11,36 @@ import {
|
||||
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' ]
|
||||
}
|
||||
)
|
||||
it('getPropertyClausesStrings', () => {
|
||||
const tmp = parse('foo bar:baz baz:|(foo bar)')::getPropertyClausesStrings()
|
||||
expect(tmp).toEqual({
|
||||
bar: [ 'baz' ],
|
||||
baz: [ 'foo', 'bar' ]
|
||||
})
|
||||
})
|
||||
|
||||
test('parse', t => {
|
||||
t.deepEqual(parse(pattern), ast)
|
||||
it('parse', () => {
|
||||
expect(parse(pattern)).toEqual(ast)
|
||||
})
|
||||
|
||||
test('setPropertyClause', t => {
|
||||
t.is(
|
||||
null::setPropertyClause('foo', 'bar')::toString(),
|
||||
'foo:bar'
|
||||
)
|
||||
it('setPropertyClause', () => {
|
||||
expect(
|
||||
null::setPropertyClause('foo', 'bar')::toString()
|
||||
).toBe('foo:bar')
|
||||
|
||||
t.is(
|
||||
parse('baz')::setPropertyClause('foo', 'bar')::toString(),
|
||||
'baz foo:bar'
|
||||
)
|
||||
expect(
|
||||
parse('baz')::setPropertyClause('foo', 'bar')::toString()
|
||||
).toBe('baz foo:bar')
|
||||
|
||||
t.is(
|
||||
parse('plip foo:baz plop')::setPropertyClause('foo', 'bar')::toString(),
|
||||
'plip plop foo:bar'
|
||||
)
|
||||
expect(
|
||||
parse('plip foo:baz plop')::setPropertyClause('foo', 'bar')::toString()
|
||||
).toBe('plip plop foo:bar')
|
||||
|
||||
t.is(
|
||||
parse('foo:|(baz plop)')::setPropertyClause('foo', 'bar')::toString(),
|
||||
'foo:bar'
|
||||
)
|
||||
expect(
|
||||
parse('foo:|(baz plop)')::setPropertyClause('foo', 'bar')::toString()
|
||||
).toBe('foo:bar')
|
||||
})
|
||||
|
||||
test('toString', t => {
|
||||
t.is(pattern, ast::toString())
|
||||
it('toString', () => {
|
||||
expect(pattern).toBe(ast::toString())
|
||||
})
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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'
|
||||
|
||||
@@ -22,9 +23,9 @@ const Copiable = propTypes({
|
||||
' ',
|
||||
<Tooltip content={_('copyToClipboard')}>
|
||||
<CopyToClipboard text={props.data || props.children}>
|
||||
<button className={classNames('btn btn-sm btn-secondary', styles.button)}>
|
||||
<Button className={styles.button} size='small'>
|
||||
<Icon icon='clipboard' />
|
||||
</button>
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
))
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { Component, PropTypes } 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 {
|
||||
|
||||
22
src/common/dropzone/index.css
Normal file
22
src/common/dropzone/index.css
Normal file
@@ -0,0 +1,22 @@
|
||||
@value dropzoneColor: #8f8686;
|
||||
|
||||
.dropzone {
|
||||
border-radius: 4px;
|
||||
border: 2px dashed dropzoneColor;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 12em;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.activeDropzone {
|
||||
background: #f0f0f0;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.dropzoneText {
|
||||
color: dropzoneColor;
|
||||
font-size: 1.2em;
|
||||
margin: auto;
|
||||
}
|
||||
20
src/common/dropzone/index.js
Normal file
20
src/common/dropzone/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Component from 'base-component'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import ReactDropzone from 'react-dropzone'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@propTypes({
|
||||
onDrop: propTypes.func,
|
||||
message: propTypes.node
|
||||
})
|
||||
export default class Dropzone extends Component {
|
||||
render () {
|
||||
const { onDrop, message } = this.props
|
||||
|
||||
return <ReactDropzone onDrop={onDrop} className={styles.dropzone} activeClassName={styles.activeDropzone}>
|
||||
<div className={styles.dropzoneText}>{message}</div>
|
||||
</ReactDropzone>
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
@@ -390,15 +390,14 @@ const MAP_TYPE_SELECT = {
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
labelProp: propTypes.string.isRequired,
|
||||
value: propTypes.oneOfType([
|
||||
propTypes.string,
|
||||
propTypes.object
|
||||
]).isRequired
|
||||
])
|
||||
})
|
||||
export class XoSelect extends Editable {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
return this.state.value
|
||||
}
|
||||
|
||||
_renderDisplay () {
|
||||
@@ -406,9 +405,8 @@ export class XoSelect extends Editable {
|
||||
<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 {
|
||||
@@ -432,7 +430,6 @@ export class XoSelect extends Editable {
|
||||
autoFocus
|
||||
disabled={saving}
|
||||
onChange={this._onChange}
|
||||
ref='select'
|
||||
/>
|
||||
</a>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import test from 'ava'
|
||||
/* eslint-env jest */
|
||||
|
||||
import filterReduce from './filter-reduce'
|
||||
|
||||
@@ -6,23 +6,20 @@ const add = (a, b) => a + b
|
||||
const data = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
|
||||
const isEven = x => !(x & 1)
|
||||
|
||||
test('filterReduce', t => {
|
||||
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),
|
||||
expect(filterReduce(data, isEven, add)).toEqual(
|
||||
[ 1, 3, 5, 7, 9, 20 ]
|
||||
)
|
||||
|
||||
// The default reducer is the identity.
|
||||
t.deepEqual(
|
||||
filterReduce(data, isEven),
|
||||
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),
|
||||
expect(filterReduce(data, isEven, add, 22)).toEqual(
|
||||
[ 1, 3, 5, 7, 9, 42 ]
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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
|
||||
|
||||
@@ -5,15 +5,19 @@ import map from 'lodash/map'
|
||||
import randomPassword from 'random-password'
|
||||
import React from 'react'
|
||||
import round from 'lodash/round'
|
||||
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 defined from '../xo-defined'
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import {
|
||||
firstDefined,
|
||||
formatSizeRaw,
|
||||
parseSize
|
||||
} from '../utils'
|
||||
@@ -36,7 +40,16 @@ export class Password extends Component {
|
||||
}
|
||||
|
||||
_generate = () => {
|
||||
this.refs.field.value = randomPassword(8)
|
||||
const value = randomPassword(8)
|
||||
const isControlled = this.props.value !== undefined
|
||||
if (isControlled) {
|
||||
this.props.onChange(value)
|
||||
} else {
|
||||
this.refs.field.value = value
|
||||
}
|
||||
|
||||
// FIXME: in controlled mode, visibility should only be updated
|
||||
// when the value prop is changed according to the emitted value.
|
||||
this.setState({
|
||||
visible: true
|
||||
})
|
||||
@@ -58,9 +71,9 @@ export class Password extends Component {
|
||||
|
||||
return <div className='input-group'>
|
||||
{enableGenerator && <span className='input-group-btn'>
|
||||
<button type='button' className='btn btn-secondary' onClick={this._generate}>
|
||||
<Button onClick={this._generate}>
|
||||
<Icon icon='password' />
|
||||
</button>
|
||||
</Button>
|
||||
</span>}
|
||||
<input
|
||||
{...props}
|
||||
@@ -69,9 +82,9 @@ export class Password extends Component {
|
||||
type={visible ? 'text' : 'password'}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<button type='button' className='btn btn-secondary' onClick={this._toggleVisibility}>
|
||||
<Button onClick={this._toggleVisibility}>
|
||||
<Icon icon={visible ? 'shown' : 'hidden'} />
|
||||
</button>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@@ -80,68 +93,45 @@ export class Password extends Component {
|
||||
// ===================================================================
|
||||
|
||||
@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'>
|
||||
return <Container>
|
||||
<SingleLineRow>
|
||||
<Col size={2}>
|
||||
<span className='pull-right'>{value}</span>
|
||||
</Col>
|
||||
<Col size={10}>
|
||||
<input
|
||||
className='form-control'
|
||||
type='range'
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
max={max}
|
||||
min={min}
|
||||
onChange={this._onChange}
|
||||
step={step}
|
||||
type='range'
|
||||
value={value}
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</Container>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +158,7 @@ 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) {
|
||||
|
||||
@@ -1,28 +1,42 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,25 +50,10 @@ export default class SelectPlainObject extends Component {
|
||||
}
|
||||
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
|
||||
@@ -64,10 +63,13 @@ export default class SelectPlainObject extends Component {
|
||||
}))
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -80,18 +82,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 +107,8 @@ export default class SelectPlainObject extends Component {
|
||||
placeholder={props.placeholder}
|
||||
required={props.required}
|
||||
value={state.value}
|
||||
valueRenderer={this._renderOption} />
|
||||
valueRenderer={this._renderOption}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
List
|
||||
} from 'react-virtualized'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
const SELECT_MENU_STYLE = {
|
||||
overflow: 'hidden'
|
||||
@@ -18,6 +18,10 @@ const SELECT_STYLE = {
|
||||
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
|
||||
@@ -75,6 +79,7 @@ export default class Select extends Component {
|
||||
rowHeight={getRowHeight}
|
||||
rowRenderer={wrappedRowRenderer}
|
||||
scrollToIndex={focusedOptionIndex}
|
||||
style={LIST_STYLE}
|
||||
width={width}
|
||||
/>
|
||||
}}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
export const Col = propTypes({
|
||||
className: propTypes.string,
|
||||
|
||||
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,4 +1,9 @@
|
||||
const common = {
|
||||
homeFilterNone: ''
|
||||
}
|
||||
|
||||
export const VM = {
|
||||
...common,
|
||||
homeFilterPendingVms: 'current_operations:"" ',
|
||||
homeFilterNonRunningVms: '!power_state:running ',
|
||||
homeFilterHvmGuests: 'virtualizationMode:hvm ',
|
||||
@@ -7,14 +12,22 @@ export const VM = {
|
||||
}
|
||||
|
||||
export const host = {
|
||||
...common,
|
||||
homeFilterRunningHosts: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const pool = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const vmTemplate = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const SR = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
36
src/common/home-tags.js
Normal file
36
src/common/home-tags.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import Tags from './tags'
|
||||
import { createString, createProperty, toString } from './complex-matcher'
|
||||
|
||||
@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(createProperty('tags', createString(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}
|
||||
/>
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ 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'
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
createSelector
|
||||
} from './selectors'
|
||||
import {
|
||||
getHostMissingPatches,
|
||||
installAllHostPatches,
|
||||
installAllPatchesOnPool
|
||||
installAllPatchesOnPool,
|
||||
subscribeHostMissingPatches
|
||||
} from './xo'
|
||||
|
||||
// ===================================================================
|
||||
@@ -64,6 +64,15 @@ const POOLS_MISSING_PATCHES_COLUMNS = [{
|
||||
sortCriteria: (host, { pools }) => pools[host.$pool].name_label
|
||||
}].concat(MISSING_PATCHES_COLUMNS)
|
||||
|
||||
// Small component to homogenize Button usage in HostsPatchesTable
|
||||
const ActionButton_ = ({ children, labelId, ...props }) =>
|
||||
<ActionButton
|
||||
{...props}
|
||||
tooltip={_(labelId)}
|
||||
>
|
||||
{children}
|
||||
</ActionButton>
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class HostsPatchesTable extends Component {
|
||||
@@ -80,11 +89,25 @@ class HostsPatchesTable extends Component {
|
||||
)
|
||||
)
|
||||
|
||||
_refreshMissingPatches = () => (
|
||||
Promise.all(
|
||||
map(this.props.hosts, this._refreshHostMissingPatches)
|
||||
_subscribeMissingPatches = (hosts = this.props.hosts) => {
|
||||
const unsubs = map(hosts, host =>
|
||||
subscribeHostMissingPatches(
|
||||
host,
|
||||
patches => this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[host.id]: patches.length
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (this.unsubscribeMissingPatches !== undefined) {
|
||||
this.unsubscribeMissingPatches()
|
||||
}
|
||||
|
||||
this.unsubscribeMissingPatches = () => forEach(unsubs, unsub => unsub())
|
||||
}
|
||||
|
||||
_installAllMissingPatches = () => {
|
||||
const pools = {}
|
||||
@@ -95,80 +118,43 @@ class HostsPatchesTable extends Component {
|
||||
return Promise.all(map(
|
||||
keys(pools),
|
||||
installAllPatchesOnPool
|
||||
)).then(this._refreshMissingPatches)
|
||||
}
|
||||
|
||||
_refreshHostMissingPatches = host => (
|
||||
getHostMissingPatches(host).then(patches => {
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[host.id]: patches.length
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
_installAllHostPatches = host => (
|
||||
installAllHostPatches(host).then(() =>
|
||||
this._refreshHostMissingPatches(host)
|
||||
)
|
||||
)
|
||||
|
||||
componentWillMount () {
|
||||
this._refreshMissingPatches()
|
||||
))
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
// Force one Portal refresh.
|
||||
// Because Portal cannot see the container reference at first rendering.
|
||||
this.forceUpdate()
|
||||
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>
|
||||
@@ -176,17 +162,25 @@ class HostsPatchesTable extends Component {
|
||||
? (
|
||||
<SortedTable
|
||||
collection={hosts}
|
||||
columns={props.displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
|
||||
columns={displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
|
||||
userData={{
|
||||
installAllHostPatches: this._installAllHostPatches,
|
||||
installAllHostPatches,
|
||||
missingPatches: this.state.missingPatches,
|
||||
pools: props.pools
|
||||
pools
|
||||
}}
|
||||
/>
|
||||
) : <p>{_('patchNothing')}</p>
|
||||
}
|
||||
<Portal container={() => props.buttonsGroupContainer()}>
|
||||
{Buttons}
|
||||
<Portal container={() => buttonsGroupContainer()}>
|
||||
<Container>
|
||||
<Button
|
||||
btnStyle='primary'
|
||||
disabled={noPatches}
|
||||
handler={this._installAllMissingPatches}
|
||||
icon='host-patch-update'
|
||||
labelId='installPoolPatches'
|
||||
/>
|
||||
</Container>
|
||||
</Portal>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
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
|
||||
|
||||
@@ -48,6 +48,10 @@ const getMessage = (props, messageId, values, render) => {
|
||||
{render}
|
||||
</FormattedMessage>
|
||||
}
|
||||
getMessage.keyValue = (key, value) => getMessage('keyValue', {
|
||||
key: <strong>{key}</strong>,
|
||||
value
|
||||
})
|
||||
|
||||
export { getMessage as default }
|
||||
|
||||
@@ -62,7 +66,13 @@ export class IntlProvider extends Component {
|
||||
|
||||
render () {
|
||||
const { lang, children } = this.props
|
||||
// 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]}
|
||||
>
|
||||
|
||||
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
File diff suppressed because it is too large
Load Diff
3592
src/common/intl/locales/hu.js
Normal file
3592
src/common/intl/locales/hu.js
Normal file
File diff suppressed because it is too large
Load Diff
3139
src/common/intl/locales/pl.js
Normal file
3139
src/common/intl/locales/pl.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,8 @@ var forEach = require('lodash/forEach')
|
||||
var isString = require('lodash/isString')
|
||||
|
||||
var messages = {
|
||||
keyValue: '{key}: {value}',
|
||||
|
||||
statusConnecting: 'Connecting',
|
||||
statusDisconnected: 'Disconnected',
|
||||
statusLoading: 'Loading…',
|
||||
@@ -13,15 +15,23 @@ var messages = {
|
||||
|
||||
editableLongClickPlaceholder: 'Long click to edit',
|
||||
editableClickPlaceholder: 'Click to edit',
|
||||
browseFiles: 'Browse files',
|
||||
|
||||
// ----- Modals -----
|
||||
alertOk: 'OK',
|
||||
confirmOk: 'OK',
|
||||
confirmCancel: 'Cancel',
|
||||
genericCancel: 'Cancel',
|
||||
|
||||
// ----- Filters -----
|
||||
onError: 'On error',
|
||||
successful: 'Successful',
|
||||
filterOnlyManaged: 'Managed disks',
|
||||
filterOnlyOrphaned: 'Orphaned disks',
|
||||
filterOnlyRegular: 'Normal disks',
|
||||
filterOnlySnapshots: 'Snapshot disks',
|
||||
filterOnlyUnmanaged: 'Unmanaged disks',
|
||||
filterSaveAs: 'Save…',
|
||||
filterSyntaxLinkTooltip: 'Explore the search syntax in the documentation',
|
||||
|
||||
// ----- Copiable component -----
|
||||
copyToClipboard: 'Copy to clipboard',
|
||||
@@ -35,6 +45,7 @@ var messages = {
|
||||
homeHostPage: 'Hosts',
|
||||
homePoolPage: 'Pools',
|
||||
homeTemplatePage: 'Templates',
|
||||
homeSrPage: 'Storages',
|
||||
dashboardPage: 'Dashboard',
|
||||
overviewDashboardPage: 'Overview',
|
||||
overviewVisualizationDashboardPage: 'Visualizations',
|
||||
@@ -52,7 +63,9 @@ var messages = {
|
||||
settingsPluginsPage: 'Plugins',
|
||||
settingsLogsPage: 'Logs',
|
||||
settingsIpsPage: 'IPs',
|
||||
settingsConfigPage: 'Config',
|
||||
aboutPage: 'About',
|
||||
aboutXoaPlan: 'About XO {xoaPlan}',
|
||||
newMenu: 'New',
|
||||
taskMenu: 'Tasks',
|
||||
taskPage: 'Tasks',
|
||||
@@ -60,10 +73,12 @@ var messages = {
|
||||
newSrPage: 'Storage',
|
||||
newServerPage: 'Server',
|
||||
newImport: 'Import',
|
||||
xosan: 'XOSAN',
|
||||
backupOverviewPage: 'Overview',
|
||||
backupNewPage: 'New',
|
||||
backupRemotesPage: 'Remotes',
|
||||
backupRestorePage: 'Restore',
|
||||
backupFileRestorePage: 'File restore',
|
||||
schedule: 'Schedule',
|
||||
newVmBackup: 'New VM backup',
|
||||
editVmBackup: 'Edit VM backup',
|
||||
@@ -90,10 +105,12 @@ var messages = {
|
||||
|
||||
// ----- Home view ------
|
||||
homeFetchingData: 'Fetching data…',
|
||||
homeWelcome: 'Welcome on Xen Orchestra!',
|
||||
homeWelcome: 'Welcome to Xen Orchestra!',
|
||||
homeWelcomeText: 'Add your XenServer hosts or pools',
|
||||
homeConnectServerText: 'Some XenServers have been registered but are not connected',
|
||||
homeHelp: 'Want some help?',
|
||||
homeAddServer: 'Add server',
|
||||
homeConnectServer: 'Connect servers',
|
||||
homeOnlineDoc: 'Online Doc',
|
||||
homeProSupport: 'Pro Support',
|
||||
homeNoVms: 'There are no VMs!',
|
||||
@@ -114,7 +131,9 @@ var messages = {
|
||||
homeAllPools: 'Pools',
|
||||
homeAllHosts: 'Hosts',
|
||||
homeAllTags: 'Tags',
|
||||
homeAllResourceSets: 'Resource sets',
|
||||
homeNewVm: 'New VM',
|
||||
homeFilterNone: 'None',
|
||||
homeFilterRunningHosts: 'Running hosts',
|
||||
homeFilterDisabledHosts: 'Disabled hosts',
|
||||
homeFilterRunningVms: 'Running VMs',
|
||||
@@ -128,16 +147,32 @@ var messages = {
|
||||
homeSortByRAM: 'RAM',
|
||||
homeSortByvCPUs: 'vCPUs',
|
||||
homeSortByCpus: 'CPUs',
|
||||
homeSortByShared: 'Shared/Not shared',
|
||||
homeSortBySize: 'Size',
|
||||
homeSortByUsage: 'Usage',
|
||||
homeSortByType: 'Type',
|
||||
homeDisplayedItems: '{displayed, number}x {icon} (on {total, number})',
|
||||
homeSelectedItems: '{selected, number}x {icon} selected (on {total, number})',
|
||||
homeMore: 'More',
|
||||
homeMigrateTo: 'Migrate to…',
|
||||
homeMissingPaths: 'Missing patches',
|
||||
homePoolMaster: 'Master:',
|
||||
homeResourceSet: 'Resource set: {resourceSet}',
|
||||
highAvailability: 'High Availability',
|
||||
srSharedType: 'Shared {type}',
|
||||
srNotSharedType: 'Not shared {type}',
|
||||
|
||||
// ----- Common components -----
|
||||
sortedTableAllItemsSelected: 'All of them are selected',
|
||||
sortedTableNoItems: 'No items found',
|
||||
sortedTableNumberOfFilteredItems: '{nFiltered, number} of {nTotal, number} items',
|
||||
sortedTableNumberOfItems: '{nTotal, number} items',
|
||||
sortedTableNumberOfSelectedItems: '{nSelected, number} selected',
|
||||
sortedTableSelectAllItems: 'Click here to select all items',
|
||||
|
||||
// ----- Forms -----
|
||||
add: 'Add',
|
||||
selectAll: 'Select all',
|
||||
remove: 'Remove',
|
||||
preview: 'Preview',
|
||||
item: 'Item',
|
||||
@@ -172,28 +207,40 @@ var messages = {
|
||||
// --- Dates/Scheduler ---
|
||||
|
||||
schedulingMonth: 'Month',
|
||||
schedulingEveryNMonth: 'Every N month',
|
||||
schedulingEachSelectedMonth: 'Each selected month',
|
||||
schedulingMonthDay: 'Day of the month',
|
||||
schedulingEachSelectedMonthDay: 'Each selected day',
|
||||
schedulingWeekDay: 'Day of the week',
|
||||
schedulingEachSelectedWeekDay: 'Each selected day',
|
||||
schedulingDay: 'Day',
|
||||
schedulingEveryNDay: 'Every N day',
|
||||
schedulingEachSelectedDay: 'Each selected day',
|
||||
schedulingSetWeekDayMode: 'Switch to week days',
|
||||
schedulingSetMonthDayMode: 'Switch to month days',
|
||||
schedulingHour: 'Hour',
|
||||
schedulingEveryNHour: 'Every N hour',
|
||||
schedulingEachSelectedHour: 'Each selected hour',
|
||||
schedulingEveryNHour: 'Every N hour',
|
||||
schedulingMinute: 'Minute',
|
||||
schedulingEveryNMinute: 'Every N minute',
|
||||
schedulingEachSelectedMinute: 'Each selected minute',
|
||||
schedulingEveryNMinute: 'Every N minute',
|
||||
selectTableAllMonth: 'Every month',
|
||||
selectTableAllDay: 'Every day',
|
||||
selectTableAllHour: 'Every hour',
|
||||
selectTableAllMinute: 'Every minute',
|
||||
schedulingReset: 'Reset',
|
||||
unknownSchedule: 'Unknown',
|
||||
timezonePickerServerValue: 'Xo-server timezone:',
|
||||
timezonePickerUseLocalTime: 'Web browser timezone',
|
||||
timezonePickerUseServerTime: 'Xo-server timezone',
|
||||
serverTimezoneOption: 'Server timezone ({value})',
|
||||
cronPattern: 'Cron Pattern:',
|
||||
backupEditNotFoundTitle: 'Cannot edit backup',
|
||||
backupEditNotFoundMessage: 'Missing required info for edition',
|
||||
successfulJobCall: 'Successful',
|
||||
failedJobCall: 'Failed',
|
||||
jobCallInProgess: 'In progress',
|
||||
jobTransferredDataSize: 'size:',
|
||||
jobTransferredDataSpeed: 'speed:',
|
||||
allJobCalls: 'All',
|
||||
job: 'Job',
|
||||
jobId: 'Job ID',
|
||||
jobModalTitle: 'Job {job}',
|
||||
jobId: 'ID',
|
||||
jobType: 'Type',
|
||||
jobName: 'Name',
|
||||
jobNamePlaceholder: 'Name of your job (forbidden: "_")',
|
||||
jobStart: 'Start',
|
||||
@@ -204,8 +251,10 @@ var messages = {
|
||||
jobTag: 'Tag',
|
||||
jobScheduling: 'Scheduling',
|
||||
jobState: 'State',
|
||||
jobStateEnabled: 'Enabled',
|
||||
jobStateDisabled: 'Disabled',
|
||||
jobTimezone: 'Timezone',
|
||||
jobServerTimezone: 'xo-server',
|
||||
jobServerTimezone: 'Server',
|
||||
runJob: 'Run job',
|
||||
runJobVerbose: 'One shot running started. See overview for logs.',
|
||||
jobStarted: 'Started',
|
||||
@@ -220,9 +269,14 @@ var messages = {
|
||||
noJobs: 'No jobs found.',
|
||||
noSchedules: 'No schedules found',
|
||||
jobActionPlaceHolder: 'Select a xo-server API command',
|
||||
jobTimeoutPlaceHolder: 'Timeout (number of seconds after which a VM is considered failed)',
|
||||
jobSchedules: 'Schedules',
|
||||
jobScheduleNamePlaceHolder: 'Name of your schedule',
|
||||
jobScheduleJobPlaceHolder: 'Select a Job',
|
||||
jobOwnerPlaceholder: 'Job owner',
|
||||
jobUserNotFound: 'This job\'s creator no longer exists',
|
||||
backupUserNotFound: 'This backup\'s creator no longer exists',
|
||||
backupOwner: 'Backup owner',
|
||||
|
||||
// ------ New backup -----
|
||||
newBackupSelection: 'Select your backup type:',
|
||||
@@ -231,15 +285,20 @@ var messages = {
|
||||
smartBackup: 'Smart backup',
|
||||
localRemoteWarningTitle: 'Local remote selected',
|
||||
localRemoteWarningMessage: 'Warning: local remotes will use limited XOA disk space. Only for advanced users.',
|
||||
backupVersionWarning: 'Warning: this feature works only with XenServer 6.5 or newer.',
|
||||
editBackupVmsTitle: 'VMs',
|
||||
editBackupSmartStatusTitle: 'VMs statuses',
|
||||
editBackupSmartResidentOn: 'Resident on',
|
||||
editBackupSmartPools: 'Pools',
|
||||
editBackupSmartTags: 'Tags',
|
||||
editBackupSmartTagsTitle: 'VMs Tags',
|
||||
editBackupNot: 'Reverse',
|
||||
editBackupTagTitle: 'Tag',
|
||||
editBackupReportTitle: 'Report',
|
||||
editBackupReportEnable: 'Enable immediately after creation',
|
||||
editBackupScheduleEnabled: 'Automatically run as scheduled',
|
||||
editBackupDepthTitle: 'Depth',
|
||||
editBackupRemoteTitle: 'Remote',
|
||||
deleteOldBackupsFirst: 'Delete the old backups first',
|
||||
|
||||
// ------ New Remote -----
|
||||
remoteList: 'Remote stores for backup',
|
||||
@@ -248,6 +307,7 @@ var messages = {
|
||||
remoteTypeNfs: 'NFS',
|
||||
remoteTypeSmb: 'SMB',
|
||||
remoteType: 'Type',
|
||||
remoteSmbWarningMessage: 'SMB remotes are meant to work on Windows Server. For other systems (Linux Samba, which means almost all NAS), please use NFS.',
|
||||
remoteTestTip: 'Test your remote',
|
||||
testRemote: 'Test Remote',
|
||||
remoteTestFailure: 'Test failed for {name}',
|
||||
@@ -255,7 +315,10 @@ var messages = {
|
||||
remoteTestError: 'Error',
|
||||
remoteTestStep: 'Test Step',
|
||||
remoteTestFile: 'Test file',
|
||||
remoteTestName: 'Test name',
|
||||
remoteTestNameFailure: 'Remote name already exists!',
|
||||
remoteTestSuccessMessage: 'The remote appears to work correctly',
|
||||
remoteConnectionFailed: 'Connection failed',
|
||||
|
||||
// ------ Remote -----
|
||||
remoteName: 'Name',
|
||||
@@ -263,17 +326,20 @@ var messages = {
|
||||
remoteState: 'State',
|
||||
remoteDevice: 'Device',
|
||||
remoteShare: 'Share',
|
||||
remoteAction: 'Action',
|
||||
remoteAuth: 'Auth',
|
||||
remoteMounted: 'Mounted',
|
||||
remoteUnmounted: 'Unmounted',
|
||||
remoteConnectTip: 'Connect',
|
||||
remoteDisconnectTip: 'Disconnect',
|
||||
remoteConnected: 'Connected',
|
||||
remoteDisconnected: 'Disconnected',
|
||||
remoteDeleteTip: 'Delete',
|
||||
remoteNamePlaceHolder: 'remote name *',
|
||||
remoteMyNamePlaceHolder: 'Name *',
|
||||
remoteLocalPlaceHolderPath: '/path/to/backup',
|
||||
remoteNfsPlaceHolderHost: 'host *',
|
||||
remoteNfsPlaceHolderPath: '/path/to/backup',
|
||||
remoteNfsPlaceHolderPath: 'path/to/backup',
|
||||
remoteSmbPlaceHolderRemotePath: 'subfolder [path\\to\\backup]',
|
||||
remoteSmbPlaceHolderUsername: 'Username',
|
||||
remoteSmbPlaceHolderPassword: 'Password',
|
||||
@@ -342,10 +408,11 @@ var messages = {
|
||||
userLabel: 'User',
|
||||
adminLabel: 'Admin',
|
||||
noUserInGroup: 'No user in group',
|
||||
countUsers: '{users} user{users, plural, one {} other {s}}',
|
||||
countUsers: '{users, number} user{users, plural, one {} other {s}}',
|
||||
selectPermission: 'Select Permission',
|
||||
|
||||
// ----- Plugins ------
|
||||
noPlugins: 'No plugins found',
|
||||
autoloadPlugin: 'Auto-load at server start',
|
||||
savePluginConfiguration: 'Save configuration',
|
||||
deletePluginConfiguration: 'Delete configuration',
|
||||
@@ -396,24 +463,37 @@ var messages = {
|
||||
convertVmToTemplateLabel: 'Convert to template',
|
||||
vmConsoleLabel: 'Console',
|
||||
|
||||
// ----- SR tabs -----
|
||||
// ----- SR advanced tab -----
|
||||
|
||||
srUnhealthyVdiNameLabel: 'Name',
|
||||
srUnhealthyVdiSize: 'Size',
|
||||
srUnhealthyVdiDepth: 'Depth',
|
||||
srUnhealthyVdiTitle: 'VDI to coalesce ({total, number})',
|
||||
|
||||
// ----- SR actions -----
|
||||
srRescan: 'Rescan all disks',
|
||||
srReconnectAll: 'Connect to all hosts',
|
||||
srDisconnectAll: 'Disconnect to all hosts',
|
||||
srDisconnectAll: 'Disconnect from all hosts',
|
||||
srForget: 'Forget this SR',
|
||||
srsForget: 'Forget SRs',
|
||||
srRemoveButton: 'Remove this SR',
|
||||
srNoVdis: 'No VDIs in this storage',
|
||||
// ----- Pool general -----
|
||||
poolTitleRamUsage: 'Pool RAM usage:',
|
||||
poolRamUsage: '{used} used on {total}',
|
||||
poolMaster: 'Master:',
|
||||
displayAllHosts: 'Display all hosts of this pool',
|
||||
displayAllStorages: 'Display all storages of this pool',
|
||||
displayAllVMs: 'Display all VMs of this pool',
|
||||
// ----- Pool tabs -----
|
||||
hostsTabName: 'Hosts',
|
||||
vmsTabName: 'Vms',
|
||||
srsTabName: 'Srs',
|
||||
// ----- Pool advanced tab -----
|
||||
poolHaStatus: 'High Availability',
|
||||
poolHaEnabled: 'Enabled',
|
||||
poolHaDisabled: 'Disabled',
|
||||
setpoolMaster: 'Master',
|
||||
// ----- Pool host tab -----
|
||||
hostNameLabel: 'Name',
|
||||
hostDescription: 'Description',
|
||||
@@ -421,6 +501,7 @@ var messages = {
|
||||
noHost: 'No hosts',
|
||||
memoryLeftTooltip: '{used}% used ({free} free)',
|
||||
// ----- Pool network tab -----
|
||||
pif: 'PIF',
|
||||
poolNetworkNameLabel: 'Name',
|
||||
poolNetworkDescription: 'Description',
|
||||
poolNetworkPif: 'PIFs',
|
||||
@@ -430,10 +511,19 @@ var messages = {
|
||||
poolNetworkPifDetached: 'Disconnected',
|
||||
showPifs: 'Show PIFs',
|
||||
hidePifs: 'Hide PIFs',
|
||||
showDetails: 'Show details',
|
||||
hideDetails: 'Hide details',
|
||||
// ----- Pool stats tab -----
|
||||
poolNoStats: 'No stats',
|
||||
poolAllHosts: 'All hosts',
|
||||
// ----- Pool actions ------
|
||||
addSrLabel: 'Add SR',
|
||||
addVmLabel: 'Add VM',
|
||||
addHostLabel: 'Add Host',
|
||||
hostNeedsPatchUpdate: 'This host needs to install {patches, number} patch{patches, plural, one {} other {es}} before it can be added to the pool. This operation may be long.',
|
||||
hostNeedsPatchUpdateNoInstall: 'This host cannot be added to the pool because it\'s missing some patches.',
|
||||
addHostErrorTitle: 'Adding host failed',
|
||||
addHostNotHomogeneousErrorMessage: 'Host patches could not be homogenized.',
|
||||
disconnectServer: 'Disconnect',
|
||||
|
||||
// ----- Host actions ------
|
||||
@@ -444,7 +534,11 @@ var messages = {
|
||||
restartHostAgent: 'Restart toolstack',
|
||||
forceRebootHostLabel: 'Force reboot',
|
||||
rebootHostLabel: 'Reboot',
|
||||
rebootUpdateHostLabel: 'Reboot for applying updates',
|
||||
noHostsAvailableErrorTitle: 'Error while restarting host',
|
||||
noHostsAvailableErrorMessage: 'Some VMs cannot be migrated before restarting this host. Please try force reboot.',
|
||||
failHostBulkRestartTitle: 'Error while restarting hosts',
|
||||
failHostBulkRestartMessage: '{failedHosts, number}/{totalHosts, number} host{failedHosts, plural, one {} other {s}} could not be restarted.',
|
||||
rebootUpdateHostLabel: 'Reboot to apply updates',
|
||||
emergencyModeLabel: 'Emergency mode',
|
||||
// ----- Host tabs -----
|
||||
storageTabName: 'Storage',
|
||||
@@ -452,6 +546,7 @@ var messages = {
|
||||
// ----- host stat tab -----
|
||||
statLoad: 'Load average',
|
||||
// ----- host advanced tab -----
|
||||
memoryHostState: 'RAM Usage: {memoryUsed}',
|
||||
hardwareHostSettingsLabel: 'Hardware',
|
||||
hostAddress: 'Address',
|
||||
hostStatus: 'Status',
|
||||
@@ -467,10 +562,20 @@ var messages = {
|
||||
hostCpusNumber: 'Core (socket)',
|
||||
hostManufacturerinfo: 'Manufacturer info',
|
||||
hostBiosinfo: 'BIOS info',
|
||||
licenseHostSettingsLabel: 'Licence',
|
||||
licenseHostSettingsLabel: 'License',
|
||||
hostLicenseType: 'Type',
|
||||
hostLicenseSocket: 'Socket',
|
||||
hostLicenseExpiry: 'Expiry',
|
||||
supplementalPacks: 'Installed supplemental packs',
|
||||
supplementalPackNew: 'Install new supplemental pack',
|
||||
supplementalPackPoolNew: 'Install supplemental pack on every host',
|
||||
supplementalPackTitle: '{name} (by {author})',
|
||||
supplementalPackInstallStartedTitle: 'Installation started',
|
||||
supplementalPackInstallStartedMessage: 'Installing new supplemental pack…',
|
||||
supplementalPackInstallErrorTitle: 'Installation error',
|
||||
supplementalPackInstallErrorMessage: 'The installation of the supplemental pack failed.',
|
||||
supplementalPackInstallSuccessTitle: 'Installation success',
|
||||
supplementalPackInstallSuccessMessage: 'Supplemental pack successfully installed.',
|
||||
// ----- Host net tabs -----
|
||||
networkCreateButton: 'Add a network',
|
||||
networkCreateBondedButton: 'Add a bonded network',
|
||||
@@ -486,6 +591,7 @@ var messages = {
|
||||
pifStatusDisconnected: 'Disconnected',
|
||||
pifNoInterface: 'No physical interface detected',
|
||||
pifInUse: 'This interface is currently in use',
|
||||
pifAction: 'Action',
|
||||
defaultLockingMode: 'Default locking mode',
|
||||
pifConfigureIp: 'Configure IP address',
|
||||
configIpErrorTitle: 'Invalid parameters',
|
||||
@@ -498,6 +604,7 @@ var messages = {
|
||||
addSrDeviceButton: 'Add a storage',
|
||||
srNameLabel: 'Name',
|
||||
srType: 'Type',
|
||||
pbdAction: 'Action',
|
||||
pbdStatus: 'Status',
|
||||
pbdStatusConnected: 'Connected',
|
||||
pbdStatusDisconnected: 'Disconnected',
|
||||
@@ -516,13 +623,17 @@ var messages = {
|
||||
patchStatus: 'Status',
|
||||
patchStatusApplied: 'Applied',
|
||||
patchStatusNotApplied: 'Missing patches',
|
||||
patchNothing: 'No patch detected',
|
||||
patchNothing: 'No patches detected',
|
||||
patchReleaseDate: 'Release date',
|
||||
patchGuidance: 'Guidance',
|
||||
patchAction: 'Action',
|
||||
hostAppliedPatches: 'Applied patches',
|
||||
hostMissingPatches: 'Missing patches',
|
||||
hostUpToDate: 'Host up-to-date!',
|
||||
installPatchWarningTitle: 'Non-recommended patch install',
|
||||
installPatchWarningContent: 'This will install a patch only on this host. This is NOT the recommended way: please go into the Pool patch view and follow instructions there. If you are sure about this, you can continue anyway',
|
||||
installPatchWarningReject: 'Go to pool',
|
||||
installPatchWarningResolve: 'Install',
|
||||
// ----- Pool patch tabs -----
|
||||
refreshPatches: 'Refresh patches',
|
||||
installPoolPatches: 'Install pool patches',
|
||||
@@ -552,6 +663,7 @@ var messages = {
|
||||
vmSettings: 'Started {ago}',
|
||||
vmCurrentStatus: 'Current status:',
|
||||
vmNotRunning: 'Not running',
|
||||
vmHaltedSince: 'Halted {ago}',
|
||||
|
||||
// ----- VM general tab -----
|
||||
noToolsDetected: 'No Xen tools detected',
|
||||
@@ -576,7 +688,6 @@ var messages = {
|
||||
copyToClipboardLabel: 'Copy',
|
||||
ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
|
||||
tipLabel: 'Tip:',
|
||||
tipConsoleLabel: 'non-US keyboard could have issues with console: switch your own layout to US.',
|
||||
hideHeaderTooltip: 'Hide infos',
|
||||
showHeaderTooltip: 'Show infos',
|
||||
|
||||
@@ -600,6 +711,8 @@ var messages = {
|
||||
vdiBootOrder: 'Boot order',
|
||||
vdiNameLabel: 'Name',
|
||||
vdiNameDescription: 'Description',
|
||||
vdiPool: 'Pool',
|
||||
vdiDisconnect: 'Disconnect',
|
||||
vdiTags: 'Tags',
|
||||
vdiSize: 'Size',
|
||||
vdiSr: 'SR',
|
||||
@@ -611,20 +724,28 @@ var messages = {
|
||||
vdiMigrateNoSrMessage: 'A target SR is required to migrate a VDI',
|
||||
vdiForget: 'Forget',
|
||||
vdiRemove: 'Remove VDI',
|
||||
vdbBootableStatus: 'Boot flag',
|
||||
vdbStatus: 'Status',
|
||||
noControlDomainVdis: 'No VDIs attached to Control Domain',
|
||||
vbdBootableStatus: 'Boot flag',
|
||||
vbdStatus: 'Status',
|
||||
vbdStatusConnected: 'Connected',
|
||||
vbdStatusDisconnected: 'Disconnected',
|
||||
vbdNoVbd: 'No disks',
|
||||
vbdConnect: 'Connect VBD',
|
||||
vbdDisconnect: 'Disconnect VBD',
|
||||
vdbBootable: 'Bootable',
|
||||
vdbReadonly: 'Readonly',
|
||||
vdbCreate: 'Create',
|
||||
vdbNamePlaceHolder: 'Disk name',
|
||||
vdbSizePlaceHolder: 'Size',
|
||||
vbdBootable: 'Bootable',
|
||||
vbdReadonly: 'Readonly',
|
||||
vbdAction: 'Action',
|
||||
vbdCreate: 'Create',
|
||||
vbdNamePlaceHolder: 'Disk name',
|
||||
vbdSizePlaceHolder: 'Size',
|
||||
cdDriveNotInstalled: 'CD drive not completely installed',
|
||||
cdDriveInstallation: 'Stop and start the VM to install the CD drive',
|
||||
saveBootOption: 'Save',
|
||||
resetBootOption: 'Reset',
|
||||
deleteSelectedVdis: 'Delete selected VDIs',
|
||||
deleteSelectedVdi: 'Delete selected VDI',
|
||||
useQuotaWarning: 'Creating this disk will use the disk space quota from the resource set {resourceSet} ({spaceLeft} left)',
|
||||
notEnoughSpaceInResourceSet: 'Not enough space in resource set {resourceSet} ({spaceLeft} left)',
|
||||
|
||||
// ----- VM network tab -----
|
||||
vifCreateDeviceButton: 'New device',
|
||||
@@ -647,6 +768,7 @@ var messages = {
|
||||
vifLockedNetworkNoIps: 'Network locked and no IPs are allowed for this interface',
|
||||
vifUnLockedNetwork: 'Network not locked',
|
||||
vifUnknownNetwork: 'Unknown network',
|
||||
vifAction: 'Action',
|
||||
vifCreate: 'Create',
|
||||
|
||||
// ----- VM snapshot tab -----
|
||||
@@ -660,6 +782,7 @@ var messages = {
|
||||
snapshotDate: 'Creation date',
|
||||
snapshotName: 'Name',
|
||||
snapshotAction: 'Action',
|
||||
snapshotQuiesce: 'Quiesced snapshot',
|
||||
|
||||
// ----- VM log tab -----
|
||||
logRemoveAll: 'Remove all logs',
|
||||
@@ -691,12 +814,21 @@ var messages = {
|
||||
osKernel: 'OS kernel',
|
||||
autoPowerOn: 'Auto power on',
|
||||
ha: 'HA',
|
||||
vmAffinityHost: 'Affinity host',
|
||||
vmVga: 'VGA',
|
||||
vmVideoram: 'Video RAM',
|
||||
noAffinityHost: 'None',
|
||||
originalTemplate: 'Original template',
|
||||
unknownOsName: 'Unknown',
|
||||
unknownOsKernel: 'Unknown',
|
||||
unknownOriginalTemplate: 'Unknown',
|
||||
vmLimitsLabel: 'VM limits',
|
||||
vmCpuLimitsLabel: 'CPU limits',
|
||||
vmCpuTopology: 'Topology',
|
||||
vmChooseCoresPerSocket: 'Default behavior',
|
||||
vmCoresPerSocket: '{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket',
|
||||
vmCoresPerSocketIncorrectValue: 'Incorrect cores per socket value',
|
||||
vmCoresPerSocketIncorrectValueSolution: 'Please change the selected value to fix it.',
|
||||
vmMemoryLimitsLabel: 'Memory limits (min/max)',
|
||||
vmMaxVcpus: 'vCPUs max:',
|
||||
vmMaxRam: 'Memory max:',
|
||||
@@ -720,7 +852,7 @@ var messages = {
|
||||
poolPanel: 'Pool{pools, plural, one {} other {s}}',
|
||||
hostPanel: 'Host{hosts, plural, one {} other {s}}',
|
||||
vmPanel: 'VM{vms, plural, one {} other {s}}',
|
||||
memoryStatePanel: 'RAM Usage',
|
||||
memoryStatePanel: 'RAM Usage:',
|
||||
cpuStatePanel: 'CPUs Usage',
|
||||
vmStatePanel: 'VMs Power state',
|
||||
taskStatePanel: 'Pending tasks',
|
||||
@@ -738,7 +870,7 @@ var messages = {
|
||||
srFree: 'free',
|
||||
srUsageStatePanel: 'Storage Usage',
|
||||
srTopUsageStatePanel: 'Top 5 SR Usage (in %)',
|
||||
vmsStates: '{running} running ({halted} halted)',
|
||||
vmsStates: '{running, number} running ({halted, number} halted)',
|
||||
dashboardStatsButtonRemoveAll: 'Clear selection',
|
||||
dashboardStatsButtonAddAllHost: 'Add all hosts',
|
||||
dashboardStatsButtonAddAllVM: 'Add all VMs',
|
||||
@@ -763,6 +895,7 @@ var messages = {
|
||||
orphanedVms: 'Orphaned VMs snapshot',
|
||||
noOrphanedObject: 'No orphans',
|
||||
removeAllOrphanedObject: 'Remove all orphaned snapshot VDIs',
|
||||
vdisOnControlDomain: 'VDIs attached to Control Domain',
|
||||
vmNameLabel: 'Name',
|
||||
vmNameDescription: 'Description',
|
||||
vmContainer: 'Resident on',
|
||||
@@ -777,7 +910,6 @@ var messages = {
|
||||
|
||||
// ----- New VM -----
|
||||
newVmCreateNewVmOn: 'Create a new VM on {select}',
|
||||
newVmCreateNewVmOn2: 'Create a new VM on {select1} or {select2}',
|
||||
newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
|
||||
newVmInfoPanel: 'Infos',
|
||||
newVmNameLabel: 'Name',
|
||||
@@ -792,6 +924,7 @@ var messages = {
|
||||
newVmInstallSettingsPanel: 'Install settings',
|
||||
newVmIsoDvdLabel: 'ISO/DVD',
|
||||
newVmNetworkLabel: 'Network',
|
||||
newVmInstallNetworkPlaceHolder: 'e.g: http://httpredir.debian.org/debian',
|
||||
newVmPvArgsLabel: 'PV Args',
|
||||
newVmPxeLabel: 'PXE',
|
||||
newVmInterfacesPanel: 'Interfaces',
|
||||
@@ -799,7 +932,6 @@ var messages = {
|
||||
newVmAddInterface: 'Add interface',
|
||||
newVmDisksPanel: 'Disks',
|
||||
newVmSrLabel: 'SR',
|
||||
newVmBootableLabel: 'Bootable',
|
||||
newVmSizeLabel: 'Size',
|
||||
newVmAddDisk: 'Add disk',
|
||||
newVmSummaryPanel: 'Summary',
|
||||
@@ -817,7 +949,7 @@ var messages = {
|
||||
newVmDefaultCpuCap: 'Default: {value, number}',
|
||||
newVmCloudConfig: 'Cloud config',
|
||||
newVmCreateVms: 'Create VMs',
|
||||
newVmCreateVmsConfirm: 'Are you sure you want to create {nbVms} VMs?',
|
||||
newVmCreateVmsConfirm: 'Are you sure you want to create {nbVms, number} VMs?',
|
||||
newVmMultipleVms: 'Multiple VMs:',
|
||||
newVmSelectResourceSet: 'Select a resource set:',
|
||||
newVmMultipleVmsPattern: 'Name pattern:',
|
||||
@@ -825,9 +957,11 @@ var messages = {
|
||||
newVmFirstIndex: 'First index:',
|
||||
newVmNumberRecalculate: 'Recalculate VMs number',
|
||||
newVmNameRefresh: 'Refresh VMs name',
|
||||
newVmAffinityHost: 'Affinity host',
|
||||
newVmAdvancedPanel: 'Advanced',
|
||||
newVmShowAdvanced: 'Show advanced settings',
|
||||
newVmHideAdvanced: 'Hide advanced settings',
|
||||
newVmShare: 'Share this VM',
|
||||
|
||||
// ----- Self -----
|
||||
resourceSets: 'Resource sets',
|
||||
@@ -895,24 +1029,46 @@ var messages = {
|
||||
simpleBackup: 'simple',
|
||||
delta: 'delta',
|
||||
restoreBackups: 'Restore Backups',
|
||||
noRemotes: 'No remotes',
|
||||
restoreBackupsInfo: 'Click on a VM to display restore options',
|
||||
restoreDeltaBackupsInfo: 'Only the files of Delta Backup which are not on a SMB remote can be restored',
|
||||
remoteEnabled: 'Enabled',
|
||||
remoteError: 'Error',
|
||||
noBackup: 'No backup available',
|
||||
backupVmNameColumn: 'VM Name',
|
||||
backupTagColumn: 'Backup Tag',
|
||||
backupTags: 'Tags',
|
||||
lastBackupColumn: 'Last Backup',
|
||||
availableBackupsColumn: 'Available Backups',
|
||||
restoreColumn: 'Restore',
|
||||
restoreTip: 'View restore options',
|
||||
backupRestoreErrorTitle: 'Missing parameters',
|
||||
backupRestoreErrorMessage: 'Choose a SR and a backup',
|
||||
backupRestoreSelectDefaultSr: 'Select default SR…',
|
||||
backupRestoreChooseSrForEachVdis: 'Choose a SR for each VDI',
|
||||
backupRestoreVdiLabel: 'VDI',
|
||||
backupRestoreSrLabel: 'SR',
|
||||
displayBackup: 'Display backups',
|
||||
importBackupTitle: 'Import VM',
|
||||
importBackupMessage: 'Starting your backup import',
|
||||
vmsToBackup: 'VMs to backup',
|
||||
|
||||
// ----- Restore files view -----
|
||||
listRemoteBackups: 'List remote backups',
|
||||
restoreFiles: 'Restore backup files',
|
||||
restoreFilesError: 'Invalid options',
|
||||
restoreFilesFromBackup: 'Restore file from {name}',
|
||||
restoreFilesSelectBackup: 'Select a backup…',
|
||||
restoreFilesSelectDisk: 'Select a disk…',
|
||||
restoreFilesSelectPartition: 'Select a partition…',
|
||||
restoreFilesSelectFolderPath: 'Folder path',
|
||||
restoreFilesSelectFiles: 'Select a file…',
|
||||
restoreFileContentNotFound: 'Content not found',
|
||||
restoreFilesNoFilesSelected: 'No files selected',
|
||||
restoreFilesSelectedFiles: 'Selected files ({files}):',
|
||||
restoreFilesDiskError: 'Error while scanning disk',
|
||||
restoreFilesSelectAllFiles: 'Select all this folder\'s files',
|
||||
restoreFilesUnselectAll: 'Unselect all files',
|
||||
|
||||
// ----- Modals -----
|
||||
emergencyShutdownHostsModalTitle: 'Emergency shutdown Host{nHosts, plural, one {} other {s}}',
|
||||
emergencyShutdownHostsModalMessage: 'Are you sure you want to shutdown {nHosts} Host{nHosts, plural, one {} other {s}}?',
|
||||
emergencyShutdownHostsModalMessage: 'Are you sure you want to shutdown {nHosts, number} Host{nHosts, plural, one {} other {s}}?',
|
||||
stopHostModalTitle: 'Shutdown host',
|
||||
stopHostModalMessage: 'This will shutdown your host. Do you want to continue? If it\'s the pool master, your connection to the pool will be lost',
|
||||
addHostModalTitle: 'Add host',
|
||||
@@ -920,44 +1076,57 @@ var messages = {
|
||||
restartHostModalTitle: 'Restart host',
|
||||
restartHostModalMessage: 'This will restart your host. Do you want to continue?',
|
||||
restartHostsAgentsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}',
|
||||
restartHostsAgentsModalMessage: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?',
|
||||
restartHostsAgentsModalMessage: 'Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?',
|
||||
restartHostsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}}',
|
||||
restartHostsModalMessage: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}}?',
|
||||
restartHostsModalMessage: 'Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}}?',
|
||||
startVmsModalTitle: 'Start VM{vms, plural, one {} other {s}}',
|
||||
startVmsModalMessage: 'Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?',
|
||||
cloneAndStartVM: 'Start a copy',
|
||||
forceStartVm: 'Force start',
|
||||
forceStartVmModalTitle: 'Forbidden operation',
|
||||
blockedStartVmModalMessage: 'Start operation for this vm is blocked.',
|
||||
blockedStartVmsModalMessage: 'Forbidden operation start for {nVms, number} vm{nVms, plural, one {} other {s}}.',
|
||||
startVmsModalMessage: 'Are you sure you want to start {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
failedVmsErrorMessage: '{nVms, number} vm{nVms, plural, one {} other {s}} are failed. Please see your logs to get more information',
|
||||
failedVmsErrorTitle: 'Start failed',
|
||||
stopHostsModalTitle: 'Stop Host{nHosts, plural, one {} other {s}}',
|
||||
stopHostsModalMessage: 'Are you sure you want to stop {nHosts} Host{nHosts, plural, one {} other {s}}?',
|
||||
stopHostsModalMessage: 'Are you sure you want to stop {nHosts, number} Host{nHosts, plural, one {} other {s}}?',
|
||||
stopVmsModalTitle: 'Stop VM{vms, plural, one {} other {s}}',
|
||||
stopVmsModalMessage: 'Are you sure you want to stop {vms} VM{vms, plural, one {} other {s}}?',
|
||||
stopVmsModalMessage: 'Are you sure you want to stop {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
restartVmModalTitle: 'Restart VM',
|
||||
restartVmModalMessage: 'Are you sure you want to restart {name}?',
|
||||
stopVmModalTitle: 'Stop VM',
|
||||
stopVmModalMessage: 'Are you sure you want to stop {name}?',
|
||||
restartVmsModalTitle: 'Restart VM{vms, plural, one {} other {s}}',
|
||||
restartVmsModalMessage: 'Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?',
|
||||
restartVmsModalMessage: 'Are you sure you want to restart {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
snapshotVmsModalTitle: 'Snapshot VM{vms, plural, one {} other {s}}',
|
||||
snapshotVmsModalMessage: 'Are you sure you want to snapshot {vms} VM{vms, plural, one {} other {s}}?',
|
||||
snapshotVmsModalMessage: 'Are you sure you want to snapshot {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
deleteVmsModalTitle: 'Delete VM{vms, plural, one {} other {s}}',
|
||||
deleteVmsModalMessage: 'Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
|
||||
deleteVmsModalMessage: 'Are you sure you want to delete {vms, number} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
|
||||
deleteVmModalTitle: 'Delete VM',
|
||||
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
|
||||
migrateVmModalTitle: 'Migrate VM',
|
||||
migrateVmSelectHost: 'Select a destination host:',
|
||||
migrateVmSelectMigrationNetwork: 'Select a migration network:',
|
||||
migrateVmSelectSrs: 'For each VDI, select an SR:',
|
||||
migrateVmSelectNetworks: 'For each VIF, select a network:',
|
||||
migrateVmsSelectSr: 'Select a destination SR:',
|
||||
migrateVmsSelectSrIntraPool: 'Select a destination SR for local disks:',
|
||||
migrateVmsSelectNetwork: 'Select a network on which to connect each VIF:',
|
||||
migrateVmsSmartMapping: 'Smart mapping',
|
||||
migrateVmName: 'Name',
|
||||
migrateVmSr: 'SR',
|
||||
migrateVmVif: 'VIF',
|
||||
migrateVmNetwork: 'Network',
|
||||
migrateVmNoTargetHost: 'No target host',
|
||||
migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
|
||||
migrateVmNoDefaultSrError: 'No default SR',
|
||||
migrateVmNotConnectedDefaultSrError: 'Default SR not connected to host',
|
||||
chooseSrForEachVdisModalSelectSr: 'For each VDI, select an SR:',
|
||||
chooseSrForEachVdisModalMainSr: 'Select main SR…',
|
||||
chooseSrForEachVdisModalVdiLabel: 'VDI',
|
||||
chooseSrForEachVdisModalSrLabel: 'SR*',
|
||||
chooseSrForEachVdisModalOptionalEntry: '* optional',
|
||||
deleteVdiModalTitle: 'Delete VDI',
|
||||
deleteVdiModalMessage: 'Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST',
|
||||
deleteVdisModalTitle: 'Delete VDI{nVdis, plural, one {} other {s}}',
|
||||
deleteVdisModalMessage: 'Are you sure you want to delete {nVdis, number} disk{nVdis, plural, one {} other {s}}? ALL DATA ON THESE DISKS WILL BE LOST',
|
||||
revertVmModalTitle: 'Revert your VM',
|
||||
deleteSnapshotModalTitle: 'Delete snapshot',
|
||||
deleteSnapshotModalMessage: 'Are you sure you want to delete this snapshot?',
|
||||
@@ -980,16 +1149,32 @@ var messages = {
|
||||
trialReadyModalText: 'During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!',
|
||||
|
||||
// ----- Servers -----
|
||||
serverLabel: 'Label',
|
||||
serverHost: 'Host',
|
||||
serverUsername: 'Username',
|
||||
serverPassword: 'Password',
|
||||
serverAction: 'Action',
|
||||
serverReadOnly: 'Read Only',
|
||||
serverUnauthorizedCertificates: 'Unauthorized Certificates',
|
||||
serverAllowUnauthorizedCertificates: 'Allow Unauthorized Certificates',
|
||||
serverUnauthorizedCertificatesInfo: 'Enable it if your certificate is rejected, but it\'s not recommended because your connection will not be secured.',
|
||||
serverDisconnect: 'Disconnect server',
|
||||
serverPlaceHolderUser: 'username',
|
||||
serverPlaceHolderPassword: 'password',
|
||||
serverPlaceHolderAddress: 'address[:port]',
|
||||
serverPlaceHolderLabel: 'label',
|
||||
serverConnect: 'Connect',
|
||||
serverError: 'Error',
|
||||
serverAddFailed: 'Adding server failed',
|
||||
serverStatus: 'Status',
|
||||
serverConnectionFailed: 'Connection failed. Click for more information.',
|
||||
serverConnecting: 'Connecting…',
|
||||
serverConnected: 'Connected',
|
||||
serverDisconnected: 'Disconnected',
|
||||
serverAuthFailed: 'Authentication error',
|
||||
serverUnknownError: 'Unknown error',
|
||||
serverSelfSignedCertError: 'Invalid self-signed certificate',
|
||||
serverSelfSignedCertQuestion: 'Do you want to accept self-signed certificate for this server even though it would decrease security?',
|
||||
|
||||
// ----- Copy VM -----
|
||||
copyVm: 'Copy VM',
|
||||
@@ -1008,6 +1193,16 @@ var messages = {
|
||||
detachHostModalMessage: 'Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST.',
|
||||
detachHost: 'Detach',
|
||||
|
||||
// ----- Forget host -----
|
||||
forgetHostModalTitle: 'Forget host',
|
||||
forgetHostModalMessage: 'Are you sure you want to forget {host} from its pool? Be sure this host can\'t be back online, or use detach instead.',
|
||||
forgetHost: 'Forget',
|
||||
|
||||
// ----- Set pool master -----
|
||||
|
||||
setPoolMasterModalTitle: 'Designate a new master',
|
||||
setPoolMasterModalMessage: 'This operation may take several minutes. Do you want to continue?',
|
||||
|
||||
// ----- Network -----
|
||||
newNetworkCreate: 'Create network',
|
||||
newBondedNetworkCreate: 'Create bonded network',
|
||||
@@ -1033,11 +1228,11 @@ var messages = {
|
||||
|
||||
// ----- About View -----
|
||||
xenOrchestra: 'Xen Orchestra',
|
||||
xenOrchestraServer: 'server',
|
||||
xenOrchestraWeb: 'web client',
|
||||
xenOrchestraServer: 'Xen Orchestra server',
|
||||
xenOrchestraWeb: 'Xen Orchestra web client',
|
||||
noProSupport: 'No pro support provided!',
|
||||
noProductionUse: 'Use in production at your own risks',
|
||||
downloadXoa: 'You can download our turnkey appliance at',
|
||||
downloadXoaFromWebsite: 'You can download our turnkey appliance at {website}',
|
||||
bugTracker: 'Bug Tracker',
|
||||
bugTrackerText: 'Issues? Report it!',
|
||||
community: 'Community',
|
||||
@@ -1049,9 +1244,9 @@ var messages = {
|
||||
documentation: 'Documentation',
|
||||
documentationText: 'Read our official doc',
|
||||
proSupportIncluded: 'Pro support included',
|
||||
xoAccount: 'Acces your XO Account',
|
||||
xoAccount: 'Access your XO Account',
|
||||
openTicket: 'Report a problem',
|
||||
openTicketText: 'Problem? Open a ticket !',
|
||||
openTicketText: 'Problem? Open a ticket!',
|
||||
|
||||
// ----- Upgrade Panel -----
|
||||
upgradeNeeded: 'Upgrade needed',
|
||||
@@ -1067,11 +1262,17 @@ var messages = {
|
||||
trial: 'Trial',
|
||||
settings: 'Settings',
|
||||
proxySettings: 'Proxy settings',
|
||||
proxySettingsHostPlaceHolder: 'Host (myproxy.example.org)',
|
||||
proxySettingsPortPlaceHolder: 'Port (eg: 3128)',
|
||||
proxySettingsUsernamePlaceHolder: 'Username',
|
||||
proxySettingsPasswordPlaceHolder: 'Password',
|
||||
updateRegistrationEmailPlaceHolder: 'Your email account',
|
||||
updateRegistrationPasswordPlaceHolder: 'Your password',
|
||||
update: 'Update',
|
||||
refresh: 'Refresh',
|
||||
upgrade: 'Upgrade',
|
||||
noUpdaterCommunity: 'No updater available for Community Edition',
|
||||
noUpdaterSubscribe: 'Please consider subscribe and try it with all features for free during 15 days on',
|
||||
considerSubscribe: 'Please consider subscribing and trying it with all the features for free during 15 days on {link}.',
|
||||
noUpdaterWarning: 'Manual update could break your current installation due to dependencies issues, do it with caution',
|
||||
currentVersion: 'Current version:',
|
||||
register: 'Register',
|
||||
@@ -1103,6 +1304,10 @@ var messages = {
|
||||
disconnectPifConfirm: 'Are you sure you want to disconnect this PIF?',
|
||||
deletePif: 'Delete PIF',
|
||||
deletePifConfirm: 'Are you sure you want to delete this PIF?',
|
||||
pifConnected: 'Connected',
|
||||
pifDisconnected: 'Disconnected',
|
||||
pifPhysicallyConnected: 'Physically connected',
|
||||
pifPhysicallyDisconnected: 'Physically disconnected',
|
||||
|
||||
// ----- User -----
|
||||
username: 'Username',
|
||||
@@ -1148,6 +1353,9 @@ var messages = {
|
||||
logDeleteAll: 'Delete all logs',
|
||||
logDeleteAllTitle: 'Delete all logs',
|
||||
logDeleteAllMessage: 'Are you sure you want to delete all the logs?',
|
||||
logIndicationToEnable: 'Click to enable',
|
||||
logIndicationToDisable: 'Click to disable',
|
||||
reportBug: 'Report a bug',
|
||||
|
||||
// ----- IPs ------
|
||||
ipPoolName: 'Name',
|
||||
@@ -1160,30 +1368,162 @@ var messages = {
|
||||
ipsDeleteAllMessage: 'Are you sure you want to delete all the IP pools?',
|
||||
ipsVifs: 'VIFs',
|
||||
ipsNotUsed: 'Not used',
|
||||
ipPoolUnknownVif: 'unknown VIF',
|
||||
ipPoolNameAlreadyExists: 'Name already exists',
|
||||
|
||||
// ----- Shortcuts -----
|
||||
shortcutModalTitle: 'Keyboard shortcuts',
|
||||
shortcut_XoApp: 'Global',
|
||||
shortcut_GO_TO_HOSTS: 'Go to hosts list',
|
||||
shortcut_GO_TO_POOLS: 'Go to pools list',
|
||||
shortcut_GO_TO_VMS: 'Go to VMs list',
|
||||
shortcut_CREATE_VM: 'Create a new VM',
|
||||
shortcut_UNFOCUS: 'Unfocus field',
|
||||
shortcut_HELP: 'Show shortcuts key bindings',
|
||||
shortcut_XoApp_GO_TO_HOSTS: 'Go to hosts list',
|
||||
shortcut_XoApp_GO_TO_POOLS: 'Go to pools list',
|
||||
shortcut_XoApp_GO_TO_VMS: 'Go to VMs list',
|
||||
shortcut_XoApp_GO_TO_SRS: 'Go to SRs list',
|
||||
shortcut_XoApp_CREATE_VM: 'Create a new VM',
|
||||
shortcut_XoApp_UNFOCUS: 'Unfocus field',
|
||||
shortcut_XoApp_HELP: 'Show shortcuts key bindings',
|
||||
shortcut_Home: 'Home',
|
||||
shortcut_SEARCH: 'Focus search bar',
|
||||
shortcut_NAV_DOWN: 'Next item',
|
||||
shortcut_NAV_UP: 'Previous item',
|
||||
shortcut_SELECT: 'Select item',
|
||||
shortcut_JUMP_INTO: 'Open',
|
||||
shortcut_Home_SEARCH: 'Focus search bar',
|
||||
shortcut_Home_NAV_DOWN: 'Next item',
|
||||
shortcut_Home_NAV_UP: 'Previous item',
|
||||
shortcut_Home_SELECT: 'Select item',
|
||||
shortcut_Home_JUMP_INTO: 'Open',
|
||||
shortcut_SortedTable: 'Supported tables',
|
||||
shortcut_SortedTable_SEARCH: 'Focus the table search bar',
|
||||
shortcut_SortedTable_NAV_DOWN: 'Next item',
|
||||
shortcut_SortedTable_NAV_UP: 'Previous item',
|
||||
shortcut_SortedTable_SELECT: 'Select item',
|
||||
shortcut_SortedTable_ROW_ACTION: 'Action',
|
||||
|
||||
// ----- Settings/ACLs -----
|
||||
settingsAclsButtonTooltipVM: 'VM',
|
||||
settingsAclsButtonTooltiphost: 'Hosts',
|
||||
settingsAclsButtonTooltippool: 'Pool',
|
||||
settingsAclsButtonTooltipSR: 'SR',
|
||||
settingsAclsButtonTooltipnetwork: 'Network'
|
||||
settingsAclsButtonTooltipnetwork: 'Network',
|
||||
|
||||
// ----- Config -----
|
||||
noConfigFile: 'No config file selected',
|
||||
importTip: 'Try dropping a config file here, or click to select a config file to upload.',
|
||||
config: 'Config',
|
||||
importConfig: 'Import',
|
||||
importConfigSuccess: 'Config file successfully imported',
|
||||
importConfigError: 'Error while importing config file',
|
||||
exportConfig: 'Export',
|
||||
downloadConfig: 'Download current config',
|
||||
noConfigImportCommunity: 'No config import available for Community Edition',
|
||||
|
||||
// ----- SR -----
|
||||
srReconnectAllModalTitle: 'Reconnect all hosts',
|
||||
srReconnectAllModalMessage: 'This will reconnect this SR to all its hosts.',
|
||||
srsReconnectAllModalMessage: 'This will reconnect each selected SR to its host (local SR) or to every hosts of its pool (shared SR).',
|
||||
srDisconnectAllModalTitle: 'Disconnect all hosts',
|
||||
srDisconnectAllModalMessage: 'This will disconnect this SR from all its hosts.',
|
||||
srsDisconnectAllModalMessage: 'This will disconnect each selected SR from its host (local SR) or from every hosts of its pool (shared SR).',
|
||||
srForgetModalTitle: 'Forget SR',
|
||||
srsForgetModalTitle: 'Forget selected SRs',
|
||||
srForgetModalMessage: 'Are you sure you want to forget this SR? VDIs on this storage won\'t be removed.',
|
||||
srsForgetModalMessage: 'Are you sure you want to forget all the selected SRs? VDIs on these storages won\'t be removed.',
|
||||
srAllDisconnected: 'Disconnected',
|
||||
srSomeConnected: 'Partially connected',
|
||||
srAllConnected: 'Connected',
|
||||
|
||||
// ----- XOSAN -----
|
||||
xosanTitle: 'XOSAN',
|
||||
xosanSrTitle: 'Xen Orchestra SAN SR',
|
||||
xosanAvailableSrsTitle: 'Select local SRs (lvm)',
|
||||
xosanSuggestions: 'Suggestions',
|
||||
xosanName: 'Name',
|
||||
xosanHost: 'Host',
|
||||
xosanHosts: 'Connected Hosts',
|
||||
xosanPool: 'Pool',
|
||||
xosanVolumeId: 'Volume ID',
|
||||
xosanSize: 'Size',
|
||||
xosanUsedSpace: 'Used space',
|
||||
xosanNeedPack: 'XOSAN pack needs to be installed on each host of the pool.',
|
||||
xosanInstallIt: 'Install it now!',
|
||||
xosanNeedRestart: 'Some hosts need their toolstack to be restarted before you can create an XOSAN',
|
||||
xosanRestartAgents: 'Restart toolstacks',
|
||||
xosanMasterOffline: 'Pool master is not running',
|
||||
xosanInstallPackTitle: 'Install XOSAN pack on {pool}',
|
||||
xosanSelect2Srs: 'Select at least 2 SRs',
|
||||
xosanLayout: 'Layout',
|
||||
xosanRedundancy: 'Redundancy',
|
||||
xosanCapacity: 'Capacity',
|
||||
xosanAvailableSpace: 'Available space',
|
||||
xosanDiskLossLegend: '* Can fail without data loss',
|
||||
xosanCreate: 'Create',
|
||||
xosanAdd: 'Add',
|
||||
xosanInstalling: 'Installing XOSAN. Please wait…',
|
||||
xosanCommunity: 'No XOSAN available for Community Edition',
|
||||
xosanNew: 'New',
|
||||
xosanAdvanced: 'Advanced',
|
||||
xosanRemoveSubvolumes: 'Remove subvolumes',
|
||||
xosanAddSubvolume: 'Add subvolume…',
|
||||
xosanWarning: 'This version of XOSAN SR is from the first beta phase. You can keep using it, but to modify it you\'ll have to save your disks and re-create it.',
|
||||
xosanVlan: 'VLAN',
|
||||
xosanNoSrs: 'No XOSAN found',
|
||||
xosanPbdsDetached: 'Some SRs are detached from the XOSAN',
|
||||
xosanBadStatus: 'Something is wrong with: {badStatuses}',
|
||||
xosanRunning: 'Running',
|
||||
xosanDelete: 'Delete XOSAN',
|
||||
xosanFixIssue: 'Fix',
|
||||
xosanCreatingOn: 'Creating XOSAN on {pool}',
|
||||
xosanState_configuringNetwork: 'Configuring network…',
|
||||
xosanState_importingVm: 'Importing VM…',
|
||||
xosanState_copyingVms: 'Copying VMs…',
|
||||
xosanState_configuringVms: 'Configuring VMs…',
|
||||
xosanState_configuringGluster: 'Configuring gluster…',
|
||||
xosanState_creatingSr: 'Creating SR…',
|
||||
xosanState_scanningSr: 'Scanning SR…',
|
||||
// Pack download modal
|
||||
xosanInstallCloudPlugin: 'Install cloud plugin first',
|
||||
xosanLoadCloudPlugin: 'Load cloud plugin first',
|
||||
xosanLoading: 'Loading…',
|
||||
xosanNotAvailable: 'XOSAN is not available at the moment',
|
||||
xosanRegisterBeta: 'Register for the XOSAN beta',
|
||||
xosanSuccessfullyRegistered: 'You have successfully registered for the XOSAN beta. Please wait until your request has been approved.',
|
||||
xosanInstallPackOnHosts: 'Install XOSAN pack on these hosts:',
|
||||
xosanInstallPack: 'Install {pack} v{version}?',
|
||||
xosanNoPackFound: 'No compatible XOSAN pack found for your XenServer versions.',
|
||||
xosanPackRequirements: 'At least one of these version requirements must be satisfied by all the hosts in this pool:',
|
||||
// SR tab XOSAN
|
||||
xosanVmsNotRunning: 'Some XOSAN Virtual Machines are not running',
|
||||
xosanVmsNotFound: 'Some XOSAN Virtual Machines could not be found',
|
||||
xosanFilesNeedingHealing: 'Files needing healing',
|
||||
xosanFilesNeedHealing: 'Some XOSAN Virtual Machines have files needing healing',
|
||||
xosanHostNotInNetwork: 'Host {hostName} is not in XOSAN network',
|
||||
xosanVm: 'VM controller',
|
||||
xosanUnderlyingStorage: 'SR',
|
||||
xosanReplace: 'Replace…',
|
||||
xosanOnSameVm: 'On same VM',
|
||||
xosanBrickName: 'Brick name',
|
||||
xosanBrickUuid: 'Brick UUID',
|
||||
xosanBrickSize: 'Brick size',
|
||||
xosanMemorySize: 'Memory size',
|
||||
xosanStatus: 'Status',
|
||||
xosanArbiter: 'Arbiter',
|
||||
xosanUsedInodes: 'Used Inodes',
|
||||
xosanBlockSize: 'Block size',
|
||||
xosanDevice: 'Device',
|
||||
xosanFsName: 'FS name',
|
||||
xosanMountOptions: 'Mount options',
|
||||
xosanPath: 'Path',
|
||||
xosanJob: 'Job',
|
||||
xosanPid: 'PID',
|
||||
xosanPort: 'Port',
|
||||
xosanReplaceBrickErrorTitle: 'Missing values',
|
||||
xosanReplaceBrickErrorMessage: 'You need to select a SR and a size',
|
||||
xosanAddSubvolumeErrorTitle: 'Bad values',
|
||||
xosanAddSubvolumeErrorMessage: 'You need to select {nSrs, number} and a size',
|
||||
xosanSelectNSrs: 'Select {nSrs, number} SRs',
|
||||
xosanRun: 'Run',
|
||||
xosanRemove: 'Remove',
|
||||
xosanVolume: 'Volume',
|
||||
xosanVolumeOptions: 'Volume options',
|
||||
xosanCouldNotFindVM: 'Could not find VM',
|
||||
xosanUnderlyingStorageUsage: 'Using {usage}',
|
||||
xosanCustomIpNetwork: 'Custom IP network (/24)',
|
||||
xosanIssueHostNotInNetwork: 'Will configure the host xosan network device with a static IP address and plug it in.'
|
||||
}
|
||||
forEach(messages, function (message, id) {
|
||||
if (isString(message)) {
|
||||
|
||||
@@ -28,7 +28,7 @@ function assertIpv4 (str, 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')
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
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 {
|
||||
@@ -44,7 +48,18 @@ import {
|
||||
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 +74,38 @@ 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,10 +1,12 @@
|
||||
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 {
|
||||
@@ -12,171 +14,110 @@ import {
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.array
|
||||
uiSchema: propTypes.object
|
||||
})
|
||||
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>
|
||||
<legend>{label}</legend>
|
||||
{descriptionRender(schema.description)}
|
||||
<hr />
|
||||
{!props.required &&
|
||||
<div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={disabled}
|
||||
onChange={this._handleOptionalChange}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
{use &&
|
||||
<div className={'card-block'}>
|
||||
<ul style={{'paddingLeft': 0}} >
|
||||
{map(this.state.children, (child, index) =>
|
||||
cloneElement(child, { ref: index })
|
||||
)}
|
||||
</ul>
|
||||
<button disabled={disabled} className='btn btn-primary pull-right mt-1 mr-1' type='button' onClick={this._handleAdd}>
|
||||
{_('add')}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
{!required && <div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={disabled}
|
||||
onChange={this.linkState('use')}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>}
|
||||
{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
|
||||
btnStyle='primary'
|
||||
className='pull-right mt-1 mr-1'
|
||||
disabled={disabled}
|
||||
onClick={this._onAddItem}
|
||||
>
|
||||
{_('add')}
|
||||
</Button>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
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'
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
</PrimitiveInputWrapper>
|
||||
|
||||
@@ -1,33 +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,
|
||||
disabled,
|
||||
schema: { enum: enumValues, enumNames = enumValues },
|
||||
required
|
||||
} = props
|
||||
} = 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'
|
||||
@@ -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
|
||||
uiSchema: propTypes.object
|
||||
})
|
||||
@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.
|
||||
|
||||
@@ -62,19 +62,19 @@ export const PrimitiveInputWrapper = ({ label, required = false, schema, childre
|
||||
|
||||
// ===================================================================
|
||||
|
||||
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,163 +1,97 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.object
|
||||
uiSchema: 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)}
|
||||
<legend>{label}</legend>
|
||||
{descriptionRender(schema.description)}
|
||||
<hr />
|
||||
{!props.required &&
|
||||
<div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={props.disabled}
|
||||
onChange={this._handleOptionalChange}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
{use &&
|
||||
<div className='card-block'>
|
||||
{map(state.children, (child, index) =>
|
||||
cloneElement(child, { ref: index })
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
{!required && <div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={disabled}
|
||||
onChange={this.linkState('use')}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>}
|
||||
{use && <div className='card-block'>
|
||||
{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,8 +1,11 @@
|
||||
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'
|
||||
|
||||
// ===================================================================
|
||||
@@ -10,22 +13,36 @@ import { PrimitiveInputWrapper } from './helpers'
|
||||
@propTypes({
|
||||
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'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -19,6 +19,7 @@ const _IGNORED_TAGNAMES = {
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
className: propTypes.string,
|
||||
tagName: propTypes.string
|
||||
})
|
||||
export class BlockLink extends Component {
|
||||
@@ -44,11 +45,22 @@ export class BlockLink extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
_addAuxClickListener = ref => {
|
||||
// FIXME: when https://github.com/facebook/react/issues/8529 is fixed,
|
||||
// remove and use onAuxClickCapture.
|
||||
// In Chrome ^55, middle-clicking triggers auxclick event instead of click
|
||||
if (ref !== null) {
|
||||
ref.addEventListener('auxclick', this._onClickCapture)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, tagName = 'div' } = this.props
|
||||
const { children, tagName = 'div', className } = this.props
|
||||
const Component = tagName
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
ref={this._addAuxClickListener}
|
||||
style={this._style}
|
||||
onClickCapture={this._onClickCapture}
|
||||
>
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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
|
||||
@@ -22,28 +25,108 @@ 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(
|
||||
<div>
|
||||
<Header closeButton>
|
||||
<Title>{title}</Title>
|
||||
</Header>
|
||||
<Body>{body}</Body>
|
||||
<Footer>
|
||||
<Button bsStyle='primary' onClick={() => {
|
||||
resolve()
|
||||
instance.close()
|
||||
}}>
|
||||
{_('alertOk')}
|
||||
@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>
|
||||
<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>
|
||||
</Footer>
|
||||
</div>,
|
||||
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 +139,38 @@ 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()
|
||||
}
|
||||
|
||||
_style = { marginRight: '0.5em' }
|
||||
|
||||
render () {
|
||||
const { Body, Footer, Header, Title } = ReactModal
|
||||
const { title, icon } = this.props
|
||||
|
||||
const body = _addRef(this.props.children, 'body')
|
||||
|
||||
return <div>
|
||||
<Header closeButton>
|
||||
<Title>
|
||||
{icon
|
||||
? <span><Icon icon={icon} /> {title}</span>
|
||||
: title
|
||||
}
|
||||
</Title>
|
||||
</Header>
|
||||
<Body>
|
||||
{body}
|
||||
</Body>
|
||||
<Footer>
|
||||
<Button
|
||||
bsStyle='primary'
|
||||
onClick={this._resolve}
|
||||
style={this._style}
|
||||
>
|
||||
{_('confirmOk')}
|
||||
</Button>
|
||||
<Button
|
||||
bsStyle='secondary'
|
||||
onClick={this._reject}
|
||||
>
|
||||
{_('confirmCancel')}
|
||||
</Button>
|
||||
</Footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
const CONFIRM_BUTTONS = [ { btnStyle: 'primary', label: _('confirmOk') } ]
|
||||
|
||||
export const confirm = ({
|
||||
body,
|
||||
title,
|
||||
icon = 'alarm'
|
||||
icon = 'alarm',
|
||||
title
|
||||
}) => (
|
||||
chooseAction({
|
||||
body,
|
||||
buttons: CONFIRM_BUTTONS,
|
||||
icon,
|
||||
title
|
||||
})
|
||||
)
|
||||
|
||||
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 +180,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 +206,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>
|
||||
)
|
||||
|
||||
30
src/common/no-objects.js
Normal file
30
src/common/no-objects.js
Normal file
@@ -0,0 +1,30 @@
|
||||
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
|
||||
@@ -8,15 +8,17 @@ export let info
|
||||
export let success
|
||||
|
||||
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
|
||||
|
||||
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 'react'
|
||||
|
||||
// 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 }
|
||||
25
src/common/react-novnc.js
vendored
25
src/common/react-novnc.js
vendored
@@ -1,14 +1,13 @@
|
||||
import React, { Component } from 'react'
|
||||
import RFB from '@nraynaud/novnc/lib/rfb'
|
||||
import { createBackoff } from 'jsonrpc-websocket-client'
|
||||
import { RFB } from 'novnc-node'
|
||||
import {
|
||||
format as formatUrl,
|
||||
parse as parseUrl,
|
||||
resolve as resolveUrl
|
||||
} from 'url'
|
||||
import { enable as enableShortcuts, disable as disableShortcuts } from 'shortcuts'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const parseRelativeUrl = url => parseUrl(resolveUrl(String(window.location), url))
|
||||
|
||||
@@ -42,7 +41,7 @@ export default class NoVnc extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (state !== 'disconnected') {
|
||||
if (state !== 'disconnected' || this.refs.canvas == null) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -77,6 +76,11 @@ export default class NoVnc extends Component {
|
||||
_connect = () => {
|
||||
this._clean()
|
||||
|
||||
const { canvas } = this.refs
|
||||
if (!canvas) {
|
||||
return
|
||||
}
|
||||
|
||||
const url = parseRelativeUrl(this.props.url)
|
||||
fixProtocol(url)
|
||||
|
||||
@@ -86,14 +90,23 @@ export default class NoVnc extends Component {
|
||||
const rfb = this._rfb = new RFB({
|
||||
encrypt: isSecure,
|
||||
target: this.refs.canvas,
|
||||
wsProtocols: [ 'chat' ],
|
||||
onClipboard: onClipboardChange && ((_, text) => {
|
||||
onClipboardChange(text)
|
||||
}),
|
||||
onUpdateState: this._onUpdateState
|
||||
})
|
||||
|
||||
rfb.connect(formatUrl(url))
|
||||
// remove leading slashes from the path
|
||||
//
|
||||
// a leading slassh will be added by noVNC
|
||||
const clippedPath = url.path.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()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from 'intl'
|
||||
import React from 'react'
|
||||
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import { createGetObject } from './selectors'
|
||||
import { isSrWritable } from './xo'
|
||||
import {
|
||||
@@ -165,7 +165,7 @@ const xoItemToRender = {
|
||||
// PIF.
|
||||
PIF: pif => (
|
||||
<span>
|
||||
<Icon icon='network' /> {pif.device}
|
||||
<Icon icon='network' color={pif.carrier ? 'text-success' : 'text-danger'} /> {pif.device} ({pif.deviceName})
|
||||
</span>
|
||||
),
|
||||
|
||||
@@ -182,6 +182,10 @@ const renderXoItem = (item, {
|
||||
} = {}) => {
|
||||
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`)
|
||||
@@ -218,7 +222,9 @@ const GenericXoItem = connectStore(() => {
|
||||
})
|
||||
})(({ xoItem, ...props }) => xoItem
|
||||
? renderXoItem(xoItem, props)
|
||||
: <span className='text-muted'>{_('errorNoSuchItem')}</span>
|
||||
: renderXoUnknownItem()
|
||||
)
|
||||
|
||||
export const renderXoItemFromId = (id, props) => <GenericXoItem {...props} id={id} />
|
||||
|
||||
export const renderXoUnknownItem = () => <span className='text-muted'>{_('errorNoSuchItem')}</span>
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
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'
|
||||
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
|
||||
@@ -130,24 +142,20 @@ const getDayName = (dayNum) =>
|
||||
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)
|
||||
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) => (
|
||||
@@ -179,7 +187,7 @@ class ToggleTd extends Component {
|
||||
render () {
|
||||
const { props } = this
|
||||
return (
|
||||
<td style={{ cursor: 'pointer' }} className={props.value ? 'table-success' : ''} onClick={this._onClick}>
|
||||
<td style={CLICKABLE} className={props.value ? 'table-success' : ''} onClick={this._onClick}>
|
||||
{props.children}
|
||||
</td>
|
||||
)
|
||||
@@ -189,14 +197,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
|
||||
})
|
||||
class TableSelect extends Component {
|
||||
static defaultProps = {
|
||||
optionsRenderer: value => value
|
||||
optionRenderer: value => value
|
||||
}
|
||||
|
||||
_reset = () => {
|
||||
@@ -226,214 +235,271 @@ class TableSelect extends Component {
|
||||
|
||||
render () {
|
||||
const {
|
||||
labelId,
|
||||
options,
|
||||
optionsRenderer,
|
||||
optionRenderer,
|
||||
value
|
||||
} = this.props
|
||||
const { length } = options[0]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className='table table-bordered table-sm'>
|
||||
<tbody>
|
||||
{map(options, (line, i) => (
|
||||
<tr key={i}>
|
||||
{map(line, (tdOption, j) => {
|
||||
const tdId = length * i + j
|
||||
return (
|
||||
<ToggleTd
|
||||
children={optionsRenderer(tdOption)}
|
||||
tdId={tdId}
|
||||
key={tdId}
|
||||
onChange={this._handleChange}
|
||||
value={includes(value, tdId)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className='btn btn-secondary pull-right' onClick={this._reset}>
|
||||
{_('selectTableReset')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
return <div>
|
||||
<table className='table table-bordered table-sm'>
|
||||
<tbody>
|
||||
{map(options, (line, i) => (
|
||||
<tr key={i}>
|
||||
{map(line, tdOption => (
|
||||
<ToggleTd
|
||||
children={optionRenderer(tdOption)}
|
||||
tdId={tdOption}
|
||||
key={tdOption}
|
||||
onChange={this._handleChange}
|
||||
value={includes(value, tdOption)}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Button
|
||||
className='pull-right'
|
||||
onClick={this._reset}
|
||||
>
|
||||
{_(`selectTableAll${labelId}`)} {value && !value.length && <Icon icon='success' />}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// "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
|
||||
value: propTypes.any.isRequired
|
||||
})
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._update(this.props.value)
|
||||
}
|
||||
|
||||
_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 {
|
||||
onChange,
|
||||
headerAddon,
|
||||
labelId,
|
||||
options,
|
||||
optionsRenderer,
|
||||
range,
|
||||
labelId
|
||||
optionRenderer,
|
||||
range
|
||||
} = this.props
|
||||
const { tableValue } = this.state
|
||||
|
||||
const tableSelect = (
|
||||
<TableSelect
|
||||
onChange={this._handleTableValue}
|
||||
options={options}
|
||||
optionsRenderer={optionsRenderer}
|
||||
value={tableValue}
|
||||
/>
|
||||
const {
|
||||
periodic,
|
||||
tableValue,
|
||||
rangeValue
|
||||
} = this.state
|
||||
|
||||
return <Card>
|
||||
<CardHeader>
|
||||
{_(`scheduling${labelId}`)}
|
||||
{headerAddon}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{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 isWeekDayMode = ({ monthDayPattern, weekDayPattern }) => {
|
||||
if (monthDayPattern === '*' && weekDayPattern === '*') {
|
||||
return
|
||||
}
|
||||
|
||||
return weekDayPattern !== '*'
|
||||
}
|
||||
|
||||
@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 { 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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{_(`scheduling${labelId}`)}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{range
|
||||
? (
|
||||
<Tabs bsStyle='tabs' activeKey={this.state.activeKey} onSelect={this._selectTab}>
|
||||
<Tab tabClassName='nav-item' eventKey={NAV_EACH_SELECTED} title={_(`schedulingEachSelected${labelId}`)}>
|
||||
{tableSelect}
|
||||
</Tab>
|
||||
<Tab tabClassName='nav-item' eventKey={NAV_EVERY_N} title={_(`schedulingEveryN${labelId}`)}>
|
||||
<Range ref='range' min={range[0]} max={range[1]} onChange={onChange} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
) : tableSelect
|
||||
}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
)
|
||||
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}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const HOURS_RANGE = [2, 12]
|
||||
const MINUTES_RANGE = [2, 30]
|
||||
|
||||
const decrement = e => e - 1
|
||||
|
||||
@propTypes({
|
||||
cronPattern: propTypes.string.isRequired,
|
||||
cronPattern: propTypes.string,
|
||||
onChange: propTypes.func,
|
||||
timezone: propTypes.string
|
||||
timezone: propTypes.string,
|
||||
value: propTypes.shape({
|
||||
cronPattern: propTypes.string.isRequired,
|
||||
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}`
|
||||
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()
|
||||
})
|
||||
}
|
||||
|
||||
const { props } = this
|
||||
const cronPattern = props.cronPattern.split(' ')
|
||||
cronPattern[PICKTIME_TO_ID[type]] = value
|
||||
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: cronPattern.join(' '),
|
||||
timezone: props.timezone
|
||||
cronPattern: this._getCronPattern(),
|
||||
timezone
|
||||
})
|
||||
}
|
||||
|
||||
_onHourChange = value => this._update('hour', value)
|
||||
_onMinuteChange = value => this._update('minute', value)
|
||||
_onMonthChange = value => this._update('month', value)
|
||||
_onMonthDayChange = value => this._update('monthDay', value)
|
||||
_onWeekDayChange = value => this._update('weekDay', value)
|
||||
_getCronPattern = () => {
|
||||
const { value, cronPattern = value.cronPattern } = this.props
|
||||
return cronPattern
|
||||
}
|
||||
|
||||
_onTimezoneChange = timezone => {
|
||||
const { props } = this
|
||||
props.onChange({
|
||||
cronPattern: props.cronPattern,
|
||||
timezone
|
||||
})
|
||||
_getTimezone = () => {
|
||||
const { value, timezone = value && value.timezone } = this.props
|
||||
return timezone
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
cronPattern,
|
||||
timezone
|
||||
} = this.props
|
||||
const cronPatternArr = cronPattern.split(' ')
|
||||
const cronPatternArr = this._getCronPattern().split(' ')
|
||||
const timezone = this._getTimezone()
|
||||
|
||||
return (
|
||||
<div className='card-block'>
|
||||
@@ -441,25 +507,16 @@ export default class Scheduler extends Component {
|
||||
<Col mediumSize={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']]}
|
||||
<DayPicker
|
||||
onChange={this._dayChange}
|
||||
monthDayPattern={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
|
||||
weekDayPattern={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
|
||||
/>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
@@ -467,14 +524,14 @@ export default class Scheduler extends Component {
|
||||
labelId='Hour'
|
||||
options={HOURS}
|
||||
range={HOURS_RANGE}
|
||||
onChange={this._onHourChange}
|
||||
onChange={this._hourChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['hour']]}
|
||||
/>
|
||||
<TimePicker
|
||||
labelId='Minute'
|
||||
options={MINS}
|
||||
range={MINUTES_RANGE}
|
||||
onChange={this._onMinuteChange}
|
||||
onChange={this._minuteChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['minute']]}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
32
src/common/select-files.js
Normal file
32
src/common/select-files.js
Normal file
@@ -0,0 +1,32 @@
|
||||
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>
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,38 @@
|
||||
import React from 'react'
|
||||
import assign from 'lodash/assign'
|
||||
import classNames from 'classnames'
|
||||
import filter from 'lodash/filter'
|
||||
import flatten from 'lodash/flatten'
|
||||
import forEach from 'lodash/forEach'
|
||||
import groupBy from 'lodash/groupBy'
|
||||
import includes from 'lodash/includes'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import keyBy from 'lodash/keyBy'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import pick from 'lodash/pick'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
import store from 'store'
|
||||
import { parse as parseRemote } from 'xo-remote-parser'
|
||||
import {
|
||||
assign,
|
||||
filter,
|
||||
flatten,
|
||||
forEach,
|
||||
groupBy,
|
||||
includes,
|
||||
isArray,
|
||||
isEmpty,
|
||||
isInteger,
|
||||
isString,
|
||||
keyBy,
|
||||
keys,
|
||||
map,
|
||||
mapValues,
|
||||
pick,
|
||||
sortBy,
|
||||
toArray
|
||||
} from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import renderXoItem from './render-xo-item'
|
||||
import store from './store'
|
||||
import Tooltip from './tooltip'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { Select } from './form'
|
||||
import {
|
||||
createCollectionWrapper,
|
||||
createFilter,
|
||||
createGetObjectsOfType,
|
||||
createGetTags,
|
||||
@@ -31,7 +42,6 @@ import {
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
mapPlus,
|
||||
resolveResourceSets
|
||||
} from './utils'
|
||||
import {
|
||||
@@ -47,6 +57,26 @@ import {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// react-select's line-height is 1.4
|
||||
// https://github.com/JedWatson/react-select/blob/916ab0e62fc7394be8e24f22251c399a68de8b1c/less/multi.less#L33
|
||||
// while bootstrap button's line-height is 1.25
|
||||
// https://github.com/twbs/bootstrap/blob/959c4e527c6ef69623928db638267ba1c370479d/scss/_variables.scss#L342
|
||||
const ADDON_BUTTON_STYLE = { lineHeight: '1.4' }
|
||||
|
||||
const getIds = value => value == null || isString(value) || isInteger(value)
|
||||
? value
|
||||
: isArray(value)
|
||||
? map(value, getIds)
|
||||
: value.id
|
||||
|
||||
const getOption = (object, container) => ({
|
||||
label: container
|
||||
? `${getLabel(object)} ${getLabel(container)}`
|
||||
: getLabel(object),
|
||||
value: object.id,
|
||||
xoItem: object
|
||||
})
|
||||
|
||||
const getLabel = object =>
|
||||
object.name_label ||
|
||||
object.name ||
|
||||
@@ -55,6 +85,10 @@ const getLabel = object =>
|
||||
object.value ||
|
||||
object.label
|
||||
|
||||
const options = props => ({
|
||||
defaultValue: props.multi ? [] : undefined
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
|
||||
/*
|
||||
@@ -86,12 +120,11 @@ const getLabel = object =>
|
||||
@propTypes({
|
||||
autoFocus: propTypes.bool,
|
||||
clearable: propTypes.bool,
|
||||
defaultValue: propTypes.any,
|
||||
disabled: propTypes.bool,
|
||||
hasSelectAll: propTypes.bool,
|
||||
multi: propTypes.bool,
|
||||
onChange: propTypes.func,
|
||||
placeholder: propTypes.any.isRequired,
|
||||
predicate: propTypes.func,
|
||||
required: propTypes.bool,
|
||||
value: propTypes.any,
|
||||
xoContainers: propTypes.array,
|
||||
@@ -101,145 +134,101 @@ const getLabel = object =>
|
||||
]).isRequired
|
||||
})
|
||||
export class GenericSelect extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
value: this._setValue(props.value || props.defaultValue, props)
|
||||
}
|
||||
}
|
||||
_getObjectsById = createSelector(
|
||||
() => this.props.xoObjects,
|
||||
objects => keyBy(
|
||||
isArray(objects)
|
||||
? objects
|
||||
: flatten(toArray(objects)),
|
||||
'id'
|
||||
)
|
||||
)
|
||||
|
||||
_getValue (xoObjectsById = this.state.xoObjectsById, props = this.props) {
|
||||
const { value } = this.state
|
||||
_getOptions = createSelector(
|
||||
() => this.props.xoContainers,
|
||||
() => this.props.xoObjects,
|
||||
(containers, objects) => { // createCollectionWrapper with a depth?
|
||||
const { name } = this.constructor
|
||||
|
||||
if (props.multi) {
|
||||
// Returns the values of the selected objects
|
||||
// if they are contained in xoObjectsById.
|
||||
return mapPlus(value, (value, push) => {
|
||||
const o = xoObjectsById[value.value !== undefined ? value.value : value]
|
||||
|
||||
if (o) {
|
||||
push(o)
|
||||
let options = []
|
||||
if (!containers) {
|
||||
if (__DEV__ && !isArray(objects)) {
|
||||
throw new Error(`${name}: without xoContainers, xoObjects must be an array`)
|
||||
}
|
||||
|
||||
options = map(objects, getOption)
|
||||
} else if (__DEV__ && isArray(objects)) {
|
||||
throw new Error(`${name}: with xoContainers, xoObjects must be an object`)
|
||||
}
|
||||
|
||||
forEach(containers, container => {
|
||||
options.push({
|
||||
disabled: true,
|
||||
xoItem: container
|
||||
})
|
||||
|
||||
forEach(objects[container.id], object => {
|
||||
options.push(getOption(object, container))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return xoObjectsById[value.value || value] || ''
|
||||
}
|
||||
|
||||
// Supports id strings and objects.
|
||||
_setValue (value, props = this.props) {
|
||||
if (props.multi) {
|
||||
return map(value, object => object.id !== undefined ? object.id : object)
|
||||
}
|
||||
|
||||
return (value != null)
|
||||
? value.id !== undefined ? value.id : value
|
||||
: ''
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { props } = this
|
||||
|
||||
this.setState({
|
||||
...this._computeOptions(props)
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
const { props } = this
|
||||
const { value, xoContainers, xoObjects } = newProps
|
||||
|
||||
if (
|
||||
xoContainers !== props.xoContainers ||
|
||||
xoObjects !== props.xoObjects
|
||||
) {
|
||||
const {
|
||||
options,
|
||||
xoObjectsById
|
||||
} = this._computeOptions(newProps)
|
||||
|
||||
const value = this._getValue(xoObjectsById, newProps)
|
||||
|
||||
this.setState({
|
||||
options,
|
||||
value: this._setValue(value, newProps),
|
||||
xoObjectsById
|
||||
})
|
||||
}
|
||||
|
||||
if (value !== props.value) {
|
||||
this.setState({
|
||||
value: this._setValue(value, newProps)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_computeOptions ({ xoContainers, xoObjects }) {
|
||||
if (!xoContainers) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!Array.isArray(xoObjects)) {
|
||||
throw new Error('without xoContainers, xoObjects must be an array')
|
||||
const values = this._getSelectValue()
|
||||
const objectsById = this._getObjectsById()
|
||||
const addIfMissing = val => {
|
||||
if (val && !objectsById[val]) {
|
||||
options.push({
|
||||
disabled: true,
|
||||
id: val,
|
||||
label: val,
|
||||
value: val,
|
||||
xoItem: {
|
||||
id: val,
|
||||
removed: true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
xoObjectsById: keyBy(xoObjects, 'id'),
|
||||
options: map(xoObjects, object => ({
|
||||
label: getLabel(object),
|
||||
value: object.id,
|
||||
xoItem: object
|
||||
}))
|
||||
if (isArray(values)) {
|
||||
forEach(values, addIfMissing)
|
||||
} else {
|
||||
addIfMissing(values)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
)
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (Array.isArray(xoObjects)) {
|
||||
throw new Error('with xoContainers, xoObjects must be an object')
|
||||
}
|
||||
}
|
||||
_getSelectValue = createSelector(
|
||||
() => this.props.value,
|
||||
createCollectionWrapper(getIds)
|
||||
)
|
||||
|
||||
const options = []
|
||||
const xoObjectsById = {}
|
||||
_getNewSelectedObjects = createSelector(
|
||||
this._getObjectsById,
|
||||
value => value,
|
||||
(objectsById, value) => value == null
|
||||
? value
|
||||
: isArray(value)
|
||||
? map(value, value => objectsById[value.value])
|
||||
: objectsById[value.value]
|
||||
)
|
||||
|
||||
forEach(xoContainers, container => {
|
||||
const containerObjects = keyBy(xoObjects[container.id], 'id')
|
||||
assign(xoObjectsById, containerObjects)
|
||||
|
||||
options.push({
|
||||
disabled: true,
|
||||
xoItem: container
|
||||
})
|
||||
|
||||
options.push.apply(options, map(containerObjects, object => ({
|
||||
label: `${getLabel(object)} ${getLabel(container)}`,
|
||||
value: object.id,
|
||||
xoItem: object
|
||||
})))
|
||||
})
|
||||
|
||||
return { xoObjectsById, options }
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this._getValue()
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.setState({
|
||||
value: this._setValue(value)
|
||||
})
|
||||
}
|
||||
|
||||
_handleChange = value => {
|
||||
_onChange = value => {
|
||||
const { onChange } = this.props
|
||||
if (onChange) {
|
||||
onChange(this._getNewSelectedObjects(value))
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
value: this._setValue(value)
|
||||
}, onChange && (() => onChange(this.value)))
|
||||
_selectAll = () => {
|
||||
this._onChange(
|
||||
filter(this._getOptions(), ({ disabled }) => !disabled)
|
||||
)
|
||||
}
|
||||
|
||||
// GroupBy: Display option with margin if not disabled and containers exists.
|
||||
_renderOption = option => (
|
||||
_renderOption = option =>
|
||||
<span
|
||||
className={classNames(
|
||||
!option.disabled && this.props.xoContainers && 'ml-1'
|
||||
@@ -247,56 +236,68 @@ export class GenericSelect extends Component {
|
||||
>
|
||||
{renderXoItem(option.xoItem)}
|
||||
</span>
|
||||
)
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const {
|
||||
autoFocus,
|
||||
disabled,
|
||||
hasSelectAll,
|
||||
multi,
|
||||
placeholder,
|
||||
required,
|
||||
|
||||
return (
|
||||
<Select
|
||||
autofocus={props.autoFocus}
|
||||
clearable={props.clearable}
|
||||
disabled={props.disabled}
|
||||
multi={props.multi}
|
||||
onChange={this._handleChange}
|
||||
openOnFocus
|
||||
optionRenderer={this._renderOption}
|
||||
options={state.options}
|
||||
placeholder={props.placeholder}
|
||||
required={props.required}
|
||||
value={state.value}
|
||||
valueRenderer={this._renderOption}
|
||||
/>
|
||||
)
|
||||
clearable = Boolean(multi || !required)
|
||||
} = this.props
|
||||
|
||||
const select = <Select
|
||||
{...{
|
||||
autofocus: autoFocus,
|
||||
clearable,
|
||||
disabled,
|
||||
multi,
|
||||
placeholder,
|
||||
required
|
||||
}}
|
||||
|
||||
onChange={this._onChange}
|
||||
openOnFocus
|
||||
optionRenderer={this._renderOption}
|
||||
options={this._getOptions()}
|
||||
value={this._getSelectValue()}
|
||||
valueRenderer={this._renderOption}
|
||||
/>
|
||||
|
||||
if (!multi || !hasSelectAll) {
|
||||
return select
|
||||
}
|
||||
|
||||
// `hasSelectAll` should be provided by react-select after this pull request has been merged:
|
||||
// https://github.com/JedWatson/react-select/pull/748
|
||||
// TODO: remove once it has been merged upstream.
|
||||
return <div className='input-group'>
|
||||
{select}
|
||||
<span className='input-group-btn'>
|
||||
<Tooltip content={_('selectAll')}>
|
||||
<Button onClick={this._selectAll} style={ADDON_BUTTON_STYLE}>
|
||||
<Icon icon='add' />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const makeStoreSelect = (createSelectors, props) => connectStore(
|
||||
createSelectors,
|
||||
{ withRef: true }
|
||||
)(
|
||||
class extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
{...props}
|
||||
{...this.props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
const makeStoreSelect = (createSelectors, defaultProps) => uncontrollableInput(options)(
|
||||
connectStore(createSelectors)(
|
||||
props =>
|
||||
<GenericSelect
|
||||
{...defaultProps}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
|
||||
const makeSubscriptionSelect = (subscribe, props) => (
|
||||
const makeSubscriptionSelect = (subscribe, props) => uncontrollableInput(options)(
|
||||
class extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
@@ -325,14 +326,6 @@ const makeSubscriptionSelect = (subscribe, props) => (
|
||||
)
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribe(::this.setState)
|
||||
}
|
||||
@@ -340,7 +333,6 @@ const makeSubscriptionSelect = (subscribe, props) => (
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
{...props}
|
||||
{...this.props}
|
||||
xoObjects={this._getFilteredXoObjects()}
|
||||
@@ -521,11 +513,11 @@ export const SelectTag = makeStoreSelect((_, props) => ({
|
||||
}), { placeholder: _('selectTags') })
|
||||
|
||||
export const SelectHighLevelObject = makeStoreSelect(() => {
|
||||
const getHosts = createGetObjectsOfType('host')
|
||||
const getNetworks = createGetObjectsOfType('network')
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
const getSrs = createGetObjectsOfType('SR')
|
||||
const getVms = createGetObjectsOfType('VM')
|
||||
const getHosts = createGetObjectsOfType('host').filter(getPredicate)
|
||||
const getNetworks = createGetObjectsOfType('network').filter(getPredicate)
|
||||
const getPools = createGetObjectsOfType('pool').filter(getPredicate)
|
||||
const getSrs = createGetObjectsOfType('SR').filter(getPredicate)
|
||||
const getVms = createGetObjectsOfType('VM').filter(getPredicate)
|
||||
|
||||
const getHighLevelObjects = createSelector(
|
||||
getHosts,
|
||||
|
||||
@@ -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'
|
||||
@@ -37,12 +40,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
|
||||
}
|
||||
@@ -122,9 +129,9 @@ export const createCounter = (collection, predicate) =>
|
||||
//
|
||||
// Should only be used with a reasonable number of properties.
|
||||
export const createPicker = (object, props) =>
|
||||
_createCollectionWrapper(
|
||||
_create2(
|
||||
object, props,
|
||||
_create2(
|
||||
object, props,
|
||||
_createCollectionWrapper(
|
||||
(object, props) => {
|
||||
const values = {}
|
||||
forEach(props, prop => {
|
||||
@@ -142,10 +149,10 @@ export const createPicker = (object, props) =>
|
||||
// - predicate == null → no filtering
|
||||
// - predicate === false → everything is filtered out
|
||||
export const createFilter = (collection, predicate) =>
|
||||
_createCollectionWrapper(
|
||||
_create2(
|
||||
collection,
|
||||
predicate,
|
||||
_create2(
|
||||
collection,
|
||||
predicate,
|
||||
_createCollectionWrapper(
|
||||
(collection, predicate) => predicate === false
|
||||
? (isArrayLike(collection) ? EMPTY_ARRAY : EMPTY_OBJECT)
|
||||
: predicate
|
||||
@@ -168,17 +175,18 @@ export const createGroupBy = (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) => {
|
||||
const start = (page - 1) * n
|
||||
return slice(array, start, start + n)
|
||||
}
|
||||
_createCollectionWrapper(
|
||||
(array, page, n) => {
|
||||
const start = (page - 1) * n
|
||||
return slice(array, start, start + n)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
export const createSort = (
|
||||
collection,
|
||||
@@ -186,12 +194,19 @@ export const createSort = (
|
||||
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,
|
||||
_create2(
|
||||
collection,
|
||||
iteratee,
|
||||
n,
|
||||
_createCollectionWrapper(
|
||||
(objects, iteratee, n) => {
|
||||
let results = orderBy(objects, iteratee, 'desc')
|
||||
if (n < results.length) {
|
||||
@@ -217,6 +232,36 @@ 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,
|
||||
@@ -312,7 +357,7 @@ export const createSortForType = invoke(() => {
|
||||
return (type, collection) => createSort(
|
||||
collection,
|
||||
autoSelector(type, getIteratees),
|
||||
autoSelector(type, getOrders),
|
||||
autoSelector(type, getOrders)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -413,6 +458,24 @@ export const createGetTags = collectionSelectors => {
|
||||
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(
|
||||
@@ -428,9 +491,10 @@ 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(
|
||||
// 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) => {
|
||||
@@ -450,26 +514,45 @@ export const createDoesHostNeedRestart = hostSelector => {
|
||||
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 => {
|
||||
const metrics = {
|
||||
count: 0,
|
||||
cpus: 0,
|
||||
memoryTotal: 0,
|
||||
memoryUsage: 0
|
||||
_createCollectionWrapper(
|
||||
hosts => {
|
||||
const metrics = {
|
||||
count: 0,
|
||||
cpus: 0,
|
||||
memoryTotal: 0,
|
||||
memoryUsage: 0
|
||||
}
|
||||
forEach(hosts, host => {
|
||||
metrics.count++
|
||||
metrics.cpus += host.cpus.cores
|
||||
metrics.memoryTotal += host.memory.size
|
||||
metrics.memoryUsage += host.memory.usage
|
||||
})
|
||||
return metrics
|
||||
}
|
||||
forEach(hosts, host => {
|
||||
metrics.count++
|
||||
metrics.cpus += host.cpus.cores
|
||||
metrics.memoryTotal += host.memory.size
|
||||
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,6 +27,10 @@ const shallowEqual = (c1, c2) => {
|
||||
return true
|
||||
}
|
||||
|
||||
if (type !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
let n = 0
|
||||
for (const _ in c2) { // eslint-disable-line no-unused-vars
|
||||
++n
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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' }
|
||||
|
||||
@@ -6,3 +6,12 @@
|
||||
color: #fff;
|
||||
background-color: #96b8d1;
|
||||
}
|
||||
|
||||
.clickableRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
outline: 2px solid #366e98;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
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,
|
||||
debounce,
|
||||
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 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'
|
||||
@@ -32,9 +47,8 @@ import styles from './index.css'
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
defaultFilter: propTypes.string,
|
||||
filters: propTypes.object,
|
||||
nFilteredItems: propTypes.number.isRequired,
|
||||
nItems: propTypes.number.isRequired,
|
||||
onChange: propTypes.func.isRequired
|
||||
})
|
||||
class TableFilter extends Component {
|
||||
@@ -56,10 +70,9 @@ class TableFilter extends Component {
|
||||
|
||||
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'>
|
||||
: <span className='input-group-btn'>
|
||||
<Dropdown id='filter'>
|
||||
<DropdownToggle bsStyle='info'>
|
||||
<Icon icon='search' />
|
||||
@@ -72,18 +85,27 @@ class TableFilter extends Component {
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>}
|
||||
</span>}
|
||||
<input
|
||||
type='text'
|
||||
ref='filter'
|
||||
onChange={this._onChange}
|
||||
className='form-control'
|
||||
defaultValue={props.defaultFilter}
|
||||
onChange={this._onChange}
|
||||
ref='filter'
|
||||
/>
|
||||
<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,7 +115,7 @@ class TableFilter extends Component {
|
||||
|
||||
@propTypes({
|
||||
columnId: propTypes.number.isRequired,
|
||||
name: propTypes.any.isRequired,
|
||||
name: propTypes.node,
|
||||
sort: propTypes.func,
|
||||
sortIcon: propTypes.string
|
||||
})
|
||||
@@ -104,10 +126,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 +137,7 @@ class ColumnHead extends Component {
|
||||
return (
|
||||
<th
|
||||
className={classNames(
|
||||
textAlign && `text-xs-${textAlign}`,
|
||||
styles.clickableColumn,
|
||||
isSelected && classNames('text-white', 'bg-info')
|
||||
)}
|
||||
@@ -131,37 +154,86 @@ class ColumnHead extends Component {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const DEFAULT_ITEMS_PER_PAGE = 10
|
||||
@propTypes({
|
||||
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` ids in parameters
|
||||
// individualActions: the function will be called with the related item's id in parameters
|
||||
handler: propTypes.func.isRequired,
|
||||
icon: propTypes.string.isRequired,
|
||||
label: propTypes.node.isRequired,
|
||||
level: propTypes.oneOf([ 'warning', 'danger' ])
|
||||
}))
|
||||
|
||||
@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.isRequired,
|
||||
itemRenderer: propTypes.func.isRequired,
|
||||
name: propTypes.node,
|
||||
itemRenderer: propTypes.func,
|
||||
sortCriteria: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
]),
|
||||
sortOrder: propTypes.string
|
||||
sortOrder: propTypes.string,
|
||||
textAlign: propTypes.string
|
||||
})).isRequired,
|
||||
filterContainer: propTypes.func,
|
||||
filterUrlParam: propTypes.string,
|
||||
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,
|
||||
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,8 +245,15 @@ export default class SortedTable extends Component {
|
||||
}
|
||||
|
||||
this.state = {
|
||||
all: false, // whether all items are selected (accross pages)
|
||||
filter: defined(
|
||||
() => context.router.location.query[props.filterUrlParam],
|
||||
() => props.filters[props.defaultFilter]
|
||||
),
|
||||
selectedColumn,
|
||||
itemsPerPage: props.itemsPerPage || DEFAULT_ITEMS_PER_PAGE
|
||||
sortOrder: props.columns[selectedColumn].sortOrder === 'desc'
|
||||
? 'desc'
|
||||
: 'asc'
|
||||
}
|
||||
|
||||
this._getSelectedColumn = () =>
|
||||
@@ -184,11 +263,11 @@ export default class SortedTable extends Component {
|
||||
() => this.props.collection
|
||||
)
|
||||
|
||||
this._getAllItems = createSort(
|
||||
this._getItems = createSort(
|
||||
createFilter(
|
||||
() => this.props.collection,
|
||||
createSelector(
|
||||
() => this.state.filter || '',
|
||||
() => this.state.filter,
|
||||
createMatcher
|
||||
)
|
||||
),
|
||||
@@ -206,16 +285,67 @@ export default class SortedTable extends Component {
|
||||
this.state.activePage = 1
|
||||
|
||||
this._getVisibleItems = createPager(
|
||||
this._getAllItems,
|
||||
this._getItems,
|
||||
() => this.state.activePage,
|
||||
this.state.itemsPerPage
|
||||
this.props.itemsPerPage
|
||||
)
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.setState({
|
||||
sortOrder: this.props.columns[this.state.selectedColumn].sortOrder === 'desc' ? 'desc' : 'asc'
|
||||
})
|
||||
this.state.selectedItemsIds = new Set()
|
||||
|
||||
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.refs.filter.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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@@ -246,30 +376,238 @@ export default class SortedTable extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const { selectedItemsIds } = this.state
|
||||
|
||||
// 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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onPageSelection = (_, event) => this.setState({
|
||||
activePage: event.eventKey
|
||||
activePage: event.eventKey,
|
||||
highlighted: undefined
|
||||
})
|
||||
|
||||
_onFilterChange = debounce(filter => {
|
||||
_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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
let 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)
|
||||
}
|
||||
|
||||
_onFilterChange = debounce(filter => {
|
||||
const { filterUrlParam } = this.props
|
||||
if (filterUrlParam !== undefined) {
|
||||
const { router } = this.context
|
||||
const { location } = router
|
||||
router.replace({
|
||||
...location,
|
||||
query: {
|
||||
...location.query,
|
||||
[filterUrlParam]: filter
|
||||
}
|
||||
})
|
||||
}
|
||||
this.setState({
|
||||
activePage: 1,
|
||||
filter,
|
||||
activePage: 1
|
||||
highlighted: undefined
|
||||
})
|
||||
}, 500)
|
||||
|
||||
_executeGroupedAction = handler => {
|
||||
const { state } = this
|
||||
return handler(
|
||||
state.all
|
||||
? map(this._getItems(), 'id')
|
||||
: state.selectedItemsIds.toArray()
|
||||
)
|
||||
}
|
||||
|
||||
_executeRowAction = event => {
|
||||
const { props } = this
|
||||
const item = this._getVisibleItems()[event.currentTarget.dataset.index]
|
||||
props.rowAction(item, props.userData)
|
||||
}
|
||||
|
||||
_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._selectItem}
|
||||
type='checkbox'
|
||||
/>
|
||||
</td>
|
||||
const actionsColumn = hasIndividualActions && <td><div className='pull-right'>
|
||||
<ButtonGroup>
|
||||
{map(individualActions, ({ icon, label, level, handler }, key) => <ActionRowButton
|
||||
btnStyle={level}
|
||||
handler={handler}
|
||||
handlerParam={id}
|
||||
icon={icon}
|
||||
key={key}
|
||||
tooltip={label}
|
||||
/>)}
|
||||
</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,
|
||||
rowLink,
|
||||
userData
|
||||
groupedActions,
|
||||
itemsPerPage,
|
||||
paginationContainer,
|
||||
shortcutsTarget
|
||||
} = 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
|
||||
@@ -278,78 +616,133 @@ export default class SortedTable extends Component {
|
||||
ellipsis
|
||||
boundaryLinks
|
||||
maxButtons={10}
|
||||
items={ceil(nFilteredItems / state.itemsPerPage)}
|
||||
activePage={this.state.activePage}
|
||||
items={ceil(nItems / itemsPerPage)}
|
||||
activePage={state.activePage}
|
||||
onSelect={this._onPageSelection}
|
||||
/>
|
||||
)
|
||||
|
||||
const filterInstance = (
|
||||
const filterInstance = displayFilter && (
|
||||
<TableFilter
|
||||
filters={filters}
|
||||
nFilteredItems={nFilteredItems}
|
||||
nItems={this._getTotalNumberOfItems()}
|
||||
defaultFilter={state.filter}
|
||||
filters={props.filters}
|
||||
onChange={this._onFilterChange}
|
||||
ref='filterInput'
|
||||
/>
|
||||
)
|
||||
|
||||
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, ({ icon, label, level, handler }, key) => <ActionRowButton
|
||||
btnStyle={level}
|
||||
handler={this._executeGroupedAction}
|
||||
handlerParam={handler}
|
||||
icon={icon}
|
||||
key={key}
|
||||
tooltip={label}
|
||||
/>)}
|
||||
</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'}
|
||||
/>
|
||||
))}
|
||||
{hasIndividualActions && <th />}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(this._getVisibleItems(), (item, i) => {
|
||||
const columns = map(props.columns, (column, key) => (
|
||||
<td key={key}>
|
||||
{column.itemRenderer(item, userData)}
|
||||
</td>
|
||||
))
|
||||
|
||||
const { id = i } = item
|
||||
|
||||
return rowLink
|
||||
? <BlockLink
|
||||
key={id}
|
||||
tagName='tr'
|
||||
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
|
||||
>{columns}</BlockLink>
|
||||
: <tr key={id}>{columns}</tr>
|
||||
})}
|
||||
{nVisibleItems !== 0
|
||||
? map(this._getVisibleItems(), this._renderItem)
|
||||
: <tr><td className='text-info text-xs-center' colSpan={nColumns}>
|
||||
{_('sortedTableNoItems')}
|
||||
</td></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()}>
|
||||
? <Portal container={() => paginationContainer()}>
|
||||
{paginationInstance}
|
||||
</Portal>
|
||||
) : paginationInstance
|
||||
}
|
||||
: paginationInstance
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
{filterContainer
|
||||
? (
|
||||
<Portal container={() => filterContainer()}>
|
||||
{displayFilter && (
|
||||
filterContainer
|
||||
? <Portal container={() => filterContainer()}>
|
||||
{filterInstance}
|
||||
</Portal>
|
||||
) : filterInstance
|
||||
}
|
||||
: filterInstance
|
||||
)}
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</Container>
|
||||
|
||||
45
src/common/state-button.js
Normal file
45
src/common/state-button.js
Normal file
@@ -0,0 +1,45 @@
|
||||
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,22 @@
|
||||
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)),
|
||||
'toString',
|
||||
{ value: () => type }
|
||||
)
|
||||
}
|
||||
return action
|
||||
})({ type }),
|
||||
'toString',
|
||||
{ value: () => type }
|
||||
)
|
||||
})()
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -102,16 +102,20 @@ export default {
|
||||
|
||||
for (const id in updates) {
|
||||
const object = updates[id]
|
||||
const previous = all[id]
|
||||
|
||||
if (object) {
|
||||
const { type } = object
|
||||
|
||||
all[id] = object
|
||||
get(object.type)[id] = object
|
||||
} else {
|
||||
const previous = all[id]
|
||||
if (previous) {
|
||||
delete all[id]
|
||||
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]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ const TabButton = ({
|
||||
{...props}
|
||||
size='large'
|
||||
style={STYLE}
|
||||
><span className='hidden-md-down'>{_(labelId)}</span></ActionButton>
|
||||
>
|
||||
{labelId !== undefined && <span className='hidden-md-down'>{_(labelId)}</span>}
|
||||
</ActionButton>
|
||||
)
|
||||
export { TabButton as default }
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ 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',
|
||||
@@ -21,6 +21,9 @@ const TAG_STYLE = {
|
||||
padding: '0.3em',
|
||||
verticalAlign: 'middle'
|
||||
}
|
||||
const LINK_STYLE = {
|
||||
cursor: 'pointer'
|
||||
}
|
||||
const ADD_TAG_STYLE = {
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8em',
|
||||
@@ -32,9 +35,10 @@ const REMOVE_TAG_STYLE = {
|
||||
|
||||
@propTypes({
|
||||
labels: propTypes.arrayOf(React.PropTypes.string).isRequired,
|
||||
onAdd: propTypes.func,
|
||||
onChange: propTypes.func,
|
||||
onDelete: propTypes.func,
|
||||
onAdd: propTypes.func
|
||||
onClick: propTypes.func,
|
||||
onDelete: propTypes.func
|
||||
})
|
||||
export default class Tags extends Component {
|
||||
componentWillMount () {
|
||||
@@ -85,6 +89,7 @@ export default class Tags extends Component {
|
||||
labels,
|
||||
onAdd,
|
||||
onChange,
|
||||
onClick,
|
||||
onDelete
|
||||
} = this.props
|
||||
|
||||
@@ -96,7 +101,7 @@ export default class Tags extends Component {
|
||||
{' '}
|
||||
<span>
|
||||
{map(labels.sort(), (label, index) =>
|
||||
<Tag label={label} onDelete={deleteTag} key={index} />
|
||||
<Tag label={label} onDelete={deleteTag} key={index} onClick={onClick} />
|
||||
)}
|
||||
</span>
|
||||
{(onAdd || onChange) && !this.state.editing
|
||||
@@ -118,9 +123,12 @@ export default class Tags extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export const Tag = ({ label, onDelete }) => (
|
||||
export const Tag = ({ type, label, onDelete, onClick }) => (
|
||||
<span style={TAG_STYLE}>
|
||||
{label}{' '}
|
||||
<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' />
|
||||
|
||||
0
src/common/themes/.index-modules
Normal file
0
src/common/themes/.index-modules
Normal file
6
src/common/themes/base.js
Normal file
6
src/common/themes/base.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
disabledStateBg: '#fff',
|
||||
disabledStateColor: '#c0392b',
|
||||
enabledStateBg: '#fff',
|
||||
enabledStateColor: '#27ae60'
|
||||
}
|
||||
@@ -5,95 +5,82 @@ import React from 'react'
|
||||
|
||||
import _ from './intl'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import { getXoServerTimezone } from './xo'
|
||||
import { Select } from './form'
|
||||
|
||||
const XO_SERVER_TIMEZONE = 'xo-server'
|
||||
const SERVER_TIMEZONE_TAG = 'server'
|
||||
const LOCAL_TIMEZONE = moment.tz.guess()
|
||||
|
||||
@propTypes({
|
||||
defaultValue: propTypes.string,
|
||||
onChange: propTypes.func.isRequired,
|
||||
required: propTypes.bool,
|
||||
value: propTypes.string
|
||||
})
|
||||
export default class TimezonePicker extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state.options = map(moment.tz.names(), value => ({ label: value, value }))
|
||||
}
|
||||
|
||||
get value () {
|
||||
const value = this.refs.select.value
|
||||
return (value === XO_SERVER_TIMEZONE) ? null : value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value || XO_SERVER_TIMEZONE
|
||||
}
|
||||
|
||||
_updateTimezone (value) {
|
||||
this.props.onChange(value)
|
||||
}
|
||||
_handleChange = option => {
|
||||
return this._updateTimezone(
|
||||
!option || option.value === XO_SERVER_TIMEZONE
|
||||
? null
|
||||
: option.value
|
||||
)
|
||||
}
|
||||
_useServerTime = () => {
|
||||
this._updateTimezone(null)
|
||||
}
|
||||
_useLocalTime = () => {
|
||||
this._updateTimezone(moment.tz.guess())
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
// Use local timezone (Web browser) if no default value.
|
||||
if (this.props.value === undefined) {
|
||||
this._useLocalTime()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
getXoServerTimezone.then(serverTimezone => {
|
||||
this.setState({
|
||||
options: [{
|
||||
label: _('serverTimezoneOption', {
|
||||
value: serverTimezone
|
||||
}),
|
||||
value: XO_SERVER_TIMEZONE
|
||||
}].concat(this.state.options),
|
||||
serverTimezone
|
||||
timezone: this.props.value || this.props.defaultValue || SERVER_TIMEZONE_TAG,
|
||||
options: [
|
||||
...map(moment.tz.names(), value => ({ label: value, value })),
|
||||
{
|
||||
label: _('serverTimezoneOption', {
|
||||
value: serverTimezone
|
||||
}),
|
||||
value: SERVER_TIMEZONE_TAG
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (props.value !== this.props.value) {
|
||||
this.setState({ timezone: props.value || SERVER_TIMEZONE_TAG })
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.state.timezone === SERVER_TIMEZONE_TAG ? null : this.state.timezone
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.setState({ timezone: value || SERVER_TIMEZONE_TAG })
|
||||
}
|
||||
|
||||
_onChange = option => {
|
||||
if (option && option.value === this.state.timezone) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
timezone: (option != null && option.value) || SERVER_TIMEZONE_TAG
|
||||
}, () =>
|
||||
this.props.onChange(this.state.timezone === SERVER_TIMEZONE_TAG ? null : this.state.timezone)
|
||||
)
|
||||
}
|
||||
|
||||
_useLocalTime = () => {
|
||||
this._onChange({ value: LOCAL_TIMEZONE })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const { timezone, options } = this.state
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='alert alert-info' role='alert'>
|
||||
{_('timezonePickerServerValue')} <strong>{state.serverTimezone}</strong>
|
||||
</div>
|
||||
<Select
|
||||
className='mb-1'
|
||||
defaultValue={props.defaultValue}
|
||||
onChange={this._handleChange}
|
||||
options={state.options}
|
||||
onChange={this._onChange}
|
||||
options={options}
|
||||
placeholder={_('selectTimezone')}
|
||||
ref='select'
|
||||
value={props.value || XO_SERVER_TIMEZONE}
|
||||
required={this.props.required}
|
||||
value={timezone}
|
||||
/>
|
||||
<div className='pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='mr-1'
|
||||
handler={this._useServerTime}
|
||||
icon='time'
|
||||
>
|
||||
{_('timezonePickerUseServerTime')}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
btnStyle='secondary'
|
||||
handler={this._useLocalTime}
|
||||
icon='time'
|
||||
>
|
||||
|
||||
@@ -280,8 +280,8 @@ const getParent = (currentTarget) => {
|
||||
currentParent = currentParent.parentElement
|
||||
}
|
||||
|
||||
const parentTop = currentParent && currentParent.getBoundingClientRect().top || 0
|
||||
const parentLeft = currentParent && currentParent.getBoundingClientRect().left || 0
|
||||
const parentTop = currentParent && currentParent.getBoundingClientRect().top
|
||||
const parentLeft = currentParent && currentParent.getBoundingClientRect().left
|
||||
|
||||
return {parentTop, parentLeft}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.tooltipEnabled {
|
||||
background-color: #222;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $fff;
|
||||
border: 1px solid #fff;
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
@@ -12,7 +12,7 @@
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
transition: opacity 0.3s ease-out, margin-top 0.3s ease-out, margin-left 0.3s ease-out;
|
||||
z-index: 999;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.tooltipDisabled {
|
||||
|
||||
@@ -5,7 +5,7 @@ import ReactDOM from 'react-dom'
|
||||
|
||||
import Component from '../base-component'
|
||||
import getPosition from './get-position'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@@ -17,11 +17,18 @@ export class TooltipViewer extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.state.place = 'top'
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (instance) {
|
||||
throw new Error('Tooltip viewer is a singleton!')
|
||||
}
|
||||
instance = this
|
||||
this.state.place = 'top'
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
instance = undefined
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
import every from 'lodash/every'
|
||||
import forEach from 'lodash/forEach'
|
||||
import getStream from 'get-stream'
|
||||
import humanFormat from 'human-format'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import isPlainObject from 'lodash/isPlainObject'
|
||||
import isString from 'lodash/isString'
|
||||
import join from 'lodash/join'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import React from 'react'
|
||||
import ReadableStream from 'readable-stream'
|
||||
import replace from 'lodash/replace'
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
clone,
|
||||
escapeRegExp,
|
||||
every,
|
||||
forEach,
|
||||
isArray,
|
||||
isEmpty,
|
||||
isFunction,
|
||||
isPlainObject,
|
||||
isString,
|
||||
join,
|
||||
keys,
|
||||
map,
|
||||
mapValues,
|
||||
replace,
|
||||
sample,
|
||||
startsWith
|
||||
} from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
import * as actions from './store/actions'
|
||||
@@ -62,14 +67,27 @@ export const addSubscriptions = subscriptions => Component => {
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this._unsubscribes = map(isFunction(subscriptions) ? subscriptions() : subscriptions, (subscribe, prop) =>
|
||||
subscribe(value => this.setState({ [prop]: value }))
|
||||
this._unsubscribes = map(
|
||||
isFunction(subscriptions)
|
||||
? subscriptions(this.props)
|
||||
: subscriptions,
|
||||
(subscribe, prop) =>
|
||||
subscribe(value => this._setState({ [prop]: value }))
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._setState = this.setState
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
forEach(this._unsubscribes, unsubscribe => unsubscribe())
|
||||
this._unsubscribes = null
|
||||
delete this._setState
|
||||
}
|
||||
|
||||
_setState (nextState) {
|
||||
this.state = { ...this.state, nextState }
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -180,19 +198,6 @@ export { default as Debug } from './debug'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Returns the first defined (non-null, non-undefined) value.
|
||||
export const firstDefined = function () {
|
||||
const n = arguments.length
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const arg = arguments[i]
|
||||
if (arg !== undefined) {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Returns the current XOA Plan or the Plan name if number given
|
||||
export const getXoaPlan = plan => {
|
||||
switch (plan || +process.env.XOA_PLAN) {
|
||||
@@ -258,6 +263,11 @@ export const formatSize = bytes => humanFormat(bytes, { scale: 'binary', unit: '
|
||||
|
||||
export const formatSizeRaw = bytes => humanFormat.raw(bytes, { scale: 'binary', unit: 'B' })
|
||||
|
||||
export const formatSpeed = (bytes, milliseconds) => humanFormat(
|
||||
bytes * 1e3 / milliseconds,
|
||||
{ scale: 'binary', unit: 'B/s' }
|
||||
)
|
||||
|
||||
export const parseSize = size => {
|
||||
let bytes = humanFormat.parse.raw(size, { scale: 'binary' })
|
||||
if (bytes.unit && bytes.unit !== 'B') {
|
||||
@@ -349,20 +359,6 @@ export const throwFn = error => () => {
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function tap (cb) {
|
||||
return this.then(value =>
|
||||
Promise.resolve(cb(value)).then(() => value)
|
||||
)
|
||||
}
|
||||
|
||||
export function rethrow (cb) {
|
||||
return this.catch(error =>
|
||||
Promise.resolve(cb(error)).then(() => { throw error })
|
||||
)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const resolveResourceSet = resourceSet => {
|
||||
@@ -462,3 +458,126 @@ export const htmlFileToStream = file => {
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const resolveId = value =>
|
||||
(value != null && typeof value === 'object' && 'id' in value)
|
||||
? value.id
|
||||
: value
|
||||
|
||||
export const resolveIds = params => {
|
||||
for (const key in params) {
|
||||
const param = params[key]
|
||||
if (param != null && typeof param === 'object' && 'id' in param) {
|
||||
params[key] = param.id
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const OPs = {
|
||||
'<': a => a < 0,
|
||||
'<=': a => a <= 0,
|
||||
'===': a => a === 0,
|
||||
'>': a => a > 0,
|
||||
'>=': a => a >= 0
|
||||
}
|
||||
|
||||
const makeNiceCompare = compare => function () {
|
||||
const { length } = arguments
|
||||
if (length === 2) {
|
||||
return compare(arguments[0], arguments[1])
|
||||
}
|
||||
|
||||
let i = 1
|
||||
let v1 = arguments[0]
|
||||
let op, v2
|
||||
while (i < length) {
|
||||
op = arguments[i++]
|
||||
v2 = arguments[i++]
|
||||
if (!OPs[op](compare(v1, v2))) {
|
||||
return false
|
||||
}
|
||||
v1 = v2
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const compareVersions = makeNiceCompare((v1, v2) => {
|
||||
v1 = v1.split('.')
|
||||
v2 = v2.split('.')
|
||||
|
||||
for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
|
||||
const n1 = +v1[i] || 0
|
||||
const n2 = +v2[i] || 0
|
||||
|
||||
if (n1 < n2) return -1
|
||||
if (n1 > n2) return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
export const isXosanPack = ({ name }) =>
|
||||
startsWith(name, 'XOSAN')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const getCoresPerSocketPossibilities = (maxCoresPerSocket, vCPUs) => {
|
||||
// According to : https://www.citrix.com/blogs/2014/03/11/citrix-xenserver-setting-more-than-one-vcpu-per-vm-to-improve-application-performance-and-server-consolidation-e-g-for-cad3-d-graphical-applications/
|
||||
const maxVCPUs = 16
|
||||
|
||||
const options = []
|
||||
if (maxCoresPerSocket !== undefined && vCPUs !== '') {
|
||||
const ratio = vCPUs / maxVCPUs
|
||||
|
||||
for (let coresPerSocket = maxCoresPerSocket; coresPerSocket >= ratio; coresPerSocket--) {
|
||||
if (vCPUs % coresPerSocket === 0) options.push(coresPerSocket)
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// Generates a random human-readable string of length `length`
|
||||
// Useful to generate random default names intended for the UI user
|
||||
export const generateReadableRandomString = (() => {
|
||||
const CONSONANTS = 'bdfgklmnprtvz'.split('')
|
||||
const VOWELS = 'aeiou'.split('')
|
||||
return (length = 8) => {
|
||||
const result = new Array(length)
|
||||
for (let i = 0; i < length; ++i) {
|
||||
result[i] = sample((i & 1) === 0 ? VOWELS : CONSONANTS)
|
||||
}
|
||||
return result.join('')
|
||||
}
|
||||
})()
|
||||
|
||||
export const cowSet = (object, path, value, depth = 0) => {
|
||||
if (depth >= path.length) {
|
||||
return value
|
||||
}
|
||||
|
||||
object = object != null ? clone(object) : {}
|
||||
const prop = path[depth]
|
||||
object[prop] = cowSet(object[prop], path, value, depth + 1)
|
||||
return object
|
||||
}
|
||||
|
||||
// Generates a function that returns a value between 0 and 1
|
||||
// This function returns an estimated progress value between 0 and 1
|
||||
// based on the elapsed time since the createFakeProgress call and
|
||||
// the given estimated duration d
|
||||
export const createFakeProgress = (() => {
|
||||
const S = 0.95 // Progress value after d seconds
|
||||
return d => {
|
||||
const startTime = Date.now() / 1e3
|
||||
return () => {
|
||||
const x = Date.now() / 1e3 - startTime
|
||||
return -Math.exp((x * Math.log(1 - S)) / d) + 1
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import classNames from 'classnames'
|
||||
import every from 'lodash/every'
|
||||
import map from 'lodash/map'
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
|
||||
import _ from '../intl'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const Wizard = ({ children }) => {
|
||||
const allDone = every(React.Children.toArray(children), (child) => child.props.done || child.props.summary)
|
||||
const allDone = every(React.Children.toArray(children), child =>
|
||||
child.props.done || child.props.summary
|
||||
)
|
||||
|
||||
return <ul className={styles.wizard}>
|
||||
{map(React.Children.toArray(children), (child, key) => cloneElement(child, { allDone, key }))}
|
||||
{React.Children.map(children, (child, key) =>
|
||||
child && cloneElement(child, { allDone, key })
|
||||
)}
|
||||
</ul>
|
||||
}
|
||||
export { Wizard as default }
|
||||
|
||||
63
src/common/xo-defined.js
Normal file
63
src/common/xo-defined.js
Normal file
@@ -0,0 +1,63 @@
|
||||
// Usage:
|
||||
//
|
||||
// ```js
|
||||
// const httpProxy = defined(
|
||||
// process.env.HTTP_PROXY,
|
||||
// process.env.http_proxy
|
||||
// )
|
||||
//
|
||||
// const httpProxy = defined([
|
||||
// process.env.HTTP_PROXY,
|
||||
// process.env.http_proxy
|
||||
// ])
|
||||
// ```
|
||||
export default function defined () {
|
||||
let args = arguments
|
||||
let n = args.length
|
||||
if (n === 1) {
|
||||
args = arguments[0]
|
||||
n = args.length
|
||||
}
|
||||
|
||||
for (let i = 0; i < n; ++i) {
|
||||
let arg = arguments[i]
|
||||
if (typeof arg === 'function') {
|
||||
arg = get(arg)
|
||||
}
|
||||
if (arg !== undefined) {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
//
|
||||
// ```js
|
||||
// const friendName = get(() => props.user.friends[0].name)
|
||||
//
|
||||
// // this form can be used to avoid recreating functions:
|
||||
// const getFriendName = _ => _.friends[0].name
|
||||
// const friendName = get(getFriendName, props.user)
|
||||
// ```
|
||||
export const get = (accessor, arg) => {
|
||||
try {
|
||||
return accessor(arg)
|
||||
} catch (error) {
|
||||
if (!(error instanceof TypeError)) { // avoid hiding other errors
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
//
|
||||
// ```js
|
||||
// const httpAgent = ifDef(
|
||||
// process.env.HTTP_PROXY,
|
||||
// _ => new ProxyAgent(_)
|
||||
// )
|
||||
// ```
|
||||
export const ifDef = (value, thenFn) =>
|
||||
value !== undefined
|
||||
? thenFn(value)
|
||||
: value
|
||||
@@ -1,20 +1,21 @@
|
||||
import map from 'lodash/map'
|
||||
import AbstractInput from '../json-schema-input/abstract-input'
|
||||
import { PureComponent } from 'react'
|
||||
|
||||
import getEventValue from '../get-event-value'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class XoAbstractInput extends AbstractInput {
|
||||
get value () {
|
||||
const value = this.refs.input.value
|
||||
const getId = value => (value != null && value.id) || value
|
||||
|
||||
if (this.props.schema.type === 'array') {
|
||||
return map(value, object => object.id || object)
|
||||
}
|
||||
export default class XoAbstractInput extends PureComponent {
|
||||
_onChange = event => {
|
||||
const value = getEventValue(event)
|
||||
const { props } = this
|
||||
|
||||
return value.id || value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
return props.onChange(
|
||||
props.schema.type === 'array'
|
||||
? map(value, getId)
|
||||
: getId(value)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class HighLevelObjectInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectHighLevelObject
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class HostInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectHost
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class PoolInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectPool
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class RemoteInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectRemote
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class RoleInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectRole
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class SrInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectSr
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user