Compare commits

...

231 Commits

Author SHA1 Message Date
Julien Fontanet
12e1da4ef2 5.8.2 2017-05-23 18:24:21 +02:00
Julien Fontanet
780d072bb7 fix(new/vm): check pool is defined (#2169)
Fixes #2168
2017-05-23 17:38:07 +02:00
Julien Fontanet
f7e5a5cf92 fix(Icon): prop-types → prop-types-decorator 2017-05-17 15:49:34 +02:00
Nicolas Raynaud
3574c8de5c fix(package): update react-select to 1.0.0-rc.4 (#2150)
Fixes #2142
2017-05-17 15:46:04 +02:00
Julien Fontanet
b09ab4d403 fix(Button): fix @propTypes() use 2017-05-17 15:30:36 +02:00
Olivier Lambert
1997f4af51 feat(host): add RAM usage for memory bar in tooltip. Fixes #2149 2017-05-16 21:33:40 +02:00
Danp2
347cd063a3 Fix scanFilesError (#2153) 2017-05-16 21:08:46 +02:00
Olivier Lambert
74a4519a33 fix(i18n): English typo 2017-05-16 15:20:52 +02:00
BCedric
20acf7cfb2 feat(vm/general): display when the VM was last running (#2147)
Fixes #1613
2017-05-16 15:16:44 +02:00
Julien Fontanet
99bc34b2da fix(form/toggle): remove debug trace 2017-05-15 16:48:50 +02:00
Julien Fontanet
f65b5e3ddd feat(settings/server): add click for info on error icon 2017-05-15 16:48:09 +02:00
Julien Fontanet
dc10492b84 fix(xo/connectServer): refresh subscription also in case of error 2017-05-15 16:48:08 +02:00
Julien Fontanet
6f7c10537b fix(vm/advanced): add missing key prop 2017-05-15 16:48:08 +02:00
Julien Fontanet
7f503cfc21 chore(form/toggle): simplify implementation 2017-05-15 16:48:08 +02:00
Julien Fontanet
9dbef0c20a chore(Icon): use propTypes decorator 2017-05-15 16:48:08 +02:00
Julien Fontanet
923166b4e3 feat(Icon): pass extraneous props down 2017-05-15 16:48:08 +02:00
Julien Fontanet
b420128e40 chore(settings/servers): remove useless styles 2017-05-15 16:48:08 +02:00
Julien Fontanet
7776a6ce23 5.8.1 2017-05-12 16:13:44 +02:00
Julien Fontanet
8db949734a feat(settings/servers): improve self-signed error 2017-05-12 16:07:49 +02:00
badrAZ
bb5bdfb9b2 feat(settings/servers): allow unauthorized certificates (#2148)
Fixes #2138
2017-05-12 12:01:08 +02:00
BCedric
9fac3ecd81 feat(backup/file-restore): explicit compatible backups (#2146) 2017-05-11 14:59:31 +02:00
BCedric
8a84cc2627 fix: display when host is disabled (#2121)
Fixes #2098
2017-05-09 17:16:38 +02:00
Julien Fontanet
61179ec67d feat(prop-types): can also be used to set context types 2017-05-09 14:33:15 +02:00
badrAZ
59fc5955ba fix(vm/advanced): affinity host selector (#2143)
Do not remove the current affinity host from the options.

Fixes #2141.
2017-05-09 10:57:31 +02:00
Julien Fontanet
e853ba6244 chore(BaseComponent): use explicit tests 2017-05-07 22:17:20 +02:00
badrAZ
fb40ae7264 feat(vm): ability to choose the cores per socket when creating or editing a VM (#2127)
Fixes #130
2017-05-04 16:12:17 +02:00
badrAZ
f629047be2 chore(vm/new-vm): use _linkState instead of _getOnChange (#2134) 2017-05-04 16:06:50 +02:00
Olivier Lambert
278d8adf1b fix(dashboard): compute correctly the total SR size and used space (#2132)
Fixes #2123
2017-05-03 17:12:58 +02:00
Olivier Lambert
87087d55aa feat(sr): also display unmanaged VDIs (#2131) 2017-05-03 17:07:03 +02:00
Julien Fontanet
46e95fe7eb feat(backup/new): min timeout is 1s 2017-04-28 22:30:32 +02:00
Julien Fontanet
090c9ea4d7 5.8.0 2017-04-28 16:35:45 +02:00
Olivier Lambert
647eb7299e feat(changelog): update changelog 2017-04-28 16:06:47 +02:00
Olivier Lambert
027652e80a feat(changelog): release 5.8 2017-04-28 11:35:40 +02:00
badrAZ
185d380c36 feat(vm/advanced): ability to switch between Cirrus/Standard VGA adapters (#531) (#2091)
Fixes #158
2017-04-27 17:07:54 +02:00
Olivier Lambert
9008b5c4e7 fix(dashboard): flip usage/total. Fixes #2115 2017-04-26 16:42:40 +02:00
Julien Fontanet
f5ad59803e fix(Wizard): take care of falsy children 2017-04-26 12:43:08 +02:00
Pierre Donias
81d1d7ba13 feat(XOSAN): ask user to restart toolstacks after pack installation (#2114) 2017-04-25 16:29:16 +02:00
Julien Fontanet
3328e71805 fix(backup/new): correctly edit schedule.enabled 2017-04-25 15:04:08 +02:00
Julien Fontanet
d7e3dbac26 fix(spelling): VDB → VBD 2017-04-25 14:32:46 +02:00
Olivier Lambert
905182bf2e feat(vm/disks): boot order only for HVMs and devices cannot be non-bootable (#2113)
Fixes #2015
2017-04-25 14:31:41 +02:00
Julien Fontanet
a0146290ee fix(package): downgrade resect to 2.5.4
Since reselect 3, selectors are pure which is incompatible with our use of inputs via closures:

```js
_getFoo = createSelector(
  () => this.props.bar,
  () => this.state.baz,
  (bar, baz) => /* ... */
)
```
2017-04-25 14:14:04 +02:00
Julien Fontanet
173aa22432 fix(intl/messages): correctly format numbers (#2112) 2017-04-25 12:29:56 +02:00
Julien Fontanet
9e5b871ebe chore(Wizard): use React.Children.map 2017-04-25 12:00:44 +02:00
Julien Fontanet
8824ce55ec chore(package): update some dependencies 2017-04-25 12:00:44 +02:00
Olivier Lambert
155edf5533 feat(dashboard): fix power test and remove users link for non-admins (#2111)
Fixes #2108
2017-04-25 12:00:34 +02:00
Olivier Lambert
6d06e1f89d feat(Menu): remove About if non admins (#2110)
Fixes #2109
2017-04-25 11:29:47 +02:00
Julien Fontanet
6d1e2c47d3 5.7.10 2017-04-24 17:53:00 +02:00
Julien Fontanet
8b9b0346cb chore(backup/new): do not send unnecessary job props 2017-04-24 17:51:13 +02:00
Julien Fontanet
0d11817e3f 5.7.9 2017-04-24 15:50:35 +02:00
Julien Fontanet
a8cb209717 chore(backup/new): clarify timeout meaning 2017-04-24 15:49:43 +02:00
Olivier Lambert
cf45ffddf1 feat(sr/disks): provide filters for snapshots (#2103)
Fixes #2102
2017-04-24 15:26:29 +02:00
Pierre Donias
2e0ea51c30 fix(dashboard): replace unused labelId by tooltip (#2100)
Fixes #2090
2017-04-21 17:41:51 +02:00
Julien Fontanet
0f7f8c7330 chore(Combobox): use uncontrollableInput 2017-04-20 12:08:16 +02:00
Julien Fontanet
808f72409f fix(home): replace history when setting initial filter 2017-04-19 17:19:14 +02:00
Julien Fontanet
f8e2d29372 chore(Button): own implementation instead of react-bootstrap (#2089) 2017-04-18 10:12:02 +02:00
Julien Fontanet
22dec27c65 chore(package): update standard to v10 (#2067) 2017-04-13 17:34:49 +02:00
Julien Fontanet
89b3806a7a fix(vm/action-bar): pending status for copy 2017-04-13 17:34:20 +02:00
Julien Fontanet
b6bedf9253 chore(ButtonGroup): own implementation instead of react-bootstrap 2017-04-13 14:43:32 +02:00
Julien Fontanet
0d4983043b feat(vm/tab-snapshots): add pending status on new snasphot 2017-04-13 14:16:07 +02:00
Julien Fontanet
f9ff3fe168 fix(vm/tab-disks): fix StateButton prop 2017-04-13 14:16:07 +02:00
Julien Fontanet
4a25c5323f feat(servers): add pending status 2017-04-13 14:16:07 +02:00
Julien Fontanet
9b4e2d3bb8 feat(vm/action-bar): add pending status 2017-04-13 14:16:07 +02:00
Julien Fontanet
3915efcf92 feat(ActionButton): add pending prop 2017-04-13 14:16:07 +02:00
Julien Fontanet
4591ff8522 chore(ActionButton): do not reassign redirectOnSuccess 2017-04-13 14:16:07 +02:00
Julien Fontanet
e3491797f3 chore(ActionButton): do not use React.PropTypes directly 2017-04-13 14:16:07 +02:00
Julien Fontanet
6eee167675 chore(ActionButton): props documentation 2017-04-13 14:16:07 +02:00
Julien Fontanet
16b965b28a chore(ActionButton): use relative import for Icon 2017-04-13 14:16:07 +02:00
Julien Fontanet
5125410efd chore(ActionButton): do not use react-bootstrap 2017-04-13 14:16:07 +02:00
karolsok
1a4da2a8de feat(intl): improve pl translation (#2088) 2017-04-13 09:50:29 +02:00
Julien Fontanet
991fbaec86 5.7.8 2017-04-12 17:32:37 +02:00
Pierre Donias
fb399278b3 fix(new-vm): add network predicate in VIF item (#2087)
Fixes #2086
2017-04-12 17:32:00 +02:00
Julien Fontanet
b868092365 fix(vms/new): only display Shared checkbox in resource set
Fixes #2061
2017-04-12 17:07:00 +02:00
Pierre Donias
80fdc6849f fix(XOSAN): use new pack format to check if XOSAN pack is installed (#2085) 2017-04-12 16:53:08 +02:00
karolsok
25ffcb952b fix(user/lang selector): fix Polski spelling (#2083) 2017-04-12 12:33:28 +02:00
Julien Fontanet
083ac1e2d6 5.7.7 2017-04-11 17:02:51 +02:00
Julien Fontanet
5a4b553a60 fix(form/Toggle): onChange now emits the raw value
Fixes #2080
2017-04-11 16:54:13 +02:00
Julien Fontanet
b1135ef566 fix(backup/new): do not send timeout=null on creation 2017-04-11 16:38:17 +02:00
Julien Fontanet
1928d1e00f chore(backup/new): remove now unnecessary code 2017-04-11 16:33:30 +02:00
Julien Fontanet
a369f7f387 fix(backup/new): wrap tags in array
Fixes #176
2017-04-11 16:12:08 +02:00
Olivier Lambert
33d9801dfe feat(i18n): add Polish language, fixes #2079 2017-04-11 08:51:44 +02:00
Julien Fontanet
8c7a031cca chore: coding style 2017-04-10 17:32:38 +02:00
Julien Fontanet
9484d87e76 chore(backup/new): clearer timeout label 2017-04-10 17:24:53 +02:00
Julien Fontanet
4b6822d6e5 fix(backup/new): default owner is current user 2017-04-10 17:23:10 +02:00
Julien Fontanet
7241a0529b fix(utils/addSubscription): only use setState() when mounted 2017-04-10 17:05:50 +02:00
Julien Fontanet
66083b4e50 fix(backup/new): timeout should be in seconds
Fixes #2076
2017-04-10 16:47:04 +02:00
karolsok
f631b3cc64 feat(intl): pl translation (#2068) 2017-04-10 16:07:01 +02:00
Julien Fontanet
bb58d9b4d6 5.7.6 2017-04-07 16:16:06 +02:00
Julien Fontanet
93ebff1055 fix(backup/new): default to non smart backup 2017-04-07 16:12:51 +02:00
Julien Fontanet
08aec1c09a fix(backup/new): job creation was broken 2017-04-07 16:10:56 +02:00
Julien Fontanet
8ca98a56fe 5.7.5 2017-04-07 15:52:13 +02:00
Julien Fontanet
705f53e3e5 fix(scheduling): timezone selection 2017-04-07 15:51:49 +02:00
Julien Fontanet
adaf069d20 5.7.4 2017-04-07 15:27:46 +02:00
Julien Fontanet
d7be7d8660 fix(select-objects): do not treat empty string as a value (2) 2017-04-07 15:25:41 +02:00
Julien Fontanet
faddee86b6 fix(select-objects): do not treat empty string as a value 2017-04-07 15:23:27 +02:00
Julien Fontanet
c4fcc65d16 fix(backup/new): coding style 2017-04-07 15:17:51 +02:00
Julien Fontanet
890631d33b fix(select-objects): correctly handle incorrect values with non-multi 2017-04-07 15:17:32 +02:00
Julien Fontanet
8e8145bb48 chore(backup/new): controlled inputs (#2072) 2017-04-07 15:02:53 +02:00
Julien Fontanet
d73d6719a5 5.7.3 2017-04-06 19:30:33 +02:00
Pierre Donias
3419bee198 feat(pack,patch): support 7.1 packs/patches format (#2069)
Fixes #2058
2017-04-06 19:05:51 +02:00
Julien Fontanet
4368fad393 fix(react-novnc): do not error if canvas is not mounted 2017-04-06 17:28:29 +02:00
badrAZ
ab93fdbf10 feat: Display a warning when the CD drive is not completely installed (#2066)
Fixes #2064
2017-04-06 15:22:42 +02:00
Julien Fontanet
8fd7697a45 fix(vm/disks): attach/create disk for non-PV VM 2017-04-06 12:15:22 +02:00
badrAZ
1121a60912 feat(host/network): use StateButton (#2063)
Fixes #2060
2017-04-06 09:58:41 +02:00
Julien Fontanet
e7b4bd2fe4 5.7.2 2017-04-05 15:07:39 +02:00
Julien Fontanet
fcd8bdd1b3 chore(backup/new): simplify smart-backup condition 2017-04-05 14:52:52 +02:00
badrAZ
e6f140f575 fix(select-objects): display missing objects (#2059)
Fixes #2052
2017-04-05 14:52:21 +02:00
Julien Fontanet
bfe4c45fcf fix(xo/configurePlugin): do not swallow error 2017-04-03 10:31:21 +02:00
Julien Fontanet
f95370124b 5.7.1 2017-03-31 18:05:29 +02:00
Julien Fontanet
2564343816 fix(xo-json-schema-input/vm): controlled mode 2017-03-31 18:03:35 +02:00
Julien Fontanet
03734eb761 fix(logs): do not fail on non-string params 2017-03-31 18:03:35 +02:00
Julien Fontanet
29d63a9fdd 5.7.0 2017-03-31 16:36:40 +02:00
Julien Fontanet
ca94b236a8 feat(settings/plugins): easier edition 2017-03-31 16:35:14 +02:00
Julien Fontanet
fa1ec30ba5 chore(json-schema-input): controlled inputs (#2001) 2017-03-31 16:21:54 +02:00
Olivier Lambert
2b1423aebe fix(changelog): it seems we are in 2017. 2017-03-31 14:55:56 +02:00
Pierre Donias
373332141f fix(pool/packs): starter plan required to install packs (#2055) 2017-03-31 11:01:17 +02:00
Olivier Lambert
ecf2cf15b5 fix(changelog): typo in 5.7 release 2017-03-31 10:32:50 +02:00
Olivier Lambert
4ee0831d93 feat(changelog): updates for 5.7 2017-03-31 10:31:11 +02:00
Pierre Donias
7df2a88c13 feat(xosan/pack): check XS version requirement (#2054) 2017-03-31 10:18:43 +02:00
Olivier Lambert
3d52556c67 feat(changelog): updates for 5.6 2017-03-31 10:11:38 +02:00
badrAZ
437b160a3f feat(servers): add label property (#2051)
Fixes #1965
2017-03-29 16:23:51 +02:00
Pierre Donias
5c87b82e0c feat(new-vm,vm): select an affinity host (#2039)
See #1983
2017-03-29 14:07:55 +02:00
badrAZ
7f2bc79d5f feat(ActionButton): improve error reporting (#2050)
Fixes #2048
2017-03-29 12:03:19 +02:00
Pierre Donias
837a61acf3 fix(home): not visible items should never be selected (#2042)
Fixes #2027
Fixes #2035
2017-03-29 10:53:31 +02:00
badrAZ
5971eed72a feat(jobs): configure job timeout (#2043)
Fixes #1956
2017-03-29 10:39:29 +02:00
Pierre Donias
1b8224030b fix(ipPools): prevent creating 2 IP pools with the same name (#2041)
Fixes #1731
2017-03-24 12:26:52 +01:00
Pierre Donias
ed3ec3fa8b fix(vm/disks): do not show bootable flags for non PV VMs (#2040)
Fixes #1996
2017-03-24 11:49:46 +01:00
Pierre Donias
aa98ca49e5 feat(locales): Hungarian (hu) (#2038)
Fixes #2019
2017-03-24 10:36:03 +01:00
badrAZ
44d35c2351 feat: more uses of StateButton (#2034) 2017-03-23 17:46:26 +01:00
badrAZ
df8eb7a000 feat({backup,job}/overview): clearer state (#2023)
Fixes #1958
2017-03-23 09:42:23 +01:00
Julien Fontanet
ac061c8750 chore(backup/new): improve description of report 2017-03-22 12:13:35 +01:00
Julien Fontanet
656d3e55ac feat(backup/new): report on failure by default 2017-03-22 12:09:13 +01:00
Julien Fontanet
50641287f8 fix(XoApp): wait for signin before show pages 2017-03-17 14:48:33 +01:00
Julien Fontanet
0bc072aa65 feat(Home): add a None filter 2017-03-17 14:26:21 +01:00
Julien Fontanet
9d7d665520 chore(Home#_getDefaultFilter): cleaner code 2017-03-17 14:26:21 +01:00
Julien Fontanet
819ea94e7b fix(xo): keep user in store up to date 2017-03-17 14:26:21 +01:00
badrAZ
40753568df fix(settings/remotes): no duplicate names (#2021)
Fixes #1879
2017-03-17 14:11:15 +01:00
badrAZ
8793aed561 feat(home): improve inter-types linkage (#2015)
Fixes #2012
2017-03-16 10:11:52 +01:00
Julien Fontanet
377a50bc09 fix: minor warnings 2017-03-15 17:02:03 +01:00
Julien Fontanet
fe5a43fbdf chore: update yarn.lock 2017-03-15 16:09:17 +01:00
badrAZ
7f44220220 feat(new VM): share a VM (#2013) 2017-03-15 14:38:28 +01:00
greenkeeper[bot]
0df1610ca9 chore(package): update gulp-csso to version 3.0.0 (#2009)
https://greenkeeper.io/
2017-03-14 14:51:53 +01:00
Julien Fontanet
24c8b9e02d chore(auto-controlled-component): remove base-component dep 2017-03-14 11:43:47 +01:00
Pierre Donias
01b311f2ba fix(new-vm): remove bootable option (#2008)
Fixes #2007
2017-03-14 11:27:49 +01:00
Pierre Donias
a2bb3182f4 feat(backup/logs): show job tag in table (#2005)
Fixes #1982
2017-03-14 10:54:39 +01:00
Pierre Donias
c86e15a310 feat(xo/utils/getDefaultNetworkForVif): match network with same VLAN (#1997)
Fixes #1990
2017-03-13 18:03:14 +01:00
Julien Fontanet
862e5a95e7 fix(package): update babel-plugin-lodash 2017-03-09 17:51:47 +01:00
Julien Fontanet
73e2c7e849 chore(package): use babel-plugin-dev 2017-03-09 17:50:08 +01:00
Julien Fontanet
0b0937e233 chore(base-component): remove shallow-equal dep 2017-03-09 15:22:52 +01:00
Julien Fontanet
6bf114859f chore(base-component): remove invoke dep 2017-03-09 15:20:14 +01:00
Julien Fontanet
db6d67eeb7 feat(JsonSchemaInput/EnumInput): handle enumNames 2017-03-08 18:08:59 +01:00
Julien Fontanet
a345d89aac fix(home): changing type reset paging
Fixes #1993
2017-03-06 15:47:42 +01:00
Pierre Donias
e8f8ebb112 feat(XOSAN): select suggestion, SVG graph (#1991) 2017-03-06 12:01:38 +01:00
Julien Fontanet
1dad5b5c3a 5.6.3 2017-03-02 19:07:29 +01:00
Pierre Donias
5cc5ee4e87 fix(XOSAN): XS v7.0 required to install XOSAN (#1981) 2017-03-02 17:38:55 +01:00
Julien Fontanet
e8d2b32a14 5.6.2 2017-03-01 17:09:19 +01:00
Julien Fontanet
f492909e42 fix(linting): ignored files go into /.gitignore 2017-03-01 15:19:39 +01:00
Pierre Donias
7ea17750a1 fix(pool/patches): disable patching for free plan (#1972) 2017-03-01 10:13:09 +01:00
Julien Fontanet
663e1f1a4b fix(menu): XOSAN only displayed to admins
Fixes #1968
2017-02-28 17:54:21 +01:00
Julien Fontanet
079310c67e fix(store/reducer/object): missing part of previous fix 2017-02-28 17:01:36 +01:00
Julien Fontanet
5cf7f1f886 fix(store/reducer/object): handle type change
Fixes #1967
2017-02-28 16:14:07 +01:00
Julien Fontanet
9f64af859e chore(package): update react-select to v1.0.0-rc.3 2017-02-28 10:44:04 +01:00
Julien Fontanet
007aa776cb chore(package): update index-modules to v0.3.0 2017-02-28 10:33:12 +01:00
Julien Fontanet
66bc092edd chore(package): update husky to v0.13.1 2017-02-28 10:30:44 +01:00
Julien Fontanet
140a88ee12 chore(package): update jest to v19.0.2 2017-02-28 10:29:44 +01:00
Julien Fontanet
f42758938d fix(package): migrate ghooks→husky config 2017-02-27 11:41:31 +01:00
Julien Fontanet
e19fd81536 chore: update yarn.lock 2017-02-27 11:40:28 +01:00
Julien Fontanet
73835ded96 chore(store/actions/createAction): minor optimizations 2017-02-27 11:37:56 +01:00
Julien Fontanet
1ec1a8bd94 chore(package): update superagent to version 3.5.0 (#1962)
Closes #1947

https://greenkeeper.io/
2017-02-27 11:34:57 +01:00
greenkeeper[bot]
f0b6d57ba8 chore(package): update modular-css to version 4.1.1 (#1952)
https://greenkeeper.io/
2017-02-27 11:33:12 +01:00
Julien Fontanet
f9a3ad14d1 5.6.1 2017-02-23 18:39:04 +01:00
Pierre Donias
1b86f533f7 feat(XOSAN) (#1955) 2017-02-23 18:34:20 +01:00
greenkeeper[bot]
46416fb026 chore(package): update notifyjs to version 3.0.0 (#1940)
https://greenkeeper.io/
2017-02-15 12:41:38 +01:00
Julien Fontanet
54ed37c95d chore: update yarn.lock 2017-02-14 15:21:44 +01:00
Julien Fontanet
fd79b47d9e fix(tasks): do not break on unknown host 2017-02-14 15:13:37 +01:00
greenkeeper[bot]
be8333824b chore(package): update browserify to version 14.1.0 (#1938)
https://greenkeeper.io/
2017-02-14 09:05:21 +01:00
Julien Fontanet
55daffc791 fix(pool): predicate for host to add is now dynamic (#1923) 2017-02-07 11:17:33 +01:00
Pierre Donias
375baf7fe5 feat(home): link to VM's resource set (#1920)
Fixes #1905
2017-02-01 17:01:40 +01:00
Pierre Donias
815e74c93c feat(vm): allow snapshot with ACLs (#1916)
Fixes #1865
2017-02-01 15:35:54 +01:00
Pierre Donias
547d6fbc93 feat(pool/advanced): install supplemental pack on all pool's hosts (#1910)
See #1896
2017-02-01 13:08:16 +01:00
greenkeeper[bot]
b45a4b9e6c chore(package): update chartist-plugin-legend to version 0.6.1 (#1915)
https://greenkeeper.io/
2017-02-01 09:21:28 +01:00
greenkeeper[bot]
3436d0256a chore(package): update modular-css to version 3.0.0 (#1914)
https://greenkeeper.io/
2017-01-31 08:35:26 +01:00
greenkeeper[bot]
2627cfd426 chore(package): update react-chartist to version 0.12.0 (#1912)
https://greenkeeper.io/
2017-01-30 10:53:38 +01:00
Olivier Lambert
34b18c00a1 fix(lang): button verbiage. Fixes #1911 2017-01-30 10:07:00 +01:00
Julien Fontanet
e13af7f5f0 5.6.0 2017-01-27 16:41:29 +01:00
Nicolas Raynaud
ca08613292 fix(VM import): behave if network is not an object (#1908) 2017-01-27 13:03:30 +01:00
Pierre Donias
4ab63591a0 feat(host/advanced): install a supplemental pack (#1895)
Fixes #1460
2017-01-25 16:08:45 +01:00
Pierre Donias
5b4f98b03b fix(scheduling): improve TimePicker UI (#1898)
Fixes #1893
2017-01-25 15:59:03 +01:00
Pierre Donias
f396d61633 feat(settings/logs): easy bug reporting from errors (#1907)
See #1602
2017-01-25 15:06:18 +01:00
Julien Fontanet
9f8c0c8cdf fix(scheduling): month range is 0-11 not 1-12 (#1894)
* fix(scheduling): month range is 0-11 not 1-12

* fix(scheduling): remove unused import
2017-01-19 17:46:37 +01:00
Julien Fontanet
198777ffab fix(xo): improve plugin errors 2017-01-19 16:42:38 +01:00
Pierre Donias
29c5ca1132 feat(host/advanced): display installed supplemental packs (#1890)
Fixes #1506
2017-01-16 18:03:57 +01:00
Pierre Donias
05d6f3d1ed feat(iso-device): show DVDs in ISO selector (#1889)
Fixes #1880
2017-01-16 17:06:56 +01:00
Olivier Lambert
536e82de3d feat(readme): add live chat link and icon 2017-01-16 14:54:57 +01:00
Pierre Donias
c59be7c315 fix(backup/new): smart backup pool filter (#1888)
Leaving the pool filter empty now means no filtering on pools, it should be clearer.

See #1885
2017-01-16 14:49:50 +01:00
Pierre Donias
b327bb5bd0 feat(backup/file-restore): restore multiple files (#1886)
Fixes #1877
2017-01-16 09:33:30 +01:00
Julien Fontanet
a3103587f5 chore(package): use husky instead of ghooks 2017-01-11 10:07:16 +01:00
Julien Fontanet
1bb11b574f chore: update yarn.lock 2017-01-11 10:05:18 +01:00
greenkeeper[bot]
405efe6a31 chore(package): update react-redux to version 5.0.0 (#1825)
https://greenkeeper.io/
2017-01-11 09:22:07 +01:00
Pierre Donias
73663c3703 fix(locales/fr): multiple fixes (#1876) 2017-01-10 17:42:41 +01:00
Julien Fontanet
421ee7125b chore: update yarn.lock 2017-01-09 10:12:31 +01:00
Julien Fontanet
1a6166b63c chore: update yarn.lock 2017-01-09 10:04:37 +01:00
Julien Fontanet
3828e75b7d fix(package): use bootstrap 4.0.0-alpha5 exactly
Bootstrap 4.0.0-alpha6 is not compatible, we'll migrate with beta1.

See #1871
2017-01-09 10:02:43 +01:00
greenkeeper[bot]
154da142c7 chore(package): update gulp-sourcemaps to version 2.2.3 (#1874)
https://greenkeeper.io/
2017-01-09 09:58:50 +01:00
Pierre Donias
312cd60dd1 feat(settings/servers): error handling (#1868)
Fixes #1833
2017-01-06 16:38:24 +01:00
greenkeeper[bot]
6bf522f72f chore(package): update promise-toolbox to version 0.8.0 (#1870)
https://greenkeeper.io/
2017-01-06 11:39:58 +01:00
Pierre Donias
a844f8d459 fix(backup/new): prevent negative depth values (#1869)
Fixes #1851
2017-01-06 11:12:56 +01:00
Julien Fontanet
8ee206174b fix(vm/advanced): PV args edition for all PV VMs
Currently only available if there are already PV args.
2017-01-05 16:51:56 +01:00
Olivier Lambert
1a08e24a5c feat(pool): harmonize links to hosts and storages (#1867)
Fixes #1864
2017-01-05 14:29:06 +01:00
greenkeeper[bot]
086cd0e038 chore(package): update modular-css to version 2.0.0 (#1862)
https://greenkeeper.io/
2017-01-04 09:43:32 +01:00
Julien Fontanet
42d123318c chore(pakcage): upgrade react/react-dom to v15.4.1 2017-01-03 15:28:53 +01:00
Julien Fontanet
89f160317c 5.5.3 2017-01-03 15:19:09 +01:00
Julien Fontanet
9ccd1a0362 chore: update yarn.lock 2017-01-03 15:18:27 +01:00
Pierre Donias
d116d014bc fix(host/network): PIF deletion (#1861)
Fixes #1853
2017-01-03 15:13:44 +01:00
Pierre Donias
7956cabcf4 feat(pool): combined stats tab (#1848)
Fixes #1324
2017-01-03 14:58:22 +01:00
Pierre Donias
36c61ad357 feat(vm/snapshots): icon to indicate if a snapshot is quiesce (#1860)
Fixes #1858
2017-01-03 10:54:58 +01:00
greenkeeper[bot]
25d60360d5 chore(package): update modular-css to version 1.0.0 (#1859)
https://greenkeeper.io/
2017-01-03 09:38:00 +01:00
Olivier Lambert
1e5579e3ad feat(icons): use microchip for the CPU icon 2017-01-02 17:14:05 +01:00
Julien Fontanet
77d43b2280 fix(settings/logs): correctly refresh users 2017-01-02 16:36:09 +01:00
Julien Fontanet
33e8929e8b fix(settings/groups): avoid error when adding a user 2017-01-02 16:36:02 +01:00
Julien Fontanet
b79fa9cb9f feat(backup/overview): sort by start if not end
Avoid unfinished (broken) jobs to stay at the top.
2017-01-02 16:34:36 +01:00
Olivier Lambert
a2812a85bd feat(vm): redirect to home after converting VM to template (#1857)
Fixes #1855
2017-01-02 14:12:33 +01:00
Olivier Lambert
e8ff46a8ba fix(health): add link from SR to hosts and not only for pools. Fixes #1850 (#1856) 2017-01-02 11:29:32 +01:00
Julien Fontanet
351c01d642 fix(intl/messages): typo
Fix #1849
2016-12-24 09:15:54 +01:00
Julien Fontanet
e333b1d083 chore(package): use new shorter paths for Jest serializer 2016-12-22 16:58:26 +01:00
Julien Fontanet
5ad49de642 5.5.2 2016-12-22 12:27:03 +01:00
Fabrice Marsaud
b45bb5c144 fix(xoa-updater): use the new source info (#1846) 2016-12-22 12:26:02 +01:00
Julien Fontanet
9402596f69 5.5.1 2016-12-22 11:18:32 +01:00
Fabrice Marsaud
096687ae2c fix(xoa-updates): also refresh on plan change (#1843) 2016-12-22 11:16:45 +01:00
Julien Fontanet
210b5de992 fix(backup/file-restore-modal): fetched timestamps are in seconds
Follow up to for #1840.
2016-12-20 17:10:55 +01:00
Julien Fontanet
f742fdbf1b fix(backup/file-restore): fetched timestamps are in seconds
Fixes #1840
2016-12-20 16:49:37 +01:00
Pierre Donias
e7026c522d fix(editable/XoSelect): update value before saving (#1835) 2016-12-20 13:54:22 +01:00
175 changed files with 13495 additions and 4036 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
/dist/
/node_modules/
/src/common/intl/locales/index.js
/src/common/themes/index.js
npm-debug.log
npm-debug.log.*

View File

@@ -1,5 +1,113 @@
# ChangeLog
## **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.
@@ -177,7 +285,7 @@ File level restore.
- 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)

View File

@@ -1,4 +1,4 @@
# Xen Orchestra Web [![Build Status](https://travis-ci.org/vatesfr/xo-web.png?branch=master)](https://travis-ci.org/vatesfr/xo-web)
# Xen Orchestra Web [![Chat with us](https://storage.crisp.im/plugins/images/936925df-f37b-4ba8-bab0-70cd2edcb0be/badge.svg)](https://go.crisp.im/chat/embed/?website_id=-JzqzzwddSV7bKGtEyAQ) [![Build Status](https://travis-ci.org/vatesfr/xo-web.png?branch=master)](https://travis-ci.org/vatesfr/xo-web)
![](http://i.imgur.com/tRffA5y.png)

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.5.0",
"version": "5.8.2",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -34,6 +34,8 @@
"ansi_up": "^1.3.0",
"asap": "^2.0.4",
"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",
@@ -47,11 +49,11 @@
"babel-runtime": "^6.6.1",
"babelify": "^7.2.0",
"benchmark": "^2.1.0",
"bootstrap": "^4.0.0-alpha.5",
"browserify": "^13.0.0",
"bootstrap": "4.0.0-alpha.5",
"browserify": "^14.1.0",
"bundle-collapser": "^1.2.1",
"chartist": "^0.10.1",
"chartist-plugin-legend": "^0.5.0",
"chartist-plugin-legend": "^0.6.1",
"chartist-plugin-tooltip": "0.0.11",
"classnames": "^2.2.3",
"cookies-js": "^1.2.2",
@@ -59,27 +61,27 @@
"dependency-check": "^2.5.1",
"enzyme": "^2.6.0",
"enzyme-to-json": "^1.4.4",
"event-to-promise": "^0.7.0",
"font-awesome": "^4.5.0",
"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-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": "^3.0.0",
"gulp-sourcemaps": "^1.9.1",
"gulp-sourcemaps": "^2.2.3",
"gulp-uglify": "^2.0.0",
"gulp-watch": "^4.3.5",
"human-format": "^0.7.0",
"index-modules": "^0.2.1",
"human-format": "^0.8.0",
"husky": "^0.13.1",
"index-modules": "^0.3.0",
"is-ip": "^1.0.0",
"jest": "^18.0.0",
"jest": "^19.0.2",
"jsonrpc-websocket-client": "^0.1.1",
"kindof": "^2.0.0",
"later": "^1.2.0",
@@ -87,32 +89,32 @@
"loose-envify": "^1.1.0",
"make-error": "^1.2.1",
"marked": "^0.3.5",
"modular-css": "^0.29.0",
"modular-css": "^4.1.1",
"moment": "^2.13.0",
"moment-timezone": "^0.5.4",
"notifyjs": "^2.0.1",
"notifyjs": "^3.0.0",
"novnc-node": "^0.5.3",
"promise-toolbox": "^0.7.0",
"promise-toolbox": "^0.8.0",
"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.11.0",
"react-chartist": "^0.12.0",
"react-copy-to-clipboard": "^4.0.2",
"react-debounce-input": "^2.4.0",
"react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2",
"react-document-title": "^2.0.2",
"react-dom": "^15.0.0",
"react-dom": "^15.4.1",
"react-dropzone": "^3.5.0",
"react-intl": "^2.0.1",
"react-key-handler": "^0.3.0",
"react-notify": "^2.0.1",
"react-overlays": "^0.6.0",
"react-redux": "^4.4.0",
"react-redux": "^5.0.0",
"react-router": "^3.0.0",
"react-select": "^1.0.0-beta13",
"react-select": "^1.0.0-rc.4",
"react-shortcuts": "^1.3.1",
"react-sparklines": "^1.5.0",
"react-virtualized": "^8.0.8",
@@ -122,15 +124,18 @@
"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.3.0",
"standard": "^10.0.0",
"styled-components": "^1.4.4",
"superagent": "^3.5.0",
"tar-stream": "^1.5.2",
"uncontrollable-input": "^0.0.1",
"vinyl": "^2.0.0",
"watchify": "^3.7.0",
"xml2js": "^0.4.17",
"xo-acl-resolver": "^0.2.3",
"xo-common": "0.1.0",
"xo-common": "^0.1.1",
"xo-lib": "^0.8.0",
"xo-remote-parser": "^0.3"
},
@@ -138,6 +143,7 @@
"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": "jest --watch",
"lint": "standard",
@@ -167,6 +173,8 @@
}
},
"plugins": [
"dev",
"lodash",
"transform-decorators-legacy",
"transform-runtime"
],
@@ -176,17 +184,15 @@
"stage-0"
]
},
"config": {
"ghooks": {
"commit-msg": "npm test"
}
},
"jest": {
"snapshotSerializers": [
"<rootDir>/node_modules/enzyme-to-json/serializer"
"enzyme-to-json/serializer"
]
},
"standard": {
"globals": [
"__DEV__"
],
"ignore": [
"dist"
],

View File

@@ -1,14 +1,19 @@
exports[`test Col 1`] = `
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Col 1`] = `
<div
className="col-xs-12" />
className="col-xs-12"
/>
`;
exports[`test Container 1`] = `
exports[`Container 1`] = `
<div
className="container-fluid" />
className="container-fluid"
/>
`;
exports[`test Row 1`] = `
exports[`Row 1`] = `
<div
className=" row" />
className=" row"
/>
`;

View File

@@ -1,14 +1,9 @@
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 { map, noop } from 'lodash'
import _ from './intl'
import ActionButton from './action-button'
import ButtonGroup from './button-group'
const ActionBar = ({ actions, param }) => (
<ButtonGroup>
@@ -17,18 +12,24 @@ const ActionBar = ({ actions, param }) => (
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 {
handler,
handlerParam = param,
icon,
label,
pending,
redirectOnSuccess
} = button
return <ActionButton
key={index}
handler={handler || noop}
handlerParam={handlerParam}
icon={icon}
pending={pending}
redirectOnSuccess={redirectOnSuccess}
size='large'
tooltip={_(label)}
/>
})}
</ButtonGroup>
)

View File

@@ -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>

View File

@@ -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

View File

@@ -1,87 +0,0 @@
import React from 'react'
import { isFunction, omit } from 'lodash'
import Component from './base-component'
import getEventValue from './get-event-value'
const __DEV__ = process.env.NODE_ENV !== 'production'
// This decorator can be used on a controlled input component to make
// it able to automatically handled the uncontrolled mode.
export default options => ControlledInput => {
class AutoControlledInput extends Component {
constructor (props) {
super()
const opts = isFunction(options) ? options(props) : options
const controlled = this._controlled = 'value' in props
if (!controlled) {
this.state.value = props.defaultValue || opts && opts.defaultValue
this._onChange = event => {
let defaultPrevented = false
const { onChange } = this.props
if (onChange) {
onChange(event)
defaultPrevented = event && event.defaultPrevented
}
if (!defaultPrevented) {
this.setState({ value: getEventValue(event) })
}
}
} else if (__DEV__ && 'defaultValue' in props) {
throw new Error(`${this.constructor.name}: controlled component should not have a default value`)
}
}
get value () {
return this._controlled
? this.props.value
: this.state.value
}
set value (value) {
if (__DEV__ && this._controlled) {
throw new Error(`${this.constructor.name}: should not set value on controlled component`)
}
this.setState({ value })
}
render () {
if (this._controlled) {
return <ControlledInput {...this.props} />
}
return <ControlledInput
{...omit(this.props, 'defaultValue')}
onChange={this._onChange}
value={this.state.value}
/>
}
}
if (__DEV__) {
AutoControlledInput.prototype.componentWillReceiveProps = function (newProps) {
const { name } = this.constructor
const controlled = this._controlled
const newControlled = 'value' in newProps
if (!controlled) {
if (newControlled) {
throw new Error(`${name}: uncontrolled component should not become controlled`)
}
} else if (!newControlled) {
throw new Error(`${name}: controlled component should not become uncontrolled`)
}
if (newProps.defaultValue !== this.props.defaultValue) {
throw new Error(`${name}: default value should not change`)
}
}
}
return AutoControlledInput
}

View File

@@ -3,11 +3,9 @@ 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 getEventValue from './get-event-value'
import invoke from './invoke'
import shallowEqual from './shallow-equal'
// Should components logs every renders?
//
@@ -19,7 +17,7 @@ const cowSet = (object, path, value, depth) => {
return value
}
object = clone(object)
object = object != null ? clone(object) : {}
const prop = path[depth]
object[prop] = cowSet(object[prop], path, value, depth + 1)
return object
@@ -36,7 +34,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 +44,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 +91,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 +110,6 @@ export default class BaseComponent extends Component {
})
})
}
shouldComponentUpdate (newProps, newState) {
return !(
shallowEqual(this.props, newProps) &&
shallowEqual(this.state, newState)
)
}
}
if (VERBOSE) {

View 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
View 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 }

View File

@@ -1,6 +1,6 @@
import React from 'react'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
const CARD_STYLE = {
minHeight: '100%'

View File

@@ -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
View 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>
)
}
}

View File

@@ -1,3 +0,0 @@
.button {
border-radius: 0px;
};

View File

@@ -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>
)
}
}

View File

@@ -294,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
}
@@ -335,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
)
}

View File

@@ -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>
))

View File

@@ -1,5 +1,5 @@
import Component from 'base-component'
import propTypes from 'prop-types'
import propTypes from 'prop-types-decorator'
import React from 'react'
import ReactDropzone from 'react-dropzone'

View File

@@ -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'
@@ -394,11 +394,11 @@ const MAP_TYPE_SELECT = {
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 () {
@@ -407,7 +407,7 @@ export class XoSelect extends Editable {
}
_onChange = object =>
object && this._save()
this.setState({ value: object }, object && this._save)
_renderEdition () {
const {
@@ -431,7 +431,6 @@ export class XoSelect extends Editable {
autoFocus
disabled={saving}
onChange={this._onChange}
ref='select'
/>
</a>
}

View File

@@ -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

View File

@@ -5,13 +5,17 @@ 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 getEventValue from '../get-event-value'
import propTypes from '../prop-types-decorator'
import {
firstDefined,
formatSizeRaw,
@@ -67,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}
@@ -78,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>
}
@@ -89,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>
}
}

View File

@@ -1,10 +1,10 @@
import autoControlledInput from 'auto-controlled-input'
import uncontrollableInput from 'uncontrollable-input'
import Component from 'base-component'
import find from 'lodash/find'
import map from 'lodash/map'
import React from 'react'
import propTypes from '../prop-types'
import propTypes from '../prop-types-decorator'
import Select from './select'
@@ -20,7 +20,7 @@ import Select from './select'
required: propTypes.bool,
value: propTypes.any
})
@autoControlledInput()
@uncontrollableInput()
export default class SelectPlainObject extends Component {
componentDidMount () {
const { options, value } = this.props

View File

@@ -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'

46
src/common/form/toggle.js Normal file
View 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}
/>
)
}
}

View File

@@ -1,3 +0,0 @@
.checkbox {
display: none;
}

View File

@@ -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>
)
}
}

View File

@@ -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,

View File

@@ -1,4 +1,9 @@
const common = {
homeFilterNone: ''
}
export const VM = {
...common,
homeFilterPendingVms: 'current_operations:"" ',
homeFilterNonRunningVms: '!power_state:running ',
homeFilterHvmGuests: 'virtualizationMode:hvm ',
@@ -7,18 +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:'
}

View File

@@ -1,7 +1,7 @@
import React from 'react'
import Component from './base-component'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
import Tags from './tags'
import { createString, createProperty, toString } from './complex-matcher'

View File

@@ -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'
@@ -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 {
@@ -150,15 +159,17 @@ class HostsPatchesTable extends Component {
const { props } = this
const Container = props.container || 'div'
const Button = props.useTabButton ? TabButton : ActionButton
const Button = this.props.useTabButton
? TabButton
: ActionButton_
const Buttons = (
<Container>
<Button
btnStyle='secondary'
handler={this._refreshMissingPatches}
icon='refresh'
labelId='refreshPatches'
labelId='checkForUpdates'
/>
<Button
btnStyle='primary'

View File

@@ -1,21 +1,25 @@
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, 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}`,
fixedWidth && 'fa-fw'
)
return <i {...props} />
}
propTypes(Icon)({
fixedWidth: propTypes.bool,
icon: propTypes.string,
size: propTypes.oneOfType([
propTypes.string,
propTypes.number
])
})
export default Icon

View File

@@ -1 +0,0 @@
/index.js

View File

@@ -1172,7 +1172,7 @@ export default {
// Original text: "Reboot"
rebootHostLabel: 'Reiniciar',
// Original text: 'Reboot for applying updates'
// Original text: 'Reboot to apply updates'
rebootUpdateHostLabel: undefined,
// Original text: "Emergency mode"
@@ -1605,10 +1605,10 @@ export default {
vdiRemove: undefined,
// Original text: "Boot flag"
vdbBootableStatus: 'Etiqueta de Inicio',
vbdBootableStatus: 'Etiqueta de Inicio',
// Original text: "Status"
vdbStatus: 'Estado',
vbdStatus: 'Estado',
// Original text: "Connected"
vbdStatusConnected: 'Conectado',
@@ -1626,19 +1626,19 @@ export default {
vbdDisconnect: undefined,
// Original text: 'Bootable'
vdbBootable: undefined,
vbdBootable: undefined,
// Original text: 'Readonly'
vdbReadonly: undefined,
vbdReadonly: undefined,
// Original text: 'Create'
vdbCreate: undefined,
vbdCreate: undefined,
// Original text: 'Disk name'
vdbNamePlaceHolder: undefined,
vbdNamePlaceHolder: undefined,
// Original text: 'Size'
vdbSizePlaceHolder: undefined,
vbdSizePlaceHolder: undefined,
// Original text: 'Save'
saveBootOption: undefined,

File diff suppressed because it is too large Load Diff

View File

@@ -1172,7 +1172,7 @@ export default {
// Original text: 'Reboot'
rebootHostLabel: undefined,
// Original text: 'Reboot for applying updates'
// Original text: 'Reboot to apply updates'
rebootUpdateHostLabel: undefined,
// Original text: 'Emergency mode'
@@ -1605,10 +1605,10 @@ export default {
vdiRemove: undefined,
// Original text: 'Boot flag'
vdbBootableStatus: undefined,
vbdBootableStatus: undefined,
// Original text: 'Status'
vdbStatus: undefined,
vbdStatus: undefined,
// Original text: 'Connected'
vbdStatusConnected: undefined,
@@ -1626,19 +1626,19 @@ export default {
vbdDisconnect: undefined,
// Original text: 'Bootable'
vdbBootable: undefined,
vbdBootable: undefined,
// Original text: 'Readonly'
vdbReadonly: undefined,
vbdReadonly: undefined,
// Original text: 'Create'
vdbCreate: undefined,
vbdCreate: undefined,
// Original text: 'Disk name'
vdbNamePlaceHolder: undefined,
vbdNamePlaceHolder: undefined,
// Original text: 'Size'
vdbSizePlaceHolder: undefined,
vbdSizePlaceHolder: undefined,
// Original text: 'Save'
saveBootOption: undefined,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1172,7 +1172,7 @@ export default {
// Original text: "Reboot"
rebootHostLabel: 'Reinicializar',
// Original text: 'Reboot for applying updates'
// Original text: 'Reboot to apply updates'
rebootUpdateHostLabel: undefined,
// Original text: "Emergency mode"
@@ -1605,10 +1605,10 @@ export default {
vdiRemove: undefined,
// Original text: "Boot flag"
vdbBootableStatus: 'Indicador de inicialização',
vbdBootableStatus: 'Indicador de inicialização',
// Original text: "Status"
vdbStatus: 'Status',
vbdStatus: 'Status',
// Original text: "Connected"
vbdStatusConnected: 'Conectado',
@@ -1626,19 +1626,19 @@ export default {
vbdDisconnect: undefined,
// Original text: 'Bootable'
vdbBootable: undefined,
vbdBootable: undefined,
// Original text: 'Readonly'
vdbReadonly: undefined,
vbdReadonly: undefined,
// Original text: 'Create'
vdbCreate: undefined,
vbdCreate: undefined,
// Original text: 'Disk name'
vdbNamePlaceHolder: undefined,
vbdNamePlaceHolder: undefined,
// Original text: 'Size'
vdbSizePlaceHolder: undefined,
vbdSizePlaceHolder: undefined,
// Original text: 'Save'
saveBootOption: undefined,

View File

@@ -1203,10 +1203,10 @@ export default {
vdiVm: '虚拟机',
// Original text: "Boot flag"
vdbBootableStatus: '启动标识',
vbdBootableStatus: '启动标识',
// Original text: "Status"
vdbStatus: '状态',
vbdStatus: '状态',
// Original text: "Connected"
vbdStatusConnected: '已连接',

View File

@@ -13,6 +13,7 @@ var messages = {
editableLongClickPlaceholder: 'Long click to edit',
editableClickPlaceholder: 'Click to edit',
browseFiles: 'Browse files',
// ----- Modals -----
alertOk: 'OK',
@@ -22,6 +23,10 @@ var messages = {
// ----- Filters -----
onError: 'On error',
successful: 'Successful',
filterNoSnapshots: 'Full disks only',
filterOnlyBaseCopy: 'Base copy only',
filterOnlyRegularDisks: 'Regular disks only',
filterOnlySnapshots: 'Snapshots only',
// ----- Copiable component -----
copyToClipboard: 'Copy to clipboard',
@@ -35,7 +40,7 @@ var messages = {
homeHostPage: 'Hosts',
homePoolPage: 'Pools',
homeTemplatePage: 'Templates',
homeSrPage: 'SRs',
homeSrPage: 'Storages',
dashboardPage: 'Dashboard',
overviewDashboardPage: 'Overview',
overviewVisualizationDashboardPage: 'Visualizations',
@@ -55,6 +60,7 @@ var messages = {
settingsIpsPage: 'IPs',
settingsConfigPage: 'Config',
aboutPage: 'About',
aboutXoaPlan: 'About XO {xoaPlan}',
newMenu: 'New',
taskMenu: 'Tasks',
taskPage: 'Tasks',
@@ -62,6 +68,7 @@ var messages = {
newSrPage: 'Storage',
newServerPage: 'Server',
newImport: 'Import',
xosan: 'XOSAN',
backupOverviewPage: 'Overview',
backupNewPage: 'New',
backupRemotesPage: 'Remotes',
@@ -95,8 +102,10 @@ var messages = {
homeFetchingData: 'Fetching data…',
homeWelcome: 'Welcome on 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!',
@@ -118,6 +127,7 @@ var messages = {
homeAllHosts: 'Hosts',
homeAllTags: 'Tags',
homeNewVm: 'New VM',
homeFilterNone: 'None',
homeFilterRunningHosts: 'Running hosts',
homeFilterDisabledHosts: 'Disabled hosts',
homeFilterRunningVms: 'Running VMs',
@@ -141,6 +151,7 @@ var messages = {
homeMigrateTo: 'Migrate to…',
homeMissingPaths: 'Missing patches',
homePoolMaster: 'Master:',
homeResourceSet: 'Resource set: {resourceSet}',
highAvailability: 'High Availability',
srSharedType: 'Shared {type}',
srNotSharedType: 'Not shared {type}',
@@ -182,17 +193,23 @@ 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',
timezonePickerUseLocalTime: 'Web browser timezone',
@@ -201,7 +218,9 @@ var messages = {
backupEditNotFoundTitle: 'Cannot edit backup',
backupEditNotFoundMessage: 'Missing required info for edition',
job: 'Job',
jobId: 'Job ID',
jobModalTitle: 'Job {job}',
jobId: 'ID',
jobType: 'Type',
jobName: 'Name',
jobNamePlaceholder: 'Name of your job (forbidden: "_")',
jobStart: 'Start',
@@ -212,6 +231,8 @@ var messages = {
jobTag: 'Tag',
jobScheduling: 'Scheduling',
jobState: 'State',
jobStateEnabled: 'Enabled',
jobStateDisabled: 'Disabled',
jobTimezone: 'Timezone',
jobServerTimezone: 'Server',
runJob: 'Run job',
@@ -228,6 +249,7 @@ 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',
@@ -271,7 +293,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',
@@ -279,11 +304,14 @@ 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 *',
@@ -358,7 +386,7 @@ 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 ------
@@ -426,8 +454,13 @@ var messages = {
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',
@@ -451,6 +484,9 @@ var messages = {
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',
@@ -468,8 +504,8 @@ var messages = {
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}/{totalHosts} host{failedHosts, plural, one {} other {s}} could not be restarted.',
rebootUpdateHostLabel: 'Reboot for applying updates',
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',
@@ -497,6 +533,16 @@ var messages = {
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',
@@ -512,6 +558,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',
@@ -524,6 +571,7 @@ var messages = {
addSrDeviceButton: 'Add a storage',
srNameLabel: 'Name',
srType: 'Type',
pbdAction: 'Action',
pbdStatus: 'Status',
pbdStatusConnected: 'Connected',
pbdStatusDisconnected: 'Disconnected',
@@ -552,6 +600,7 @@ var messages = {
// ----- Pool patch tabs -----
refreshPatches: 'Refresh patches',
installPoolPatches: 'Install pool patches',
checkForUpdates: 'Check for updates',
// ----- Pool storage tabs -----
defaultSr: 'Default SR',
setAsDefaultSr: 'Set as default SR',
@@ -578,6 +627,7 @@ var messages = {
vmSettings: 'Started {ago}',
vmCurrentStatus: 'Current status:',
vmNotRunning: 'Not running',
vmHaltedSince: 'Halted {ago}',
// ----- VM general tab -----
noToolsDetected: 'No Xen tools detected',
@@ -637,18 +687,21 @@ var messages = {
vdiMigrateNoSrMessage: 'A target SR is required to migrate a VDI',
vdiForget: 'Forget',
vdiRemove: 'Remove VDI',
vdbBootableStatus: 'Boot flag',
vdbStatus: 'Status',
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',
@@ -673,6 +726,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 -----
@@ -686,6 +740,7 @@ var messages = {
snapshotDate: 'Creation date',
snapshotName: 'Name',
snapshotAction: 'Action',
snapshotQuiesce: 'Quiesced snapshot',
// ----- VM log tab -----
logRemoveAll: 'Remove all logs',
@@ -717,12 +772,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:',
@@ -764,7 +828,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',
@@ -826,7 +890,6 @@ var messages = {
newVmAddInterface: 'Add interface',
newVmDisksPanel: 'Disks',
newVmSrLabel: 'SR',
newVmBootableLabel: 'Bootable',
newVmSizeLabel: 'Size',
newVmAddDisk: 'Add disk',
newVmSummaryPanel: 'Summary',
@@ -844,7 +907,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:',
@@ -852,9 +915,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',
@@ -923,6 +988,7 @@ var messages = {
delta: 'delta',
restoreBackups: 'Restore Backups',
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',
@@ -948,10 +1014,15 @@ var messages = {
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',
@@ -959,25 +1030,25 @@ 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}}?',
startVmsModalMessage: 'Are you sure you want to start {vms, number} VM{vms, plural, one {} other {s}}?',
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',
@@ -1019,16 +1090,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',
@@ -1072,11 +1159,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',
@@ -1088,7 +1175,7 @@ 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!',
@@ -1148,6 +1235,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',
@@ -1193,6 +1284,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',
@@ -1206,6 +1300,7 @@ var messages = {
ipsVifs: 'VIFs',
ipsNotUsed: 'Not used',
ipPoolUnknownVif: 'unknown VIF',
ipPoolNameAlreadyExists: 'Name already exists',
// ----- Shortcuts -----
shortcutModalTitle: 'Keyboard shortcuts',
@@ -1255,7 +1350,45 @@ var messages = {
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'
srAllConnected: 'Connected',
// ----- XOSAN -----
xosanTitle: 'XOSAN',
xosanSrTitle: 'Xen Orchestra SAN SR',
xosanAvailableSrsTitle: 'Select local SRs (lvm)',
xosanSuggestions: 'Suggestions',
xosanName: 'Name',
xosanHost: 'Host',
xosanHosts: 'Hosts',
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',
xosanInstalling: 'Installing XOSAN. Please wait...',
xosanCommunity: 'No XOSAN available for Community Edition',
// 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:'
}
forEach(messages, function (message, id) {

View File

@@ -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')

View File

@@ -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,8 +74,10 @@ 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'>
@@ -71,12 +88,24 @@ export default class IsoDevice extends Component {
/>
<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>
)
}

View File

@@ -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
}
}

View File

@@ -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,175 +14,110 @@ import {
forceDisplayOptionalAttr
} from './helpers'
// ===================================================================
class ArrayItem extends Component {
get value () {
return this.refs.input.value
}
set value (value) {
this.setState({
use: true
}, () => {
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>
)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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.

View File

@@ -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
}
}

View File

@@ -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'
/>

View File

@@ -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'
/>

View File

@@ -1,167 +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 = {}) {
this.setState({
use: true
}, () => {
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>
)
}

View File

@@ -1,8 +1,10 @@
import React from 'react'
import AbstractInput from './abstract-input'
import uncontrollableInput from 'uncontrollable-input'
import Combobox from '../combobox'
import propTypes from '../prop-types'
import Component from '../base-component'
import propTypes from '../prop-types-decorator'
import { PrimitiveInputWrapper } from './helpers'
// ===================================================================
@@ -10,22 +12,29 @@ import { PrimitiveInputWrapper } from './helpers'
@propTypes({
password: propTypes.bool
})
export default class StringInput extends AbstractInput {
@uncontrollableInput()
export default class StringInput extends Component {
render () {
const { props } = this
const { schema } = props
const { required, schema } = this.props
const {
disabled,
onChange,
password,
placeholder = schema.default,
value,
...props
} = this.props
return (
<PrimitiveInputWrapper {...props}>
<Combobox
defaultValue={props.defaultValue}
disabled={props.disabled}
onChange={props.onChange}
value={value || ''}
disabled={disabled}
onChange={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>
)

View File

@@ -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'
// ===================================================================

View File

@@ -1,11 +1,12 @@
import _ from 'intl'
import Icon from 'icon'
import isArray from 'lodash/isArray'
import isString from 'lodash/isString'
import React, { Component, cloneElement } from 'react'
import { Button, Modal as ReactModal } from 'react-bootstrap-4/lib'
import { 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 {
disable as disableShortcuts,
enable as enableShortcuts
@@ -75,8 +76,6 @@ class Confirm extends Component {
instance.close()
}
_style = { marginRight: '0.5em' }
render () {
const { Body, Footer, Header, Title } = ReactModal
const { title, icon } = this.props
@@ -97,14 +96,14 @@ class Confirm extends Component {
</Body>
<Footer>
<Button
bsStyle='primary'
btnStyle='primary'
onClick={this._resolve}
style={this._style}
>
{_('confirmOk')}
</Button>
{' '}
<Button
bsStyle='secondary'
onClick={this._reject}
>
{_('confirmCancel')}

View 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 }

View File

@@ -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 }

View File

@@ -8,7 +8,7 @@ import {
} 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))
@@ -77,6 +77,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)

View File

@@ -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' /> {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`)

View File

@@ -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>

View 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>
}
}

View File

@@ -1,9 +1,5 @@
import React from 'react'
import classNames from 'classnames'
import Icon from 'icon'
import store from 'store'
import Tooltip from 'tooltip'
import { Button } from 'react-bootstrap-4/lib'
import { parse as parseRemote } from 'xo-remote-parser'
import {
assign,
@@ -26,10 +22,14 @@ import {
} from 'lodash'
import _ from './intl'
import autoControlledInput from './auto-controlled-input'
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,
@@ -42,7 +42,6 @@ import {
import {
addSubscriptions,
connectStore,
mapPlus,
resolveResourceSets
} from './utils'
import {
@@ -135,37 +134,6 @@ const options = props => ({
]).isRequired
})
export class GenericSelect extends Component {
componentDidUpdate (prevProps) {
const { onChange, xoObjects } = this.props
if (!onChange || prevProps.xoObjects === xoObjects) {
return
}
const ids = this._getSelectValue()
const objectsById = this._getObjectsById()
if (!isArray(ids)) {
ids && !objectsById[ids] && onChange(undefined)
} else {
let shouldTriggerOnChange
const newValue = isArray(ids) && mapPlus(ids, (id, push) => {
const object = objectsById[id]
if (object) {
push(object)
} else {
shouldTriggerOnChange = true
}
})
if (shouldTriggerOnChange) {
this.props.onChange(newValue)
}
}
}
_getObjectsById = createSelector(
() => this.props.xoObjects,
objects => keyBy(
@@ -180,22 +148,19 @@ export class GenericSelect extends Component {
() => this.props.xoContainers,
() => this.props.xoObjects,
(containers, objects) => { // createCollectionWrapper with a depth?
const __DEV__ = process.env.NODE_ENV !== 'production'
const { name } = this.constructor
let options = []
if (!containers) {
if (__DEV__ && !isArray(objects)) {
throw new Error(`${name}: without xoContainers, xoObjects must be an array`)
}
return map(objects, getOption)
}
if (__DEV__ && isArray(objects)) {
options = map(objects, getOption)
} else if (__DEV__ && isArray(objects)) {
throw new Error(`${name}: with xoContainers, xoObjects must be an object`)
}
const options = []
forEach(containers, container => {
options.push({
disabled: true,
@@ -206,6 +171,30 @@ export class GenericSelect extends Component {
options.push(getOption(object, container))
})
})
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
}
})
}
}
if (isArray(values)) {
forEach(values, addIfMissing)
} else {
addIfMissing(values)
}
return options
}
)
@@ -289,7 +278,7 @@ export class GenericSelect extends Component {
{select}
<span className='input-group-btn'>
<Tooltip content={_('selectAll')}>
<Button type='button' bsStyle='secondary' onClick={this._selectAll} style={ADDON_BUTTON_STYLE}>
<Button onClick={this._selectAll} style={ADDON_BUTTON_STYLE}>
<Icon icon='add' />
</Button>
</Tooltip>
@@ -298,7 +287,7 @@ export class GenericSelect extends Component {
}
}
const makeStoreSelect = (createSelectors, defaultProps) => autoControlledInput(options)(
const makeStoreSelect = (createSelectors, defaultProps) => uncontrollableInput(options)(
connectStore(createSelectors)(
props =>
<GenericSelect
@@ -308,7 +297,7 @@ const makeStoreSelect = (createSelectors, defaultProps) => autoControlledInput(o
)
)
const makeSubscriptionSelect = (subscribe, props) => autoControlledInput(options)(
const makeSubscriptionSelect = (subscribe, props) => uncontrollableInput(options)(
class extends Component {
constructor (props) {
super(props)

View File

@@ -222,6 +222,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,
@@ -317,7 +347,7 @@ export const createSortForType = invoke(() => {
return (type, collection) => createSort(
collection,
autoSelector(type, getIteratees),
autoSelector(type, getOrders),
autoSelector(type, getOrders)
)
})
@@ -418,6 +448,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(

View File

@@ -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' }

View File

@@ -12,9 +12,10 @@ import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabric
import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
import { Portal } from 'react-overlays'
import Button from '../button'
import Component from '../base-component'
import Icon from '../icon'
import propTypes from '../prop-types'
import propTypes from '../prop-types-decorator'
import SingleLineRow from '../single-line-row'
import { BlockLink } from '../link'
import { Container, Col } from '../grid'
@@ -80,9 +81,9 @@ class TableFilter extends Component {
className='form-control'
/>
<div className='input-group-btn'>
<button className='btn btn-secondary' onClick={this._cleanFilter}>
<Button onClick={this._cleanFilter}>
<Icon icon='clear-search' />
</button>
</Button>
</div>
</div>
)
@@ -93,7 +94,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 +105,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 +116,7 @@ class ColumnHead extends Component {
return (
<th
className={classNames(
textAlign && `text-xs-${textAlign}`,
styles.clickableColumn,
isSelected && classNames('text-white', 'bg-info')
)}
@@ -141,13 +143,14 @@ const DEFAULT_ITEMS_PER_PAGE = 10
]).isRequired,
columns: propTypes.arrayOf(propTypes.shape({
default: propTypes.bool,
name: propTypes.node.isRequired,
name: propTypes.node,
itemRenderer: propTypes.func.isRequired,
sortCriteria: propTypes.oneOfType([
propTypes.func,
propTypes.string
]),
sortOrder: propTypes.string
sortOrder: propTypes.string,
textAlign: propTypes.string
})).isRequired,
filterContainer: propTypes.func,
filters: propTypes.object,
@@ -302,7 +305,9 @@ export default class SortedTable extends Component {
<tr>
{map(props.columns, (column, key) => (
<ColumnHead
textAlign={column.textAlign}
columnId={key}
key={key}
name={column.name}
sort={column.sortCriteria && this._sort}
@@ -314,7 +319,7 @@ export default class SortedTable extends Component {
<tbody>
{map(this._getVisibleItems(), (item, i) => {
const columns = map(props.columns, (column, key) => (
<td key={key}>
<td key={key} className={column.textAlign && `text-xs-${column.textAlign}`}>
{column.itemRenderer(item, userData)}
</td>
))

View File

@@ -0,0 +1,38 @@
import React from 'react'
import styled from 'styled-components'
import ActionButton from './action-button'
import propTypes from './prop-types-decorator'
const Button = styled(ActionButton)`
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,
disabledLabel,
disabledTooltip,
enabledLabel,
enabledTooltip,
enabledHandler,
state,
...props
}) =>
<Button
handler={state ? enabledHandler : disabledHandler}
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)

View File

@@ -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 }
)
})()
// ===================================================================

View File

@@ -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]
}
}

View File

@@ -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',

View File

View File

@@ -0,0 +1,6 @@
export default {
disabledStateBg: '#fff',
disabledStateColor: '#c0392b',
enabledStateBg: '#fff',
enabledStateColor: '#27ae60'
}

View File

@@ -5,7 +5,7 @@ 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'
@@ -56,7 +56,7 @@ export default class TimezonePicker extends Component {
}
this.setState({
timezone: option && option.value || SERVER_TIMEZONE_TAG
timezone: (option != null && option.value) || SERVER_TIMEZONE_TAG
}, () =>
this.props.onChange(this.state.timezone === SERVER_TIMEZONE_TAG ? null : this.state.timezone)
)
@@ -81,7 +81,6 @@ export default class TimezonePicker extends Component {
/>
<div className='pull-right'>
<ActionButton
btnStyle='secondary'
handler={this._useLocalTime}
icon='time'
>

View File

@@ -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}
}

View File

@@ -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 {

View File

@@ -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'

View File

@@ -15,6 +15,7 @@ import mapValues from 'lodash/mapValues'
import React from 'react'
import ReadableStream from 'readable-stream'
import replace from 'lodash/replace'
import startsWith from 'lodash/startsWith'
import { connect } from 'react-redux'
import _ from './intl'
@@ -63,13 +64,22 @@ export const addSubscriptions = subscriptions => Component => {
componentWillMount () {
this._unsubscribes = map(isFunction(subscriptions) ? subscriptions() : subscriptions, (subscribe, prop) =>
subscribe(value => this.setState({ [prop]: value }))
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 () {
@@ -479,3 +489,69 @@ export const resolveIds = params => {
}
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
}

View File

@@ -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 }

View File

@@ -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)
)
}
}

View File

@@ -16,10 +16,10 @@ export default class HighLevelObjectInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class HostInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class PoolInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class RemoteInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class RoleInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class SrInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class SubjectInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class TagInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class VmInput extends XoAbstractInput {
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>
)

View File

@@ -1,4 +1,4 @@
.dashedLine {
stroke: black;
stroke-dasharray: 4px 5px;
stroke: black;
stroke-dasharray: 4px 2px;
}

View File

@@ -1,14 +1,21 @@
import ChartistGraph from 'react-chartist'
import ChartistLegend from 'chartist-plugin-legend'
import ChartistTooltip from 'chartist-plugin-tooltip'
import map from 'lodash/map'
import React from 'react'
import size from 'lodash/size'
import values from 'lodash/values'
import { injectIntl } from 'react-intl'
import find from 'lodash/find'
import { messages } from 'intl'
import {
find,
flatten,
floor,
map,
max,
size,
sum,
values
} from 'lodash'
import propTypes from '../prop-types'
import propTypes from '../prop-types-decorator'
import { computeArraysSum } from '../xo-stats'
import { formatSize } from '../utils'
@@ -53,7 +60,7 @@ const makeOptions = ({ intl, nValues, endTimestamp, interval, valueTransform })
// ===================================================================
const makeLabelInterpolationFnc = (intl, nValues, endTimestamp, interval) => {
const labelSpace = Math.floor(nValues / N_LABELS_X)
const labelSpace = floor(nValues / N_LABELS_X)
let format
if (interval === 3600) {
@@ -150,7 +157,7 @@ export const CpuLineChart = injectIntl(propTypes({
nValues: length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: value => `${value}%`
valueTransform: value => `${floor(value)}%`
}),
high: !addSumSeries ? 100 : stats.length * 100,
...options
@@ -159,6 +166,54 @@ export const CpuLineChart = injectIntl(propTypes({
)
}))
export const PoolCpuLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const firstHostData = data[0]
const length = getStatsLength(firstHostData.stats.cpus)
if (!length) {
return templateError
}
const series = map(data, ({ host, stats }) => ({
name: host,
data: computeArraysSum(stats.cpus)
}))
if (addSumSeries) {
series.push({
name: intl.formatMessage(messages.poolAllHosts),
data: computeArraysSum(map(series, 'data')),
className: styles.dashedLine
})
}
const nbCpusByHost = map(data, ({ stats }) => stats.cpus.length)
return (
<ChartistGraph
type='Line'
data={{
series
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: firstHostData.endTimestamp,
interval: firstHostData.interval,
valueTransform: value => `${floor(value)}%`
}),
high: 100 * (addSumSeries ? sum(nbCpusByHost) : max(nbCpusByHost)),
...options
}}
/>
)
}))
export const MemoryLineChart = injectIntl(propTypes({
data: propTypes.object.isRequired,
options: propTypes.object
@@ -196,6 +251,57 @@ export const MemoryLineChart = injectIntl(propTypes({
)
}))
export const PoolMemoryLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const firstHostData = data[0]
const {
memory,
memoryUsed
} = firstHostData.stats
if (!memory || !memoryUsed) {
return templateError
}
const series = map(data, ({ host, stats }) => ({
name: host,
data: stats.memoryUsed
}))
if (addSumSeries) {
series.push({
name: intl.formatMessage(messages.poolAllHosts),
data: computeArraysSum(map(data, 'stats.memoryUsed')),
className: styles.dashedLine
})
}
const currentMemoryByHost = map(data, ({ stats }) => stats.memory[stats.memory.length - 1])
return (
<ChartistGraph
type='Line'
data={{
series
}}
options={{
...makeOptions({
intl,
nValues: firstHostData.stats.memoryUsed.length,
endTimestamp: firstHostData.endTimestamp,
interval: firstHostData.interval,
valueTransform: formatSize
}),
high: addSumSeries ? sum(currentMemoryByHost) : max(currentMemoryByHost),
...options
}}
/>
)
}))
export const XvdLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
@@ -292,6 +398,51 @@ export const PifLineChart = injectIntl(propTypes({
)
}))
const ios = ['rx', 'tx']
export const PoolPifLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const firstHostData = data[0]
const length = firstHostData.stats && getStatsLength(firstHostData.stats.pifs.rx)
if (!length) {
return templateError
}
const series = addSumSeries
? map(ios, io => ({
name: `${intl.formatMessage(messages.poolAllHosts)} (${io})`,
data: computeArraysSum(map(data, ({ stats }) => computeArraysSum(stats.pifs[io])))
}))
: flatten(map(data, ({ stats, host }) =>
map(ios, io => ({
name: `${host} (${io})`,
data: computeArraysSum(stats.pifs[io])
}))
))
return (
<ChartistGraph
type='Line'
data={{
series
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: firstHostData.endTimestamp,
interval: firstHostData.interval,
valueTransform: formatSize
}),
...options
}}
/>
)
}))
export const LoadLineChart = injectIntl(propTypes({
data: propTypes.object.isRequired,
options: propTypes.object
@@ -325,3 +476,48 @@ export const LoadLineChart = injectIntl(propTypes({
/>
)
}))
export const PoolLoadLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const firstHostData = data[0]
const length = firstHostData.stats && firstHostData.stats.load.length
if (!length) {
return templateError
}
const series = map(data, ({ host, stats }) => ({
name: host,
data: stats.load
}))
if (addSumSeries) {
series.push({
name: intl.formatMessage(messages.poolAllHosts),
data: computeArraysSum(map(data, 'stats.load')),
className: styles.dashedLine
})
}
return (
<ChartistGraph
type='Line'
data={{
series
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: firstHostData.endTimestamp,
interval: firstHostData.interval,
valueTransform: value => `${value.toPrecision(3)}`
}),
...options
}}
/>
)
}))

View File

@@ -6,7 +6,7 @@ import map from 'lodash/map'
import times from 'lodash/times'
import Component from './base-component'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
import { setStyles } from './d3-utils'
// ===================================================================

View File

@@ -4,7 +4,7 @@ import {
SparklinesLine
} from 'react-sparklines'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
import {
computeArraysAvg,
computeObjectsAvg

View File

@@ -5,7 +5,7 @@ import map from 'lodash/map'
import Component from '../base-component'
import _ from '../intl'
import propTypes from '../prop-types'
import propTypes from '../prop-types-decorator'
import { Toggle } from '../form'
import { setStyles } from '../d3-utils'
import {
@@ -381,7 +381,7 @@ export default class XoWeekCharts extends Component {
<p className='mt-1'>
{_('weeklyChartsScaleInfo')}
{' '}
<Toggle iconSize={1} icon='scale' className='btn btn-secondary' onChange={this._updateScale} />
<Toggle iconSize={1} icon='scale' onChange={this._updateScale} />
</p>
</div>
<div

View File

@@ -13,7 +13,7 @@ import { FormattedTime } from 'react-intl'
import _ from '../intl'
import Component from '../base-component'
import propTypes from '../prop-types'
import propTypes from '../prop-types-decorator'
import Tooltip from '../tooltip'
import styles from './index.css'

View File

@@ -1,24 +1,46 @@
import _ from 'intl'
import BaseComponent from 'base-component'
import every from 'lodash/every'
import React from 'react'
import SingleLineRow from 'single-line-row'
import { SelectHost } from 'select-objects'
import { Col } from 'grid'
import { connectStore } from 'utils'
import { createGetObjectsOfType } from 'selectors'
import { createCollectionWrapper, createGetObjectsOfType, createSelector } from 'selectors'
import { forEach } from 'lodash'
import { SelectHost } from 'select-objects'
@connectStore(() => ({
hosts: createGetObjectsOfType('host')
singleHosts: createSelector(
(_, { pool }) => pool && pool.id,
createGetObjectsOfType('host'),
createCollectionWrapper((poolId, hosts) => {
const visitedPools = {}
const singleHosts = {}
forEach(hosts, host => {
const { $pool } = host
if ($pool !== poolId) {
const previousHost = visitedPools[$pool]
if (previousHost) {
delete singleHosts[previousHost]
} else {
const { id } = host
singleHosts[id] = true
visitedPools[$pool] = id
}
}
})
return singleHosts
})
)
}), { withRef: true })
export default class AddHostModal extends BaseComponent {
get value () {
return this.state
}
_hostPredicate = host =>
host.$pool !== this.props.pool.id &&
every(this.props.hosts, h => h.$pool !== host.$pool || h.id === host.id)
_getHostPredicate = createSelector(
() => this.props.singleHosts,
singleHosts => host => singleHosts[host.id]
)
render () {
return <div>
@@ -27,7 +49,7 @@ export default class AddHostModal extends BaseComponent {
<Col size={6}>
<SelectHost
onChange={this.linkState('host')}
predicate={this._hostPredicate}
predicate={this._getHostPredicate()}
value={this.state.host}
/>
</Col>

View File

@@ -5,7 +5,7 @@ import * as FormGrid from '../../form-grid'
import _ from '../../intl'
import Combobox from '../../combobox'
import Component from '../../base-component'
import propTypes from '../../prop-types'
import propTypes from '../../prop-types-decorator'
import { createSelector } from '../../selectors'
@propTypes({

View File

@@ -15,8 +15,8 @@ import sortBy from 'lodash/sortBy'
import throttle from 'lodash/throttle'
import Xo from 'xo-lib'
import { createBackoff } from 'jsonrpc-websocket-client'
import { lastly, reflect } from 'promise-toolbox'
import { noHostsAvailable } from 'xo-common/api-errors'
import { reflect } from 'promise-toolbox'
import { resolve } from 'url'
import _ from '../intl'
@@ -41,6 +41,10 @@ export const XEN_DEFAULT_CPU_CAP = 0
// ===================================================================
export const XEN_VIDEORAM_VALUES = [1, 2, 4, 8, 16]
// ===================================================================
export const isSrWritable = sr => sr && sr.content_type !== 'iso' && sr.size > 0
export const isSrShared = sr => sr && sr.$PBDs.length > 1
export const isVmRunning = vm => vm && vm.power_state === 'Running'
@@ -128,6 +132,13 @@ export const connectStore = store => {
sendUpdates()
})
subscribePermissions(permissions => store.dispatch(updatePermissions(permissions)))
// work around to keep the user in Redux store up to date
//
// FIXME: store subscriptions data directly in Redux
subscribeUsers(user => {
store.dispatch(signedIn(xo.user))
})
}
// -------------------------------------------------------------------
@@ -216,9 +227,9 @@ export const subscribeAcls = createSubscription(() => _call('acl.get'))
export const subscribeJobs = createSubscription(() => _call('job.getAll'))
export const subscribeJobsLogs = createSubscription(() => _call('log.get', {namespace: 'jobs'}))
export const subscribeJobsLogs = createSubscription(() => _call('log.get', { namespace: 'jobs' }))
export const subscribeApiLogs = createSubscription(() => _call('log.get', {namespace: 'api'}))
export const subscribeApiLogs = createSubscription(() => _call('log.get', { namespace: 'api' }))
export const subscribePermissions = createSubscription(() => _call('acl.getCurrentPermissions'))
@@ -260,6 +271,19 @@ export const subscribeRoles = createSubscription(invoke(
export const subscribeIpPools = createSubscription(() => _call('ipPool.getAll'))
export const subscribeResourceCatalog = createSubscription(() => _call('cloud.getResourceCatalog'))
const xosanSubscriptions = {}
export const subscribeIsInstallingXosan = (pool, cb) => {
const poolId = resolveId(pool)
if (!xosanSubscriptions[poolId]) {
xosanSubscriptions[poolId] = createSubscription(() => _call('xosan.checkSrIsBusy', { poolId }))
}
return xosanSubscriptions[poolId](cb)
}
// System ============================================================
export const apiMethods = _call('system.getMethodsInfo')
@@ -286,20 +310,20 @@ export const exportConfig = () => (
// Server ------------------------------------------------------------
export const addServer = (host, username, password) => (
_call('server.add', { host, username, password })::tap(
export const addServer = (host, username, password, label) => (
_call('server.add', { host, label, password, username })::tap(
subscribeServers.forceRefresh
)
)::rethrow(() => error(_('serverError'), _('serverAddFailed')))
)
export const editServer = (server, { host, username, password, readOnly }) => (
_call('server.set', { id: resolveId(server), host, username, password, readOnly })::tap(
export const editServer = (server, props) => (
_call('server.set', { ...props, id: resolveId(server) })::tap(
subscribeServers.forceRefresh
)
)
export const connectServer = server => (
_call('server.connect', { id: resolveId(server) })::tap(
_call('server.connect', { id: resolveId(server) })::lastly(
subscribeServers.forceRefresh
)
)
@@ -322,7 +346,7 @@ export const editPool = (pool, props) => (
_call('pool.set', { id: resolveId(pool), ...props })
)
import AddHostModalBody from './add-host-modal'
import AddHostModalBody from './add-host-modal' // eslint-disable-line import/first
export const addHostToPool = (pool, host) => {
if (host) {
return confirm({
@@ -352,14 +376,14 @@ export const detachHost = host => (
confirm({
icon: 'host-eject',
title: _('detachHostModalTitle'),
body: _('detachHostModalMessage', {host: <strong>{host.name_label}</strong>})
body: _('detachHostModalMessage', { host: <strong>{host.name_label}</strong> })
}).then(
() => _call('host.detach', { host: host.id })
)
)
export const setDefaultSr = sr => (
_call('pool.setDefaultSr', {sr: resolveId(sr)})
_call('pool.setDefaultSr', { sr: resolveId(sr) })
)
// Host --------------------------------------------------------------
@@ -416,7 +440,7 @@ export const restartHostsAgents = hosts => {
title: _('restartHostsAgentsModalTitle', { nHosts }),
body: _('restartHostsAgentsModalMessage', { nHosts })
}).then(
() => map(hosts, restartHostAgent),
() => Promise.all(map(hosts, restartHostAgent)),
noop
)
}
@@ -485,6 +509,44 @@ export const installAllPatchesOnPool = pool => (
_call('pool.installAllPatches', { pool: resolveId(pool) })
)
export const installSupplementalPack = (host, file) => {
info(_('supplementalPackInstallStartedTitle'), _('supplementalPackInstallStartedMessage'))
return _call('host.installSupplementalPack', { host: resolveId(host) }).then(({ $sendTo: url }) => (
request.post(url)
.send(file)
.then(res => {
if (res.status !== 200) {
throw new Error('installing supplemental pack failed')
}
success(_('supplementalPackInstallSuccessTitle'), _('supplementalPackInstallSuccessMessage'))
}).catch(err => {
error(_('supplementalPackInstallErrorTitle'), _('supplementalPackInstallErrorMessage'))
throw err
})
))
}
export const installSupplementalPackOnAllHosts = (pool, file) => {
info(_('supplementalPackInstallStartedTitle'), _('supplementalPackInstallStartedMessage'))
return _call('pool.installSupplementalPack', { pool: resolveId(pool) }).then(({ $sendTo: url }) => (
request.post(url)
.send(file)
.then(res => {
if (res.status !== 200) {
throw new Error('installing supplemental pack failed')
}
success(_('supplementalPackInstallSuccessTitle'), _('supplementalPackInstallSuccessMessage'))
}).catch(err => {
error(_('supplementalPackInstallErrorTitle'), _('supplementalPackInstallErrorMessage'))
throw err
})
))
}
// Containers --------------------------------------------------------
export const pauseContainer = (vm, container) => (
@@ -585,7 +647,7 @@ export const cloneVm = ({ id, name_label: nameLabel }, fullCopy = false) => (
})
)
import CopyVmModalBody from './copy-vm-modal'
import CopyVmModalBody from './copy-vm-modal' // eslint-disable-line import/first
export const copyVm = (vm, sr, name, compress) => {
if (sr) {
return confirm({
@@ -609,7 +671,7 @@ export const copyVm = (vm, sr, name, compress) => {
}
}
import CopyVmsModalBody from './copy-vms-modal'
import CopyVmsModalBody from './copy-vms-modal' // eslint-disable-line import/first
export const copyVms = vms => {
const _vms = resolveIds(vms)
return confirm({
@@ -627,7 +689,7 @@ export const copyVms = vms => {
sr
} = params
Promise.all(map(_vms, (vm, index) =>
_call('vm.copy', { vm, sr, compress, name: names[index] }),
_call('vm.copy', { vm, sr, compress, name: names[index] })
))
},
noop
@@ -681,7 +743,7 @@ export const deleteSnapshot = vm => (
)
)
import MigrateVmModalBody from './migrate-vm-modal'
import MigrateVmModalBody from './migrate-vm-modal' // eslint-disable-line import/first
export const migrateVm = (vm, host) => (
confirm({
title: _('migrateVmModalTitle'),
@@ -697,7 +759,7 @@ export const migrateVm = (vm, host) => (
)
)
import MigrateVmsModalBody from './migrate-vms-modal'
import MigrateVmsModalBody from './migrate-vms-modal' // eslint-disable-line import/first
export const migrateVms = vms => (
confirm({
title: _('migrateVmModalTitle'),
@@ -739,7 +801,7 @@ export const createVm = args => (
export const createVms = (args, nameLabels) => (
confirm({
title: _('newVmCreateVms'),
body: _('newVmCreateVmsConfirm', {nbVms: nameLabels.length})
body: _('newVmCreateVmsConfirm', { nbVms: nameLabels.length })
}).then(
() => Promise.all(map(nameLabels, name_label => // eslint-disable-line camelcase
_call('vm.create', { ...args, name_label })
@@ -772,15 +834,15 @@ export const deleteVms = vms => (
)
)
export const importBackup = ({remote, file, sr}) => (
_call('vm.importBackup', resolveIds({remote, file, sr}))
export const importBackup = ({ remote, file, sr }) => (
_call('vm.importBackup', resolveIds({ remote, file, sr }))
)
export const importDeltaBackup = ({remote, file, sr}) => (
_call('vm.importDeltaBackup', resolveIds({remote, filePath: file, sr}))
export const importDeltaBackup = ({ remote, file, sr }) => (
_call('vm.importDeltaBackup', resolveIds({ remote, filePath: file, sr }))
)
import RevertSnapshotModalBody from './revert-snapshot-modal'
import RevertSnapshotModalBody from './revert-snapshot-modal' // eslint-disable-line import/first
export const revertSnapshot = vm => (
confirm({
title: _('revertVmModalTitle'),
@@ -849,11 +911,11 @@ export const setVmBootOrder = (vm, order) => (
})
)
export const attachDiskToVm = (vdi, vm, {bootable, mode, position}) => (
export const attachDiskToVm = (vdi, vm, { bootable, mode, position }) => (
_call('vm.attachDisk', {
bootable,
mode,
position: position && String(position) || undefined,
position: (position && String(position)) || undefined,
vdi: resolveId(vdi),
vm: resolveId(vm)
})
@@ -902,7 +964,7 @@ export const migrateVdi = (vdi, sr) => (
_call('vdi.migrate', { id: resolveId(vdi), sr_id: resolveId(sr) })
)
// VDB ---------------------------------------------------------------
// VBD ---------------------------------------------------------------
export const connectVbd = vbd => (
_call('vbd.connect', { id: resolveId(vbd) })
@@ -927,7 +989,7 @@ export const setBootableVbd = (vbd, bootable) => (
// VIF ---------------------------------------------------------------
export const createVmInterface = (vm, network, mac) => (
_call('vm.createInterface', resolveIds({vm, network, mac}))
_call('vm.createInterface', resolveIds({ vm, network, mac }))
)
export const connectVif = vif => (
@@ -952,7 +1014,7 @@ export const editNetwork = (network, props) => (
_call('network.set', { ...props, id: resolveId(network) })
)
import CreateNetworkModalBody from './create-network-modal'
import CreateNetworkModalBody from './create-network-modal' // eslint-disable-line import/first
export const createNetwork = container => (
confirm({
icon: 'network',
@@ -972,7 +1034,7 @@ export const createNetwork = container => (
export const getBondModes = () =>
_call('network.getBondModes')
import CreateBondedNetworkModalBody from './create-bonded-network-modal'
import CreateBondedNetworkModalBody from './create-bonded-network-modal' // eslint-disable-line import/first
export const createBondedNetwork = container => (
confirm({
icon: 'network',
@@ -1263,7 +1325,7 @@ export const loadPlugin = async id => (
_call('plugin.load', { id })::tap(
subscribePlugins.forceRefresh
)::rethrow(
err => error(_('pluginError'), JSON.stringify(err.data) || _('unknownPluginError'))
err => error(_('pluginError'), (err && err.message) || _('unknownPluginError'))
)
)
@@ -1271,7 +1333,7 @@ export const unloadPlugin = id => (
_call('plugin.unload', { id })::tap(
subscribePlugins.forceRefresh
)::rethrow(
err => error(_('pluginError'), JSON.stringify(err.data) || _('unknownPluginError'))
err => error(_('pluginError'), (err && err.message) || _('unknownPluginError'))
)
)
@@ -1287,7 +1349,7 @@ export const disablePluginAutoload = id => (
)
)
export const configurePlugin = (id, configuration) => {
export const configurePlugin = (id, configuration) =>
_call('plugin.configure', { id, configuration })::tap(
() => {
info(_('pluginConfigurationSuccess'), _('pluginConfigurationChanges'))
@@ -1296,7 +1358,6 @@ export const configurePlugin = (id, configuration) => {
)::rethrow(
err => error(_('pluginError'), JSON.stringify(err.data) || _('unknownPluginError'))
)
}
export const purgePluginConfiguration = async id => {
await confirm({
@@ -1342,43 +1403,43 @@ export const recomputeResourceSetsLimits = () => (
// Remote ------------------------------------------------------------
export const getRemote = remote => (
_call('remote.get', resolveIds({id: remote}))::rethrow(
_call('remote.get', resolveIds({ id: remote }))::rethrow(
err => error(_('getRemote'), err.message || String(err))
)
)
export const createRemote = (name, url) => (
_call('remote.create', {name, url})::tap(
_call('remote.create', { name, url })::tap(
subscribeRemotes.forceRefresh
)
)
export const deleteRemote = remote => (
_call('remote.delete', {id: resolveId(remote)})::tap(
_call('remote.delete', { id: resolveId(remote) })::tap(
subscribeRemotes.forceRefresh
)
)
export const enableRemote = remote => (
_call('remote.set', {id: resolveId(remote), enabled: true})::tap(
_call('remote.set', { id: resolveId(remote), enabled: true })::tap(
subscribeRemotes.forceRefresh
)
)
export const disableRemote = remote => (
_call('remote.set', {id: resolveId(remote), enabled: false})::tap(
_call('remote.set', { id: resolveId(remote), enabled: false })::tap(
subscribeRemotes.forceRefresh
)
)
export const editRemote = (remote, {name, url}) => (
_call('remote.set', resolveIds({remote, name, url}))::tap(
export const editRemote = (remote, { name, url }) => (
_call('remote.set', resolveIds({ remote, name, url }))::tap(
subscribeRemotes.forceRefresh
)
)
export const listRemote = remote => (
_call('remote.list', resolveIds({id: remote}))::tap(
_call('remote.list', resolveIds({ id: remote }))::tap(
subscribeRemotes.forceRefresh
)::rethrow(
err => error(_('listRemote'), err.message || String(err))
@@ -1392,7 +1453,7 @@ export const listRemoteBackups = remote => (
)
export const testRemote = remote => (
_call('remote.test', resolveIds({id: remote}))::rethrow(
_call('remote.test', resolveIds({ id: remote }))::rethrow(
err => error(_('testRemote'), err.message || String(err))
)
)
@@ -1407,8 +1468,8 @@ export const scanFiles = (remote, disk, path, partition) => (
_call('backup.scanFiles', resolveIds({ remote, disk, path, partition }))
)
export const fetchFiles = (remote, disk, partition, paths) => (
_call('backup.fetchFiles', resolveIds({ remote, disk, partition, paths })).then(
export const fetchFiles = (remote, disk, partition, paths, format) => (
_call('backup.fetchFiles', resolveIds({ remote, disk, partition, paths, format })).then(
({ $getFrom: url }) => { window.location = `.${url}` }
)
)
@@ -1416,15 +1477,15 @@ export const fetchFiles = (remote, disk, partition, paths) => (
// -------------------------------------------------------------------
export const probeSrNfs = (host, server) => (
_call('sr.probeNfs', {host, server})
_call('sr.probeNfs', { host, server })
)
export const probeSrNfsExists = (host, server, serverPath) => (
_call('sr.probeNfsExists', {host, server, serverPath})
_call('sr.probeNfsExists', { host, server, serverPath })
)
export const probeSrIscsiIqns = (host, target, port = undefined, chapUser = undefined, chapPassword) => {
const params = {host, target}
const params = { host, target }
port && (params.port = port)
chapUser && (params.chapUser = chapUser)
chapPassword && (params.chapPassword = chapPassword)
@@ -1432,14 +1493,14 @@ export const probeSrIscsiIqns = (host, target, port = undefined, chapUser = unde
}
export const probeSrIscsiLuns = (host, target, targetIqn, chapUser = undefined, chapPassword) => {
const params = {host, target, targetIqn}
const params = { host, target, targetIqn }
chapUser && (params.chapUser = chapUser)
chapPassword && (params.chapPassword = chapPassword)
return _call('sr.probeIscsiLuns', params)
}
export const probeSrIscsiExists = (host, target, targetIqn, scsiId, port = undefined, chapUser = undefined, chapPassword) => {
const params = {host, target, targetIqn, scsiId}
const params = { host, target, targetIqn, scsiId }
port && (params.port = port)
chapUser && (params.chapUser = chapUser)
chapPassword && (params.chapPassword = chapPassword)
@@ -1447,21 +1508,21 @@ export const probeSrIscsiExists = (host, target, targetIqn, scsiId, port = undef
}
export const reattachSr = (host, uuid, nameLabel, nameDescription, type) => (
_call('sr.reattach', {host, uuid, nameLabel, nameDescription, type})
_call('sr.reattach', { host, uuid, nameLabel, nameDescription, type })
)
export const reattachSrIso = (host, uuid, nameLabel, nameDescription, type) => (
_call('sr.reattachIso', {host, uuid, nameLabel, nameDescription, type})
_call('sr.reattachIso', { host, uuid, nameLabel, nameDescription, type })
)
export const createSrNfs = (host, nameLabel, nameDescription, server, serverPath, nfsVersion = undefined) => {
const params = {host, nameLabel, nameDescription, server, serverPath}
const params = { host, nameLabel, nameDescription, server, serverPath }
nfsVersion && (params.nfsVersion = nfsVersion)
return _call('sr.createNfs', params)
}
export const createSrIscsi = (host, nameLabel, nameDescription, target, targetIqn, scsiId, port = undefined, chapUser = undefined, chapPassword = undefined) => {
const params = {host, nameLabel, nameDescription, target, targetIqn, scsiId}
const params = { host, nameLabel, nameDescription, target, targetIqn, scsiId }
port && (params.port = port)
chapUser && (params.chapUser = chapUser)
chapPassword && (params.chapPassword = chapPassword)
@@ -1469,20 +1530,20 @@ export const createSrIscsi = (host, nameLabel, nameDescription, target, targetIq
}
export const createSrIso = (host, nameLabel, nameDescription, path, type, user = undefined, password = undefined) => {
const params = {host, nameLabel, nameDescription, path, type}
const params = { host, nameLabel, nameDescription, path, type }
user && (params.user = user)
password && (params.password = password)
return _call('sr.createIso', params)
}
export const createSrLvm = (host, nameLabel, nameDescription, device) => (
_call('sr.createLvm', {host, nameLabel, nameDescription, device})
_call('sr.createLvm', { host, nameLabel, nameDescription, device })
)
// Job logs ----------------------------------------------------------
export const deleteJobsLog = id => (
_call('log.delete', {namespace: 'jobs', id})::tap(
_call('log.delete', { namespace: 'jobs', id })::tap(
subscribeJobsLogs.forceRefresh
)
)
@@ -1490,23 +1551,23 @@ export const deleteJobsLog = id => (
// Logs
export const deleteApiLog = id => (
_call('log.delete', {namespace: 'api', id})::tap(
_call('log.delete', { namespace: 'api', id })::tap(
subscribeApiLogs.forceRefresh
)
)
// Acls, users, groups ----------------------------------------------------------
export const addAcl = ({subject, object, action}) => (
_call('acl.add', resolveIds({subject, object, action}))::tap(
export const addAcl = ({ subject, object, action }) => (
_call('acl.add', resolveIds({ subject, object, action }))::tap(
subscribeAcls.forceRefresh
)::rethrow(
err => error('Add ACL', err.message || String(err))
)
)
export const removeAcl = ({subject, object, action}) => (
_call('acl.remove', resolveIds({subject, object, action}))::tap(
export const removeAcl = ({ subject, object, action }) => (
_call('acl.remove', resolveIds({ subject, object, action }))::tap(
subscribeAcls.forceRefresh
)::rethrow(
err => error('Remove ACL', err.message || String(err))
@@ -1521,14 +1582,14 @@ export const editAcl = (
action: newAction = action
}
) => (
_call('acl.remove', resolveIds({subject, object, action}))
.then(() => _call('acl.add', resolveIds({subject: newSubject, object: newObject, action: newAction})))
_call('acl.remove', resolveIds({ subject, object, action }))
.then(() => _call('acl.add', resolveIds({ subject: newSubject, object: newObject, action: newAction })))
::tap(subscribeAcls.forceRefresh)
::rethrow(err => error('Edit ACL', err.message || String(err)))
)
export const createGroup = name => (
_call('group.create', {name})::tap(
_call('group.create', { name })::tap(
subscribeGroups.forceRefresh
):: rethrow(
err => error(_('createGroup'), err.message || String(err))
@@ -1536,7 +1597,7 @@ export const createGroup = name => (
)
export const setGroupName = (group, name) => (
_call('group.set', resolveIds({group, name}))::tap(
_call('group.set', resolveIds({ group, name }))::tap(
subscribeGroups.forceRefresh
)
)
@@ -1545,13 +1606,13 @@ export const deleteGroup = group => (
confirm({
title: _('deleteGroup'),
body: <p>{_('deleteGroupConfirm')}</p>
}).then(() => _call('group.delete', resolveIds({id: group})))
}).then(() => _call('group.delete', resolveIds({ id: group })))
::tap(subscribeGroups.forceRefresh)
::rethrow(err => error(_('deleteGroup'), err.message || String(err)))
)
export const removeUserFromGroup = (user, group) => (
_call('group.removeUser', resolveIds({id: group, userId: user}))::tap(
_call('group.removeUser', resolveIds({ id: group, userId: user }))::tap(
subscribeGroups.forceRefresh
)::rethrow(
err => error(_('removeUserFromGroup'), err.message || String(err))
@@ -1559,7 +1620,7 @@ export const removeUserFromGroup = (user, group) => (
)
export const addUserToGroup = (user, group) => (
_call('group.addUser', resolveIds({id: group, userId: user}))::tap(
_call('group.addUser', resolveIds({ id: group, userId: user }))::tap(
subscribeGroups.forceRefresh
)::rethrow(
err => error('Add User', err.message || String(err))
@@ -1567,7 +1628,7 @@ export const addUserToGroup = (user, group) => (
)
export const createUser = (email, password, permission) => (
_call('user.create', {email, password, permission})::tap(
_call('user.create', { email, password, permission })::tap(
subscribeUsers.forceRefresh
)::rethrow(
err => error('Create user', err.message || String(err))
@@ -1610,10 +1671,10 @@ const _setUserPreferences = preferences => (
)
)
import NewSshKeyModalBody from './new-ssh-key-modal'
import NewSshKeyModalBody from './new-ssh-key-modal' // eslint-disable-line import/first
export const addSshKey = key => {
const { preferences } = xo.user
const otherKeys = preferences && preferences.sshKeys || []
const otherKeys = (preferences && preferences.sshKeys) || []
if (key) {
return _setUserPreferences({ sshKeys: [
...otherKeys,
@@ -1656,7 +1717,7 @@ export const deleteSshKey = key => (
// User filters --------------------------------------------------
import AddUserFilterModalBody from './add-user-filter-modal'
import AddUserFilterModalBody from './add-user-filter-modal' // eslint-disable-line import/first
export const addCustomFilter = (type, value) => {
const { user } = xo
return confirm({
@@ -1750,3 +1811,30 @@ export const setIpPool = (ipPool, { name, addresses, networks }) => (
subscribeIpPools.forceRefresh
)
)
// XO SAN ----------------------------------------------------------------------
export const getVolumeInfo = (xosanSr) => _call('xosan.getVolumeInfo', { sr: xosanSr })
export const createXosanSR = ({ template, pif, vlan, srs, glusterType, redundancy }) => _call('xosan.createSR', {
template,
pif: pif.id,
vlan: String(vlan),
srs: resolveIds(srs),
glusterType,
redundancy: Number.parseInt(redundancy)
})
export const computeXosanPossibleOptions = lvmSrs => _call('xosan.computeXosanPossibleOptions', { lvmSrs })
import InstallXosanPackModal from './install-xosan-pack-modal' // eslint-disable-line import/first
export const downloadAndInstallXosanPack = pool =>
confirm({
title: _('xosanInstallPackTitle', { pool: pool.name_label }),
icon: 'export',
body: <InstallXosanPackModal pool={pool} />
}).then(
pack => _call('xosan.downloadAndInstallXosanPack', { id: pack.id, version: pack.version, pool: resolveId(pool) })
)
export const registerXosan = namespace => _call('cloud.registerResource', { namespace: 'xosan' })

View File

@@ -0,0 +1,109 @@
import _ from 'intl'
import Component from 'base-component'
import React from 'react'
import { connectStore, compareVersions, isXosanPack } from 'utils'
import { subscribeResourceCatalog, subscribePlugins } from 'xo'
import { createGetObjectsOfType, createSelector, createCollectionWrapper } from 'selectors'
import { satisfies as versionSatisfies } from 'semver'
import {
every,
filter,
forEach,
map,
some
} from 'lodash'
const findLatestPack = (packs, hostsVersions) => {
const checkVersion = version =>
every(hostsVersions, hostVersion => versionSatisfies(hostVersion, version))
let latestPack = { version: '0' }
forEach(packs, pack => {
const xsVersionRequirement = pack.requirements && pack.requirements.xenserver
if (
pack.type === 'iso' &&
compareVersions(pack.version, latestPack.version) > 0 &&
(!xsVersionRequirement || checkVersion(xsVersionRequirement))
) {
latestPack = pack
}
})
if (latestPack.version === '0') {
// No compatible pack was found
return
}
return latestPack
}
@connectStore(() => ({
hosts: createGetObjectsOfType('host').filter(
createSelector(
(_, { pool }) => pool != null && pool.id,
poolId => poolId
? host => host.$pool === poolId && !some(host.supplementalPacks, isXosanPack)
: false
)
)
}), { withRef: true })
export default class InstallXosanPackModal extends Component {
componentDidMount () {
this._unsubscribePlugins = subscribePlugins(plugins => this.setState({ plugins }))
this._unsubscribeResourceCatalog = subscribeResourceCatalog(catalog => this.setState({ catalog }))
}
componentWillUnmount () {
this._unsubscribePlugins()
this._unsubscribeResourceCatalog()
}
_getXosanLatestPack = createSelector(
() => this.state.catalog && this.state.catalog.xosan,
createSelector(
() => this.props.hosts,
createCollectionWrapper(hosts => map(hosts, 'version'))
),
findLatestPack
)
_getXosanPacks = createSelector(
() => this.state.catalog && this.state.catalog.xosan,
packs => filter(packs, ({ type }) => type === 'iso')
)
get value () {
return this._getXosanLatestPack()
}
render () {
const { hosts } = this.props
const latestPack = this._getXosanLatestPack()
return <div>
{latestPack
? <div>
{_('xosanInstallPackOnHosts')}
<ul>
{map(hosts, host => <li key={host.id}>{host.name_label}</li>)}
</ul>
<div className='mt-1'>
{_('xosanInstallPack', { pack: latestPack.name, version: latestPack.version })}
</div>
</div>
: <div>
<p>{_('xosanNoPackFound')}</p>
<p>
{_('xosanPackRequirements')}
<ul>
{map(this._getXosanPacks(), ({ name, requirements }) => <li>
{name}: <strong>{requirements && requirements.xenserver ? requirements.xenserver : '/'}</strong>
</li>)}
</ul>
</p>
</div>
}
</div>
}
}

View File

@@ -1,14 +1,28 @@
import forEach from 'lodash/forEach'
import {
forEach,
includes,
map
} from 'lodash'
export const getDefaultNetworkForVif = (vif, destHost, pifs, networks) => {
const originNetwork = networks[vif.$network]
const originVlans = map(originNetwork.PIFs, pifId => pifs[pifId].vlan)
let destNetworkId = pifs[destHost.$PIFs[0]].$network
forEach(destHost.$PIFs, pifId => {
const { $network, vlan } = pifs[pifId]
if (networks[$network].name_label === originNetwork.name_label) {
destNetworkId = $network
export const getDefaultNetworkForVif = (vif, host, pifs, networks) => {
const nameLabel = networks[vif.$network].name_label
let defaultNetwork
forEach(host.$PIFs, pifId => {
const pif = pifs[pifId]
if (networks[pif.$network].name_label === nameLabel) {
defaultNetwork = pif.$network
return false
}
if (vlan !== -1 && includes(originVlans, vlan)) {
destNetworkId = $network
}
})
return defaultNetwork
return destNetworkId
}

View File

@@ -77,7 +77,7 @@ class XoaUpdater extends EventEmitter {
state (state) {
this._state = state
this.emit(state, this._lowState && this._lowState.packages)
this.emit(state, this._lowState && this._lowState.source)
}
async update () {
@@ -99,7 +99,7 @@ class XoaUpdater extends EventEmitter {
}
_upgradeSuccessful () {
this.emit('upgradeSuccessful', this._lowState && this._lowState.packages)
this.emit('upgradeSuccessful', this._lowState && this._lowState.source)
}
async _open () {
@@ -240,7 +240,7 @@ class XoaUpdater extends EventEmitter {
this.registerState = 'error'
}
} finally {
this.emit('registerState', {state: this.registerState, email: this.token && this.token.registrationEmail || '', error: this.registerError})
this.emit('registerState', {state: this.registerState, email: (this.token && this.token.registrationEmail) || '', error: this.registerError})
}
}
@@ -262,7 +262,7 @@ class XoaUpdater extends EventEmitter {
this.registerState = 'error'
}
} finally {
this.emit('registerState', {state: this.registerState, email: this.token && this.token.registrationEmail || '', error: this.registerError})
this.emit('registerState', {state: this.registerState, email: (this.token && this.token.registrationEmail) || '', error: this.registerError})
if (this.registerState === 'registered') {
this.update()
}
@@ -351,7 +351,7 @@ class XoaUpdater extends EventEmitter {
}
log (level, message) {
message = message && message.message || String(message)
message = (message != null && message.message) || String(message)
const date = new Date()
this._log.unshift({
date: date.toLocaleString(),

Some files were not shown because too many files have changed in this diff Show More