Compare commits

...

203 Commits

Author SHA1 Message Date
Julien Fontanet
a9eb1f3d27 5.12.0 2017-08-31 18:06:42 +02:00
Olivier Lambert
9a0544c4aa feat(changelog): add changelog for 5.12 2017-08-31 17:59:25 +02:00
Pierre Donias
31c365313b feat(xosan): beta 2 (#2331) 2017-08-31 17:56:53 +02:00
Pierre Donias
b44017ca95 feat(pool/actions): add SR, add VM and disconnect server (#2308)
Fixes #2307
2017-08-31 17:50:59 +02:00
Julien Fontanet
289112af27 feat(SortedTable): support range selection with shift key (#2328)
Fixes #2323
2017-08-31 17:25:48 +02:00
Julien Fontanet
4d2dc4eece feat(backups): add retention param for CR (#2325)
Fixes #1692
2017-08-31 17:21:56 +02:00
Pierre Donias
712101d8d6 feat(render-xo-item): icon color to show if PIF is physically connected (#2327)
Fixes #2326
2017-08-30 17:53:10 +02:00
Pierre Donias
828ba5d448 fix(ActionBar): pending prop & undefined prop access (#2322) 2017-08-28 15:54:01 +02:00
Olivier Lambert
03a2ff8e8c fix(migrate): re-display migrate button for offline VMs (#2321) 2017-08-28 15:23:33 +02:00
badrAZ
75487203cf fix(job/log): remove unused function (#2319) 2017-08-24 15:24:21 +02:00
badrAZ
6ad751f079 feat(SortedTable): add grouped actions feature (#2288)
Fixes #2276
2017-08-24 15:23:40 +02:00
Pierre Donias
78ddad839e feat(home): resource sets filter dropdown (#2314)
Fixes #2303
2017-08-17 17:03:33 +02:00
Pierre Donias
812cecdcc4 fix(groups): "Add user" selector should be empty after adding a user (#2312)
Fixes #2196
2017-08-17 10:48:24 +02:00
Pierre Donias
4b49da7d8f feat(srs/advanced): total VDIs to coalesce (#2309)
Fixes #2300
2017-08-16 18:08:46 +02:00
Pierre Donias
fc8c37d66c fix(home): set to page 1 when filter changes (#2311)
Fixes #2310
2017-08-16 17:42:26 +02:00
Julien Fontanet
0d618e6477 fix(sr/disks): fix links
Thanks @Danp2 :)
2017-08-14 11:29:58 +02:00
badrAZ
d7e2b12d3d feat(tasks): add pool filter (#2295)
Fixes #2293
2017-08-11 18:01:08 +02:00
badrAZ
2ae4ed3999 fix: display "loading" while fetching objects (#2294)
Fixes  #2285
2017-08-11 17:47:12 +02:00
badrAZ
eaaf70e52e feat(logs): add a call state filter (#2290)
Fixes #2246
2017-08-11 16:07:19 +02:00
Julien Fontanet
bb4ebcd198 chore(sr/disks): fix link to snapshot 2017-08-09 17:40:16 +02:00
Julien Fontanet
a8b7431a02 feat(card/Card): allow passing extra props 2017-08-09 14:27:07 +02:00
Julien Fontanet
a5ec70f7fa chore(card/Card): remove unused propType 2017-08-09 14:26:50 +02:00
Julien Fontanet
75dfdd4854 fix(sr/disks): link to dashboard/health if snapshot parent is missing 2017-08-09 14:26:06 +02:00
badrAZ
3f29dd129f fix(vm/action-bar): no migrate when VM is halted (#2291)
Fixes #2233
2017-08-08 14:53:36 +02:00
Julien Fontanet
7b19341406 fix(xo/createSubscription): race condition on unsubscribe
Issue if the sole subscriber unsubscribe before the call finishes
2017-08-08 10:15:50 +02:00
badrAZ
838ad58946 feat(pool): ability to designate a new master (#2289)
Fixes #2213
2017-08-03 12:38:19 +02:00
Julien Fontanet
ec4a7325da 5.11.0 2017-07-31 18:26:18 +02:00
Olivier Lambert
efbd588c9e feat(objects): remove a useless br (#2283) 2017-07-31 17:49:00 +02:00
Olivier Lambert
e535e064fa feat(changelog): add changelog for 5.11 version 2017-07-31 11:23:24 +02:00
Pierre Donias
448068178b feat(action-bar): implement as standard JSX (#2281) 2017-07-28 17:26:16 +02:00
Julien Fontanet
7308d9ca96 fix(xo): fix API method name 2017-07-27 10:50:46 +02:00
badrAZ
e642f54815 feat(SR/advanced): display the non-healthy vdi chains (#2273)
Fixes #2178
2017-07-26 14:03:55 +02:00
badrAZ
ebb6cb17ea fix: a user with ACLs should not be able to create a VM (#2220)
Fixes #2191 #2202
2017-07-24 11:32:59 +02:00
Pierre Donias
08a0aa9f98 fix(new-vm): disks' device property (#2271)
- existingDisks: use vbd.position as the device
- new VDIs: do not assign any device, xo-server will do it
2017-07-21 17:20:17 +02:00
Pierre Donias
8a933c98e3 fix: multiple fixes (#2272) 2017-07-21 10:36:14 +02:00
Julien Fontanet
363b22bffe feat(logs): display duration for each call (#2266) 2017-07-07 12:19:18 +02:00
Julien Fontanet
79a85659aa 5.10.5 2017-07-05 17:36:31 +02:00
Pierre Donias
eca145e113 chore(intl/locales/fr): update and new translations (#2260) 2017-07-04 14:44:52 +02:00
Julien Fontanet
04b75fb3b3 chore(intl/locales/fr): update translation 2017-07-04 12:21:27 +02:00
Julien Fontanet
582e220a02 chore(intl/messages): remove extraneous space 2017-07-04 12:21:07 +02:00
Julien Fontanet
2e87abefc4 fix(react-novnc): use correct port 2017-07-04 11:40:34 +02:00
Julien Fontanet
5ea19ee56f chore(intl/locales/es): update (#2256) 2017-07-03 16:24:04 +02:00
Julien Fontanet
2ee733399e 5.10.4 2017-06-30 19:34:31 +02:00
Julien Fontanet
73f228c719 Merge branch 'next-release' into stable 2017-06-30 19:34:19 +02:00
Julien Fontanet
ba79673715 5.10.3 2017-06-30 19:32:53 +02:00
Julien Fontanet
86b0962063 fix(react-novnc): url.port is not undefined when empty 2017-06-30 19:32:08 +02:00
Julien Fontanet
e2a482a6ca 5.10.2 2017-06-30 19:15:43 +02:00
Julien Fontanet
8924444cc1 fix(react-novnc): use correct var (sic) 2017-06-30 19:14:35 +02:00
Julien Fontanet
8e83b0ffd2 5.10.1 2017-06-30 19:12:55 +02:00
Julien Fontanet
451384bcdc fix(react-novnc): default ports if missing 2017-06-30 19:12:17 +02:00
Julien Fontanet
b733164c50 5.10.0 2017-06-30 18:27:46 +02:00
badrAZ
72d9d8ba86 feat(backups): optional VDI→SR mapping (#2201)
Fixes #2070
2017-06-30 18:27:31 +02:00
Olivier Lambert
7c7646c65c feat(changelog): add the last changes for 5.10 release 2017-06-30 17:37:47 +02:00
Olivier Lambert
b1c087451e feat(console): remove the tip regarding console layout issues (#2251) 2017-06-30 17:30:53 +02:00
Nicolas Raynaud
f0f72f3bdd update noVNC to latest upstream version (#1780)
Fixes #404
2017-06-30 16:55:42 +02:00
badrAZ
0ab3267541 feat(self-service): improve IP pool UI (#2228)
Fixes #2203
2017-06-30 16:38:01 +02:00
badrAZ
995e76d323 feat(job/log): add more details on a backup (#2245)
Fixes #2239
2017-06-30 16:33:31 +02:00
Olivier Lambert
62a9f805c2 feat(changelog): add one enhancement 2017-06-30 13:43:37 +02:00
Olivier Lambert
84ea95a641 feat(changelog): initial changelog for 5.10 release 2017-06-30 10:08:44 +02:00
Olivier Lambert
316de42cd9 feat(host): forget host. Fixes #1934 (#2244)
feat(host): forget host. Fixes #1934
2017-06-29 12:27:47 +02:00
badrAZ
bc2256fc86 fix(vm/action-bar): always display migrate button (#2232)
Fixes #2212
2017-06-23 14:50:43 +02:00
Julien Fontanet
f0d85f4c4e fix(vm): remove unused imports 2017-06-20 19:19:29 +02:00
Julien Fontanet
1801f9cb06 feat(Home): display total disk size for each VM 2017-06-20 19:07:57 +02:00
Julien Fontanet
4be018ad15 feat(selectors/createSumBy): sum collection of items 2017-06-20 17:51:47 +02:00
Julien Fontanet
5dcf060975 chore(selectors/createPicker): avoid running collection wrapper when possible 2017-06-20 17:51:00 +02:00
Julien Fontanet
59f6b1f0c8 fix(StateButton): missing semicolons in CSS 2017-06-20 15:19:51 +02:00
Julien Fontanet
ae38a85b19 chore(package): update dependencies 2017-06-20 15:08:41 +02:00
badrAZ
324dbbcfc8 fix(modal/alert): resolve on close (#2224)
Fixes #2222
2017-06-20 15:08:33 +02:00
Julien Fontanet
9770b77df4 feat(sr/disks): improve filters 2017-06-20 10:41:50 +02:00
Julien Fontanet
0f91de389a fix(Health): check VBDs for orphaned VDI snapshots
VDI snapshots attached to a VM are not considered orphaned.
2017-06-19 16:41:55 +02:00
Julien Fontanet
7f5a623b37 feat(sr/general): display disks size 2017-06-08 11:59:44 +02:00
Julien Fontanet
c7cf73ff05 fix(sr/disks): difference between no VM and unknown VM 2017-06-08 11:59:08 +02:00
Julien Fontanet
4aab425cef 5.9.1 2017-06-08 10:23:34 +02:00
Julien Fontanet
0d9666639f fix(sr): unused imports 2017-06-06 17:08:12 +02:00
Julien Fontanet
6c26c09685 fix(sr/general): show VDI snaphots 2017-06-06 16:57:56 +02:00
Julien Fontanet
819f650b48 chore(sr/general): better retrieve VM associated to VDI 2017-06-06 16:57:56 +02:00
Julien Fontanet
353eba6365 fix(sr/disks): show the correct attached VM for snapshots 2017-06-06 16:57:55 +02:00
Julien Fontanet
063302b91d feat(renderXoUnknownItem): expose it 2017-06-06 16:57:55 +02:00
Julien Fontanet
562b51bc2f feat(SortedTable): can accept component instead of itemRenderer 2017-06-06 16:57:55 +02:00
Julien Fontanet
e33a6f9a05 chore(xo): use tap() from promise-toolbox 2017-06-05 15:50:16 +02:00
Danp2
b9db4e7704 fix(xo/deleteGroup): properly handle confirm rejection (#2197)
Resolve issue with canceling / exiting dialog
2017-06-05 15:48:31 +02:00
Julien Fontanet
3270d9c3a7 chore(plugins): coding style 2017-06-02 16:57:32 +02:00
Julien Fontanet
6d7399f96c fix(plugins): remove collpase button if not configurable 2017-06-02 16:57:32 +02:00
Julien Fontanet
886ef87bc5 fix(plugins): primary style on save button 2017-06-02 16:57:32 +02:00
Julien Fontanet
1e5dc9efe7 fix(json-schema-input/string): consider empty value as undefined (#2192) 2017-06-02 16:21:30 +02:00
Julien Fontanet
28ec66bf3b fix(backups): handle object values without id prop 2017-06-02 12:11:55 +02:00
Julien Fontanet
9199784a23 5.9.0 2017-05-31 18:14:51 +02:00
Pierre Donias
c7e447db6f feat(host): update patches when joining pool (#2187)
Fixes #878
2017-05-31 17:59:11 +02:00
Pierre Donias
f81615f8b6 feat(dashboard/health): VDIs attached to control domain (#2183)
Fixes #2126
2017-05-31 16:33:37 +02:00
badrAZ
12caceb02b feat: start a VM even when forbidden (#2161)
Fixes #2119
2017-05-31 16:05:57 +02:00
Julien Fontanet
30f71ab444 feat(selectors/createDoesHostNeedRestart): use host.rebootRequired (#2179) 2017-05-31 15:33:05 +02:00
Pierre Donias
fe04481ca3 feat(xo): subscribe to missing patches instead of explicitly checking (#2182) 2017-05-31 15:02:05 +02:00
Pierre Donias
7766e8edcd Better createDoesHostNeedRestart selector 2017-05-31 14:54:07 +02:00
Olivier Lambert
31d417c9d3 feat(changelog): added info for 5.9 release 2017-05-31 13:46:29 +02:00
Pierre Donias
5ed29197cf Use host.rebootRequired boolean 2017-05-30 16:26:32 +02:00
Pierre Donias
ff5f3e12d3 feat(selectors/createDoesHostNeedRestart): use host.patchesRequiringReboot
Fixes #2124
2017-05-30 16:26:32 +02:00
badrAZ
240180405c fix(job/logs): correctly extract vm id from returned value (#2167) 2017-05-30 15:51:37 +02:00
badrAZ
edca6495fc feat(self-service): add button "Select all" to the selects (#2181) 2017-05-30 12:39:20 +02:00
BCedric
8a9b753b01 feat(host/patches): advise to patch from pool (#2130)
Fixes #2057
2017-05-29 14:52:54 +02:00
Julien Fontanet
445fc696c9 fix(backup/new): clarify enabled setting (#2177) 2017-05-29 10:39:15 +02:00
Julien Fontanet
492e2362be chore(utils/firstDefined): fix comment, null is considered defined 2017-05-26 15:50:28 +02:00
badrAZ
1acee209be feat(backup/new): DR previous backups can be removed first (#2173)
Fixes #2157
2017-05-26 13:34:16 +02:00
Olivier Lambert
6785c48709 feat(tasks): display task description if it exists (#2172)
Fixes #2125
2017-05-25 12:45:33 +02:00
Olivier Lambert
808e674503 feat(menu): hide About entry if non-admin (on XOA) (#2170) 2017-05-24 17:36:13 +02:00
Julien Fontanet
6b2650282d 5.8.3 2017-05-23 18:53:10 +02:00
Julien Fontanet
475be2ee30 fix(vm/advanced): behave with missing container 2017-05-23 18:52:39 +02:00
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
149 changed files with 12074 additions and 4743 deletions

View File

@@ -1,5 +1,131 @@
# ChangeLog
## **5.12.0** (2017-08-31)
### Enhancements
* PIF selector with physical status [#2326](https://github.com/vatesfr/xo-web/issues/2326)
* [SortedTable] Range selection [#2323](https://github.com/vatesfr/xo-web/issues/2323)
* Self service filter for home/VM view [#2303](https://github.com/vatesfr/xo-web/issues/2303)
* SR/Disks Display total of VDIs to coalesce [#2300](https://github.com/vatesfr/xo-web/issues/2300)
* Pool filter in the task view [#2293](https://github.com/vatesfr/xo-web/issues/2293)
* "Loading" while fetching objects [#2285](https://github.com/vatesfr/xo-web/issues/2285)
* [SortedTable] Add grouped actions feature [#2276](https://github.com/vatesfr/xo-web/issues/2276)
* Add a filter to the backups' log [#2246](https://github.com/vatesfr/xo-web/issues/2246)
* It should not be possible to migrate a halted VM. [#2233](https://github.com/vatesfr/xo-web/issues/2233)
* [Home][Keyboard navigation] Allow selecting the objects [#2214](https://github.com/vatesfr/xo-web/issues/2214)
* Allow to set pool master [#2213](https://github.com/vatesfr/xo-web/issues/2213)
* Continuous Replication Retention [#1692](https://github.com/vatesfr/xo-web/issues/1692)
### Bugs
* Home pagination bug [#2310](https://github.com/vatesfr/xo-web/issues/2310)
* Fix PoolActionBar to add a new SR [#2307](https://github.com/vatesfr/xo-web/issues/2307)
* VM snapshots are not correctly deleted [#2304](https://github.com/vatesfr/xo-web/issues/2304)
* Parallel deletion of VMs fails [#2297](https://github.com/vatesfr/xo-web/issues/2297)
* Continous replication create multiple zombie disks [#2292](https://github.com/vatesfr/xo-web/issues/2292)
* Add user to Group issue [#2196](https://github.com/vatesfr/xo-web/issues/2196)
* [VM migration] Error if default SR not accessible to target host [#2180](https://github.com/vatesfr/xo-web/issues/2180)
## **5.11.0** (2017-07-31)
### Enhancements
- Storage VHD chain health [\#2178](https://github.com/vatesfr/xo-web/issues/2178)
### Bug fixes
- No web VNC console [\#2258](https://github.com/vatesfr/xo-web/issues/2258)
- Patching issues [\#2254](https://github.com/vatesfr/xo-web/issues/2254)
- Advanced button in VM creation for self service user [\#2202](https://github.com/vatesfr/xo-web/issues/2202)
- Hide "new VM" menu entry if not admin or not self service user [\#2191](https://github.com/vatesfr/xo-web/issues/2191)
## **5.10.0** (2017-06-30)
### Enhancements
- Improve backup log display [\#2239](https://github.com/vatesfr/xo-web/issues/2239)
- Patch SR detection improvement [\#2215](https://github.com/vatesfr/xo-web/issues/2215)
- Less strict coalesce detection [\#2207](https://github.com/vatesfr/xo-web/issues/2207)
- IP pool UI improvement [\#2203](https://github.com/vatesfr/xo-web/issues/2203)
- Ability to clear "Auto power on" flag for DR-ed VM [\#2097](https://github.com/vatesfr/xo-web/issues/2097)
- [Delta backup restoration] Choose SR for each VDIs [\#2070](https://github.com/vatesfr/xo-web/issues/2070)
- Ability to forget an host (even if no longer present) [\#1934](https://github.com/vatesfr/xo-web/issues/1934)
### Bug fixes
- Cross pool migrate fail [\#2248](https://github.com/vatesfr/xo-web/issues/2248)
- ActionButtons with modals stay in pending state forever [\#2222](https://github.com/vatesfr/xo-web/issues/2222)
- Permission issue for a user on self service VMs [\#2212](https://github.com/vatesfr/xo-web/issues/2212)
- Self-Service resource loophole [\#2198](https://github.com/vatesfr/xo-web/issues/2198)
- Backup log no longer shows the name of destination VM [\#2195](https://github.com/vatesfr/xo-web/issues/2195)
- State not restored when exiting modal dialog [\#2194](https://github.com/vatesfr/xo-web/issues/2194)
- [Xapi#exportDeltaVm] Cannot read property 'managed' of undefined [\#2189](https://github.com/vatesfr/xo-web/issues/2189)
- VNC keyboard layout change [\#404](https://github.com/vatesfr/xo-web/issues/404)
## **5.9.0** (2017-05-31)
### Enhancements
- Allow DR to remove previous backup first [\#2157](https://github.com/vatesfr/xo-web/issues/2157)
- Feature request - add amount of RAM to memory bars [\#2149](https://github.com/vatesfr/xo-web/issues/2149)
- Make the acceptability of invalid certificates configurable [\#2138](https://github.com/vatesfr/xo-web/issues/2138)
- label of VM names in tasks link [\#2135](https://github.com/vatesfr/xo-web/issues/2135)
- Backup report timezone [\#2133](https://github.com/vatesfr/xo-web/issues/2133)
- xo-server-recover-account [\#2129](https://github.com/vatesfr/xo-web/issues/2129)
- Detect disks attached to control domain [\#2126](https://github.com/vatesfr/xo-web/issues/2126)
- Add task description in Tasks view [\#2125](https://github.com/vatesfr/xo-web/issues/2125)
- Host reboot warning after patching for 7.1 [\#2124](https://github.com/vatesfr/xo-web/issues/2124)
- Continuous Replication - possibility run VM without a clone [\#2119](https://github.com/vatesfr/xo-web/issues/2119)
- Unreachable host should be detected [\#2099](https://github.com/vatesfr/xo-web/issues/2099)
- Orange icon when host is is disabled [\#2098](https://github.com/vatesfr/xo-web/issues/2098)
- Enhanced backup report logs [\#2096](https://github.com/vatesfr/xo-web/issues/2096)
- Only show failures when configured to report on failures [\#2095](https://github.com/vatesfr/xo-web/issues/2095)
- "Add all" button in self service [\#2081](https://github.com/vatesfr/xo-web/issues/2081)
- Patch and pack mechanism changed on Ely [\#2058](https://github.com/vatesfr/xo-web/issues/2058)
- Tip or ask people to patch from pool view [\#2057](https://github.com/vatesfr/xo-web/issues/2057)
- File restore - Remind compatible backup [\#1930](https://github.com/vatesfr/xo-web/issues/1930)
- Reporting for halted vm time [\#1613](https://github.com/vatesfr/xo-web/issues/1613)
- Add standalone XS server to a pool and patch it to the pool level [\#878](https://github.com/vatesfr/xo-web/issues/878)
- Add Cores-per-sockets [\#130](https://github.com/vatesfr/xo-web/issues/130)
### Bug fixes
- VM creation is broken for non-admins [\#2168](https://github.com/vatesfr/xo-web/issues/2168)
- Can't create cloud config drive [\#2162](https://github.com/vatesfr/xo-web/issues/2162)
- Select is "moving" [\#2142](https://github.com/vatesfr/xo-web/issues/2142)
- Select issue for affinity host [\#2141](https://github.com/vatesfr/xo-web/issues/2141)
- Dashboard Storage Usage incorrect [\#2123](https://github.com/vatesfr/xo-web/issues/2123)
- Detect unmerged *base copy* and prevent too long chains [\#2047](https://github.com/vatesfr/xo-web/issues/2047)
## **5.8.0** (2017-04-28)
### Enhancements
- Limit About view info for non-admins [\#2109](https://github.com/vatesfr/xo-web/issues/2109)
- Enabling/disabling boot device on HVM VM [\#2105](https://github.com/vatesfr/xo-web/issues/2105)
- Filter: Hide snapshots in SR disk view [\#2102](https://github.com/vatesfr/xo-web/issues/2102)
- Smarter XOSAN install [\#2084](https://github.com/vatesfr/xo-web/issues/2084)
- PL translation [\#2079](https://github.com/vatesfr/xo-web/issues/2079)
- Remove the "share this VM" option if not in self service [\#2061](https://github.com/vatesfr/xo-web/issues/2061)
- "connected" status graphics are not the same on the host storage and networking tabs [\#2060](https://github.com/vatesfr/xo-web/issues/2060)
- Ability to view and edit `vga` and `videoram` fields in VM view [\#158](https://github.com/vatesfr/xo-web/issues/158)
- Performances [\#1](https://github.com/vatesfr/xen-api/issues/1)
### Bug fixes
- Dashboard display issues [\#2108](https://github.com/vatesfr/xo-web/issues/2108)
- Dashboard CPUs Usage [\#2105](https://github.com/vatesfr/xo-web/issues/2105)
- [Dashboard/Overview] Warning [\#2090](https://github.com/vatesfr/xo-web/issues/2090)
- VM creation displays all networks [\#2086](https://github.com/vatesfr/xo-web/issues/2086)
- Cannot change HA mode for a VM [\#2080](https://github.com/vatesfr/xo-web/issues/2080)
- [Smart backup] Tags selection does not work [\#2077](https://github.com/vatesfr/xo-web/issues/2077)
- [Backup jobs] Timeout should be in seconds, not milliseconds [\#2076](https://github.com/vatesfr/xo-web/issues/2076)
- Missing VM templates [\#2075](https://github.com/vatesfr/xo-web/issues/2075)
- [transport-email] From header not set [\#2074](https://github.com/vatesfr/xo-web/issues/2074)
- Missing objects should be displayed in backup edition [\#2052](https://github.com/vatesfr/xo-web/issues/2052)
## **5.7.0** (2017-03-31)
### Enhancements
@@ -258,7 +384,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

@@ -258,7 +258,7 @@ gulp.task(function buildScripts () {
]
}),
require('gulp-sourcemaps').init({ loadMaps: true }),
PRODUCTION && require('gulp-uglify')(),
PRODUCTION && require('gulp-uglify/composer')(require('uglify-es'))(),
dest()
)
})

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.7.3",
"version": "5.12.0",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -31,6 +31,7 @@
"npm": ">=3"
},
"devDependencies": {
"@nraynaud/novnc": "^0.6.1-1",
"ansi_up": "^1.3.0",
"asap": "^2.0.4",
"babel-eslint": "^7.0.0",
@@ -61,13 +62,13 @@
"dependency-check": "^2.5.1",
"enzyme": "^2.6.0",
"enzyme-to-json": "^1.4.4",
"event-to-promise": "^0.7.0",
"event-to-promise": "^0.8.0",
"font-awesome": "^4.7.0",
"font-mfizz": "github:fizzed/font-mfizz",
"get-stream": "^2.3.0",
"globby": "^6.0.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-autoprefixer": "^3.1.0",
"gulp-autoprefixer": "^4.0.0",
"gulp-csso": "^3.0.0",
"gulp-embedlr": "^0.5.2",
"gulp-plumber": "^1.1.0",
@@ -75,13 +76,14 @@
"gulp-refresh": "^1.1.0",
"gulp-sass": "^3.0.0",
"gulp-sourcemaps": "^2.2.3",
"gulp-uglify": "^2.0.0",
"gulp-uglify": "^3.0.0",
"gulp-watch": "^4.3.5",
"human-format": "^0.7.0",
"human-format": "^0.8.0",
"husky": "^0.13.1",
"immutable": "^3.8.1",
"index-modules": "^0.3.0",
"is-ip": "^1.0.0",
"jest": "^19.0.2",
"jest": "^20.0.4",
"jsonrpc-websocket-client": "^0.1.1",
"kindof": "^2.0.0",
"later": "^1.2.0",
@@ -89,20 +91,19 @@
"loose-envify": "^1.1.0",
"make-error": "^1.2.1",
"marked": "^0.3.5",
"modular-css": "^4.1.1",
"modular-css": "^5.1.6",
"moment": "^2.13.0",
"moment-timezone": "^0.5.4",
"notifyjs": "^3.0.0",
"novnc-node": "^0.5.3",
"promise-toolbox": "^0.8.0",
"promise-toolbox": "^0.9.4",
"random-password": "^0.1.2",
"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.12.0",
"react-copy-to-clipboard": "^4.0.2",
"react-debounce-input": "^2.4.0",
"react-copy-to-clipboard": "^5.0.0",
"react-debounce-input": "^3.0.0",
"react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2",
"react-document-title": "^2.0.2",
@@ -114,7 +115,7 @@
"react-overlays": "^0.6.0",
"react-redux": "^5.0.0",
"react-router": "^3.0.0",
"react-select": "^1.0.0-rc.3",
"react-select": "^1.0.0-rc.4",
"react-shortcuts": "^1.3.1",
"react-sparklines": "^1.5.0",
"react-virtualized": "^8.0.8",
@@ -124,18 +125,19 @@
"redux-devtools-dock-monitor": "^1.1.0",
"redux-devtools-log-monitor": "^1.0.5",
"redux-thunk": "^2.0.1",
"reselect": "^2.2.1",
"reselect": "^2.5.4",
"semver": "^5.3.0",
"standard": "^8.4.0",
"styled-components": "^1.4.4",
"standard": "^10.0.0",
"styled-components": "^2.1.0",
"superagent": "^3.5.0",
"tar-stream": "^1.5.2",
"uncontrollable-input": "^0.0.0",
"uglify-es": "^3.0.18",
"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"
},

View File

@@ -1,44 +1,50 @@
import _ from 'intl'
import ActionButton from 'action-button'
import map from 'lodash/map'
import React from 'react'
import {
ButtonGroup
} from 'react-bootstrap-4/lib'
import {
noop
} from 'utils'
import propTypes from 'prop-types-decorator'
import React, { cloneElement } from 'react'
import { noop } from 'lodash'
const ActionBar = ({ actions, param }) => (
import ButtonGroup from './button-group'
export const Action = ({ display, handler, handlerParam, icon, label, pending, redirectOnSuccess }) =>
<ActionButton
handler={handler}
handlerParam={handlerParam}
icon={icon}
pending={pending}
redirectOnSuccess={redirectOnSuccess}
size='large'
tooltip={display === 'icon' ? label : undefined}
>
{display === 'both' && label}
</ActionButton>
Action.propTypes = {
display: propTypes.oneOf([ 'icon', 'both' ]),
handler: propTypes.func.isRequired,
icon: propTypes.string.isRequired,
label: propTypes.node,
pending: propTypes.bool,
redirectOnSuccess: propTypes.string
}
const ActionBar = ({ children, handlerParam = noop, display = 'both' }) =>
<ButtonGroup>
{map(actions, (button, index) => {
if (!button) {
{React.Children.map(children, (child, key) => {
if (!child) {
return
}
const { handler, handlerParam = param, label, icon, redirectOnSuccess } = button
return <ActionButton
key={index}
btnStyle='secondary'
handler={handler || noop}
handlerParam={handlerParam}
icon={icon}
redirectOnSuccess={redirectOnSuccess}
size='large'
tooltip={_(label)}
/>
const { props } = child
return cloneElement(child, {
display: props.display || display,
handlerParam: props.handlerParam || handlerParam,
key
})
})}
</ButtonGroup>
)
ActionBar.propTypes = {
actions: React.PropTypes.arrayOf(
React.PropTypes.shape({
label: React.PropTypes.string.isRequired,
icon: React.PropTypes.string.isRequired,
handler: React.PropTypes.func,
redirectOnSuccess: React.PropTypes.string
})
).isRequired,
display: React.PropTypes.oneOf(['icon', 'text', 'both'])
display: propTypes.oneOf([ 'icon', 'both' ]),
handlerParam: propTypes.any
}
export { ActionBar as default }

View File

@@ -1,38 +1,59 @@
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
}
@@ -45,18 +66,17 @@ export default class ActionButton extends Component {
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({
@@ -101,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

@@ -17,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
@@ -54,20 +54,20 @@ export default class BaseComponent extends PureComponent {
// 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 {
@@ -91,9 +91,9 @@ export default class BaseComponent extends PureComponent {
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
}

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%'
@@ -17,16 +17,16 @@ const CARD_HEADER_STYLE = {
}
export const Card = propTypes({
disableMaxHeight: propTypes.bool,
shadow: propTypes.bool
})(({
children,
shadow
}) => (
<div className='card' style={shadow ? CARD_STYLE_WITH_SHADOW : CARD_STYLE}>
{children}
</div>
))
shadow,
...props
}) => {
props.className = 'card'
props.style = shadow ? CARD_STYLE_WITH_SHADOW : CARD_STYLE
return <div {...props} />
})
export const CardHeader = propTypes({
className: propTypes.string

View File

@@ -1,8 +1,9 @@
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,
@@ -27,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,105 +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,
max: propTypes.number,
min: propTypes.number,
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}
max={props.max}
min={props.min}
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'
@@ -390,7 +390,6 @@ const MAP_TYPE_SELECT = {
}
@propTypes({
labelProp: propTypes.string.isRequired,
value: propTypes.oneOfType([
propTypes.string,
propTypes.object

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

@@ -12,9 +12,10 @@ import {
MenuItem
} from 'react-bootstrap-4/lib'
import Button from '../button'
import Component from '../base-component'
import getEventValue from '../get-event-value'
import propTypes from '../prop-types'
import propTypes from '../prop-types-decorator'
import {
firstDefined,
formatSizeRaw,
@@ -70,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}
@@ -81,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>
}

View File

@@ -4,7 +4,7 @@ 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'
@@ -15,7 +15,7 @@ import Select from './select'
multi: propTypes.bool,
onChange: propTypes.func,
options: propTypes.array,
placeholder: propTypes.string,
placeholder: propTypes.node,
predicate: propTypes.func,
required: propTypes.bool,
value: propTypes.any

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'

View File

@@ -2,11 +2,9 @@ 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'
import styles from './index.css'
import Component from '../base-component'
import Icon from '../icon'
import propTypes from '../prop-types-decorator'
@uncontrollableInput()
@propTypes({
@@ -25,28 +23,24 @@ export default class Toggle extends Component {
iconSize: 2
}
_toggle = () => {
const { props } = this
props.onChange(!props.value)
}
render () {
const { props } = this
return (
<label
<Icon
className={classNames(
props.disabled ? 'text-muted' : props.value ? 'text-success' : null,
props.className
)}
>
<Icon
icon={props.icon || (props.value ? props.iconOn : props.iconOff)}
size={props.iconSize}
/>
<input
checked={props.value || false}
className={styles.checkbox}
disabled={props.disabled}
onChange={props.onChange}
type='checkbox'
/>
</label>
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,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,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'
@@ -19,9 +19,9 @@ import {
createSelector
} from './selectors'
import {
getHostMissingPatches,
installAllHostPatches,
installAllPatchesOnPool
installAllPatchesOnPool,
subscribeHostMissingPatches
} from './xo'
// ===================================================================
@@ -64,6 +64,15 @@ const POOLS_MISSING_PATCHES_COLUMNS = [{
sortCriteria: (host, { pools }) => pools[host.$pool].name_label
}].concat(MISSING_PATCHES_COLUMNS)
// Small component to homogenize Button usage in HostsPatchesTable
const ActionButton_ = ({ children, labelId, ...props }) =>
<ActionButton
{...props}
tooltip={_(labelId)}
>
{children}
</ActionButton>
// ===================================================================
class HostsPatchesTable extends Component {
@@ -80,11 +89,25 @@ class HostsPatchesTable extends Component {
)
)
_refreshMissingPatches = () => (
Promise.all(
map(this.props.hosts, this._refreshHostMissingPatches)
_subscribeMissingPatches = (hosts = this.props.hosts) => {
const unsubs = map(hosts, host =>
subscribeHostMissingPatches(
host,
patches => this.setState({
missingPatches: {
...this.state.missingPatches,
[host.id]: patches.length
}
})
)
)
)
if (this.unsubscribeMissingPatches !== undefined) {
this.unsubscribeMissingPatches()
}
this.unsubscribeMissingPatches = () => forEach(unsubs, unsub => unsub())
}
_installAllMissingPatches = () => {
const pools = {}
@@ -95,80 +118,43 @@ class HostsPatchesTable extends Component {
return Promise.all(map(
keys(pools),
installAllPatchesOnPool
)).then(this._refreshMissingPatches)
}
_refreshHostMissingPatches = host => (
getHostMissingPatches(host).then(patches => {
this.setState({
missingPatches: {
...this.state.missingPatches,
[host.id]: patches.length
}
})
})
)
_installAllHostPatches = host => (
installAllHostPatches(host).then(() =>
this._refreshHostMissingPatches(host)
)
)
componentWillMount () {
this._refreshMissingPatches()
))
}
componentDidMount () {
// Force one Portal refresh.
// Because Portal cannot see the container reference at first rendering.
this.forceUpdate()
this._subscribeMissingPatches()
}
componentWillReceiveProps (nextProps) {
forEach(nextProps.hosts, host => {
const { id } = host
if (nextProps.hosts !== this.props.hosts) {
this._subscribeMissingPatches(nextProps.hosts)
}
}
if (this.state.missingPatches[id] !== undefined) {
return
}
this.setState({
missingPatches: {
...this.state.missingPatches,
[id]: 0
}
})
this._refreshHostMissingPatches(host)
})
componentWillUnmount () {
this.unsubscribeMissingPatches()
}
render () {
const {
buttonsGroupContainer,
container,
displayPools,
pools,
useTabButton
} = this.props
const hosts = this._getHosts()
const noPatches = isEmpty(hosts)
const { props } = this
const Container = props.container || 'div'
const Button = props.useTabButton ? TabButton : ActionButton
const Container = container || 'div'
const Buttons = (
<Container>
<Button
btnStyle='secondary'
handler={this._refreshMissingPatches}
icon='refresh'
labelId='refreshPatches'
/>
<Button
btnStyle='primary'
disabled={noPatches}
handler={this._installAllMissingPatches}
icon='host-patch-update'
labelId='installPoolPatches'
/>
</Container>
)
const Button = useTabButton
? TabButton
: ActionButton_
return (
<div>
@@ -176,17 +162,25 @@ class HostsPatchesTable extends Component {
? (
<SortedTable
collection={hosts}
columns={props.displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
columns={displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
userData={{
installAllHostPatches: this._installAllHostPatches,
installAllHostPatches,
missingPatches: this.state.missingPatches,
pools: props.pools
pools
}}
/>
) : <p>{_('patchNothing')}</p>
}
<Portal container={() => props.buttonsGroupContainer()}>
{Buttons}
<Portal container={() => buttonsGroupContainer()}>
<Container>
<Button
btnStyle='primary'
disabled={noPatches}
handler={this._installAllMissingPatches}
icon='host-patch-update'
labelId='installPoolPatches'
/>
</Container>
</Portal>
</div>
)

View File

@@ -1,21 +1,27 @@
import classNames from 'classnames'
import isInteger from 'lodash/isInteger'
import React, { PropTypes } from 'react'
import React from 'react'
const Icon = ({ className, icon, size = 1, fixedWidth }) => (
<i className={classNames(
className,
icon ? `xo-icon-${icon}` : 'fa', // Without icon prop, is a placeholder.
isInteger(size) ? `fa-${size}x` : `fa-${size}`,
fixedWidth && 'fa-fw'
)} />
)
Icon.propTypes = {
fixedWidth: PropTypes.bool,
icon: PropTypes.string,
size: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
])
import propTypes from './prop-types-decorator'
const Icon = ({ icon, size = 1, color, fixedWidth, ...props }) => {
props.className = classNames(
props.className,
icon !== undefined ? `xo-icon-${icon}` : 'fa', // Without icon prop, is a placeholder.
isInteger(size) ? `fa-${size}x` : `fa-${size}`,
color,
fixedWidth && 'fa-fw'
)
return <i {...props} />
}
propTypes(Icon)({
color: propTypes.string,
fixedWidth: propTypes.bool,
icon: propTypes.string,
size: propTypes.oneOfType([
propTypes.string,
propTypes.number
])
})
export default Icon

View File

@@ -48,6 +48,10 @@ const getMessage = (props, messageId, values, render) => {
{render}
</FormattedMessage>
}
getMessage.keyValue = (key, value) => getMessage('keyValue', {
key: <strong>{key}</strong>,
value
})
export { getMessage as default }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -627,7 +627,7 @@ export default {
editBackupReportTitle: undefined,
// Original text: 'Enable immediately after creation'
editBackupReportEnable: undefined,
editBackupScheduleEnabled: undefined,
// Original text: 'Depth'
editBackupDepthTitle: undefined,
@@ -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,

View File

@@ -726,7 +726,7 @@ export default {
editBackupReportTitle: 'Riport',
// Original text: "Enable immediately after creation"
editBackupReportEnable: 'Azonnal a létrehozás után',
editBackupScheduleEnabled: 'Azonnal a létrehozás után',
// Original text: "Depth"
editBackupDepthTitle: 'Mélység',
@@ -1806,10 +1806,10 @@ export default {
vdiRemove: 'VDI Eltávolítás',
// Original text: "Boot flag"
vdbBootableStatus: 'Boot flag',
vbdBootableStatus: 'Boot flag',
// Original text: "Status"
vdbStatus: 'Állapot',
vbdStatus: 'Állapot',
// Original text: "Connected"
vbdStatusConnected: 'Kapcsolódva',
@@ -1827,22 +1827,22 @@ export default {
vbdDisconnect: 'VBD Lecsatlakozás',
// Original text: "Bootable"
vdbBootable: 'Bootolható',
vbdBootable: 'Bootolható',
// Original text: "Readonly"
vdbReadonly: 'Csak olvasható',
vbdReadonly: 'Csak olvasható',
// Original text: 'Action'
vbdAction: undefined,
// Original text: "Create"
vdbCreate: 'Létrehozás',
vbdCreate: 'Létrehozás',
// Original text: "Disk name"
vdbNamePlaceHolder: 'Diszk név',
vbdNamePlaceHolder: 'Diszk név',
// Original text: "Size"
vdbSizePlaceHolder: 'Méret',
vbdSizePlaceHolder: 'Méret',
// Original text: "Save"
saveBootOption: 'Mentés',

File diff suppressed because it is too large Load Diff

View File

@@ -627,7 +627,7 @@ export default {
editBackupReportTitle: undefined,
// Original text: 'Enable immediately after creation'
editBackupReportEnable: undefined,
editBackupScheduleEnabled: undefined,
// Original text: 'Depth'
editBackupDepthTitle: undefined,
@@ -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

@@ -5,6 +5,8 @@ var forEach = require('lodash/forEach')
var isString = require('lodash/isString')
var messages = {
keyValue: '{key}: {value}',
statusConnecting: 'Connecting',
statusDisconnected: 'Disconnected',
statusLoading: 'Loading…',
@@ -18,11 +20,16 @@ var messages = {
// ----- Modals -----
alertOk: 'OK',
confirmOk: 'OK',
confirmCancel: 'Cancel',
genericCancel: 'Cancel',
// ----- Filters -----
onError: 'On error',
successful: 'Successful',
filterOnlyManaged: 'Managed disks',
filterOnlyOrphaned: 'Orphaned disks',
filterOnlyRegular: 'Normal disks',
filterOnlySnapshots: 'Snapshot disks',
filterOnlyUnmanaged: 'Unmanaged disks',
// ----- Copiable component -----
copyToClipboard: 'Copy to clipboard',
@@ -96,7 +103,7 @@ var messages = {
// ----- Home view ------
homeFetchingData: 'Fetching data…',
homeWelcome: 'Welcome on Xen Orchestra!',
homeWelcome: 'Welcome to Xen Orchestra!',
homeWelcomeText: 'Add your XenServer hosts or pools',
homeConnectServerText: 'Some XenServers have been registered but are not connected',
homeHelp: 'Want some help?',
@@ -122,6 +129,7 @@ var messages = {
homeAllPools: 'Pools',
homeAllHosts: 'Hosts',
homeAllTags: 'Tags',
homeAllResourceSets: 'Resource sets',
homeNewVm: 'New VM',
homeFilterNone: 'None',
homeFilterRunningHosts: 'Running hosts',
@@ -152,6 +160,9 @@ var messages = {
srSharedType: 'Shared {type}',
srNotSharedType: 'Not shared {type}',
// ----- Common components -----
sortedTableSelectedItems: '{selected, number} selected on {total, number}',
// ----- Forms -----
add: 'Add',
selectAll: 'Select all',
@@ -213,6 +224,12 @@ var messages = {
cronPattern: 'Cron Pattern:',
backupEditNotFoundTitle: 'Cannot edit backup',
backupEditNotFoundMessage: 'Missing required info for edition',
successfulJobCall: 'Successful',
failedJobCall: 'Failed',
jobCallInProgess: 'In progress',
jobTransferredDataSize: 'size:',
jobTransferredDataSpeed: 'speed:',
allJobCalls: 'All',
job: 'Job',
jobModalTitle: 'Job {job}',
jobId: 'ID',
@@ -245,7 +262,7 @@ var messages = {
noJobs: 'No jobs found.',
noSchedules: 'No schedules found',
jobActionPlaceHolder: 'Select a xo-server API command',
jobTimeoutPlaceHolder: ' Job timeout (seconds)',
jobTimeoutPlaceHolder: 'Timeout (number of seconds after which a VM is considered failed)',
jobSchedules: 'Schedules',
jobScheduleNamePlaceHolder: 'Name of your schedule',
jobScheduleJobPlaceHolder: 'Select a Job',
@@ -271,9 +288,10 @@ var messages = {
editBackupNot: 'Reverse',
editBackupTagTitle: 'Tag',
editBackupReportTitle: 'Report',
editBackupReportEnable: 'Enable immediately after creation',
editBackupScheduleEnabled: 'Automatically run as scheduled',
editBackupDepthTitle: 'Depth',
editBackupRemoteTitle: 'Remote',
deleteOldBackupsFirst: 'Delete the old backups first',
// ------ New Remote -----
remoteList: 'Remote stores for backup',
@@ -382,7 +400,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 ------
@@ -437,7 +455,13 @@ var messages = {
convertVmToTemplateLabel: 'Convert to template',
vmConsoleLabel: 'Console',
// ----- SR tabs -----
// ----- SR advanced tab -----
srUnhealthyVdiNameLabel: 'Name',
srUnhealthyVdiSize: 'Size',
srUnhealthyVdiDepth: 'Depth',
srUnhealthyVdiTitle: 'VDI to coalesce ({total, number})',
// ----- SR actions -----
srRescan: 'Rescan all disks',
srReconnectAll: 'Connect to all hosts',
@@ -461,6 +485,7 @@ var messages = {
poolHaStatus: 'High Availability',
poolHaEnabled: 'Enabled',
poolHaDisabled: 'Disabled',
setpoolMaster: 'Master',
// ----- Pool host tab -----
hostNameLabel: 'Name',
hostDescription: 'Description',
@@ -487,6 +512,10 @@ var messages = {
addSrLabel: 'Add SR',
addVmLabel: 'Add VM',
addHostLabel: 'Add Host',
hostNeedsPatchUpdate: 'This host needs to install {patches, number} patch{patches, plural, one {} other {es}} before it can be added to the pool. This operation may be long.',
hostNeedsPatchUpdateNoInstall: 'This host cannot be added to the pool because it\'s missing some patches.',
addHostErrorTitle: 'Adding host failed',
addHostNotHomogeneousErrorMessage: 'Host patches could not be homogenized.',
disconnectServer: 'Disconnect',
// ----- Host actions ------
@@ -500,7 +529,7 @@ 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.',
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 -----
@@ -525,7 +554,7 @@ var messages = {
hostCpusNumber: 'Core (socket)',
hostManufacturerinfo: 'Manufacturer info',
hostBiosinfo: 'BIOS info',
licenseHostSettingsLabel: 'Licence',
licenseHostSettingsLabel: 'License',
hostLicenseType: 'Type',
hostLicenseSocket: 'Socket',
hostLicenseExpiry: 'Expiry',
@@ -586,13 +615,17 @@ var messages = {
patchStatus: 'Status',
patchStatusApplied: 'Applied',
patchStatusNotApplied: 'Missing patches',
patchNothing: 'No patch detected',
patchNothing: 'No patches detected',
patchReleaseDate: 'Release date',
patchGuidance: 'Guidance',
patchAction: 'Action',
hostAppliedPatches: 'Applied patches',
hostMissingPatches: 'Missing patches',
hostUpToDate: 'Host up-to-date!',
installPatchWarningTitle: 'Non-recommended patch install',
installPatchWarningContent: 'This will install a patch only on this host. This is NOT the recommended way: please go into the Pool patch view and follow instructions there. If you are sure about this, you can continue anyway',
installPatchWarningReject: 'Go to pool',
installPatchWarningResolve: 'Install',
// ----- Pool patch tabs -----
refreshPatches: 'Refresh patches',
installPoolPatches: 'Install pool patches',
@@ -622,6 +655,7 @@ var messages = {
vmSettings: 'Started {ago}',
vmCurrentStatus: 'Current status:',
vmNotRunning: 'Not running',
vmHaltedSince: 'Halted {ago}',
// ----- VM general tab -----
noToolsDetected: 'No Xen tools detected',
@@ -646,7 +680,6 @@ var messages = {
copyToClipboardLabel: 'Copy',
ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
tipLabel: 'Tip:',
tipConsoleLabel: 'Due to a XenServer issue, non-US keyboard layouts aren\'t well supported. Switch your own layout to US to workaround it.',
hideHeaderTooltip: 'Hide infos',
showHeaderTooltip: 'Show infos',
@@ -670,6 +703,8 @@ var messages = {
vdiBootOrder: 'Boot order',
vdiNameLabel: 'Name',
vdiNameDescription: 'Description',
vdiPool: 'Pool',
vdiDisconnect: 'Disconnect',
vdiTags: 'Tags',
vdiSize: 'Size',
vdiSr: 'SR',
@@ -681,23 +716,26 @@ var messages = {
vdiMigrateNoSrMessage: 'A target SR is required to migrate a VDI',
vdiForget: 'Forget',
vdiRemove: 'Remove VDI',
vdbBootableStatus: 'Boot flag',
vdbStatus: 'Status',
noControlDomainVdis: 'No VDIs attached to Control Domain',
vbdBootableStatus: 'Boot flag',
vbdStatus: 'Status',
vbdStatusConnected: 'Connected',
vbdStatusDisconnected: 'Disconnected',
vbdNoVbd: 'No disks',
vbdConnect: 'Connect VBD',
vbdDisconnect: 'Disconnect VBD',
vdbBootable: 'Bootable',
vdbReadonly: 'Readonly',
vbdBootable: 'Bootable',
vbdReadonly: 'Readonly',
vbdAction: 'Action',
vdbCreate: 'Create',
vdbNamePlaceHolder: 'Disk name',
vdbSizePlaceHolder: 'Size',
vbdCreate: 'Create',
vbdNamePlaceHolder: 'Disk name',
vbdSizePlaceHolder: 'Size',
cdDriveNotInstalled: 'CD drive not completely installed',
cdDriveInstallation: 'Stop and start the VM to install the CD drive',
saveBootOption: 'Save',
resetBootOption: 'Reset',
deleteSelectedVdis: 'Delete selected VDIs',
deleteSelectedVdi: 'Delete selected VDI',
// ----- VM network tab -----
vifCreateDeviceButton: 'New device',
@@ -767,6 +805,8 @@ var messages = {
autoPowerOn: 'Auto power on',
ha: 'HA',
vmAffinityHost: 'Affinity host',
vmVga: 'VGA',
vmVideoram: 'Video RAM',
noAffinityHost: 'None',
originalTemplate: 'Original template',
unknownOsName: 'Unknown',
@@ -774,6 +814,11 @@ var messages = {
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:',
@@ -815,7 +860,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',
@@ -840,6 +885,7 @@ var messages = {
orphanedVms: 'Orphaned VMs snapshot',
noOrphanedObject: 'No orphans',
removeAllOrphanedObject: 'Remove all orphaned snapshot VDIs',
vdisOnControlDomain: 'VDIs attached to Control Domain',
vmNameLabel: 'Name',
vmNameDescription: 'Description',
vmContainer: 'Resident on',
@@ -854,7 +900,6 @@ var messages = {
// ----- New VM -----
newVmCreateNewVmOn: 'Create a new VM on {select}',
newVmCreateNewVmOn2: 'Create a new VM on {select1} or {select2}',
newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
newVmInfoPanel: 'Infos',
newVmNameLabel: 'Name',
@@ -894,7 +939,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:',
@@ -975,6 +1020,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',
@@ -984,6 +1030,10 @@ var messages = {
availableBackupsColumn: 'Available Backups',
backupRestoreErrorTitle: 'Missing parameters',
backupRestoreErrorMessage: 'Choose a SR and a backup',
backupRestoreSelectDefaultSr: 'Select default SR…',
backupRestoreChooseSrForEachVdis: 'Choose a SR for each VDI',
backupRestoreVdiLabel: 'VDI',
backupRestoreSrLabel: 'SR',
displayBackup: 'Display backups',
importBackupTitle: 'Import VM',
importBackupMessage: 'Starting your backup import',
@@ -1008,7 +1058,7 @@ var messages = {
// ----- 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',
@@ -1016,44 +1066,57 @@ var messages = {
restartHostModalTitle: 'Restart host',
restartHostModalMessage: 'This will restart your host. Do you want to continue?',
restartHostsAgentsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}',
restartHostsAgentsModalMessage: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?',
restartHostsAgentsModalMessage: 'Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?',
restartHostsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}}',
restartHostsModalMessage: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}}?',
restartHostsModalMessage: 'Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}}?',
startVmsModalTitle: 'Start VM{vms, plural, one {} other {s}}',
startVmsModalMessage: 'Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?',
cloneAndStartVM: 'Start a copy',
forceStartVm: 'Force start',
forceStartVmModalTitle: 'Forbidden operation',
blockedStartVmModalMessage: 'Start operation for this vm is blocked.',
blockedStartVmsModalMessage: 'Forbidden operation start for {nVms, number} vm{nVms, plural, one {} other {s}}.',
startVmsModalMessage: 'Are you sure you want to start {vms, number} VM{vms, plural, one {} other {s}}?',
failedVmsErrorMessage: '{nVms, number} vm{nVms, plural, one {} other {s}} are failed. Please see your logs to get more information',
failedVmsErrorTitle: 'Start failed',
stopHostsModalTitle: 'Stop Host{nHosts, plural, one {} other {s}}',
stopHostsModalMessage: 'Are you sure you want to stop {nHosts} Host{nHosts, plural, one {} other {s}}?',
stopHostsModalMessage: 'Are you sure you want to stop {nHosts, number} Host{nHosts, plural, one {} other {s}}?',
stopVmsModalTitle: 'Stop VM{vms, plural, one {} other {s}}',
stopVmsModalMessage: 'Are you sure you want to stop {vms} VM{vms, plural, one {} other {s}}?',
stopVmsModalMessage: 'Are you sure you want to stop {vms, number} VM{vms, plural, one {} other {s}}?',
restartVmModalTitle: 'Restart VM',
restartVmModalMessage: 'Are you sure you want to restart {name}?',
stopVmModalTitle: 'Stop VM',
stopVmModalMessage: 'Are you sure you want to stop {name}?',
restartVmsModalTitle: 'Restart VM{vms, plural, one {} other {s}}',
restartVmsModalMessage: 'Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?',
restartVmsModalMessage: 'Are you sure you want to restart {vms, number} VM{vms, plural, one {} other {s}}?',
snapshotVmsModalTitle: 'Snapshot VM{vms, plural, one {} other {s}}',
snapshotVmsModalMessage: 'Are you sure you want to snapshot {vms} VM{vms, plural, one {} other {s}}?',
snapshotVmsModalMessage: 'Are you sure you want to snapshot {vms, number} VM{vms, plural, one {} other {s}}?',
deleteVmsModalTitle: 'Delete VM{vms, plural, one {} other {s}}',
deleteVmsModalMessage: 'Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
deleteVmsModalMessage: 'Are you sure you want to delete {vms, number} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
deleteVmModalTitle: 'Delete VM',
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
migrateVmModalTitle: 'Migrate VM',
migrateVmSelectHost: 'Select a destination host:',
migrateVmSelectMigrationNetwork: 'Select a migration network:',
migrateVmSelectSrs: 'For each VDI, select an SR:',
migrateVmSelectNetworks: 'For each VIF, select a network:',
migrateVmsSelectSr: 'Select a destination SR:',
migrateVmsSelectSrIntraPool: 'Select a destination SR for local disks:',
migrateVmsSelectNetwork: 'Select a network on which to connect each VIF:',
migrateVmsSmartMapping: 'Smart mapping',
migrateVmName: 'Name',
migrateVmSr: 'SR',
migrateVmVif: 'VIF',
migrateVmNetwork: 'Network',
migrateVmNoTargetHost: 'No target host',
migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
migrateVmNoDefaultSrError: 'No default SR',
migrateVmNotConnectedDefaultSrError: 'Default SR not connected to host',
chooseSrForEachVdisModalSelectSr: 'For each VDI, select an SR:',
chooseSrForEachVdisModalMainSr: 'Select main SR…',
chooseSrForEachVdisModalVdiLabel: 'VDI',
chooseSrForEachVdisModalSrLabel: 'SR*',
chooseSrForEachVdisModalOptionalEntry: '* optional',
deleteVdiModalTitle: 'Delete VDI',
deleteVdiModalMessage: 'Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST',
deleteVdisModalTitle: 'Delete VDI{nVdis, plural, one {} other {s}}',
deleteVdisModalMessage: 'Are you sure you want to delete {nVdis, number} disk{nVdis, plural, one {} other {s}}? ALL DATA ON THESE DISKS WILL BE LOST',
revertVmModalTitle: 'Revert your VM',
deleteSnapshotModalTitle: 'Delete snapshot',
deleteSnapshotModalMessage: 'Are you sure you want to delete this snapshot?',
@@ -1082,6 +1145,9 @@ var messages = {
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',
@@ -1091,12 +1157,14 @@ var messages = {
serverError: 'Error',
serverAddFailed: 'Adding server failed',
serverStatus: 'Status',
serverConnectionFailed: 'Connection failed',
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',
@@ -1115,6 +1183,16 @@ var messages = {
detachHostModalMessage: 'Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST.',
detachHost: 'Detach',
// ----- Forget host -----
forgetHostModalTitle: 'Forget host',
forgetHostModalMessage: 'Are you sure you want to forget {host} from its pool? Be sure this host can\'t be back online, or use detach instead.',
forgetHost: 'Forget',
// ----- Set pool master -----
setPoolMasterModalTitle: 'Designate a new master',
setPoolMasterModalMessage: 'This operation may take several minutes. Do you want to continue?',
// ----- Network -----
newNetworkCreate: 'Create network',
newBondedNetworkCreate: 'Create bonded network',
@@ -1184,7 +1262,7 @@ var messages = {
refresh: 'Refresh',
upgrade: 'Upgrade',
noUpdaterCommunity: 'No updater available for Community Edition',
considerSubscribe: 'Please consider subscribe and try it with all features for free during 15 days on {link}.',
considerSubscribe: 'Please consider subscribing and trying it with all the features for free during 15 days on {link}.',
noUpdaterWarning: 'Manual update could break your current installation due to dependencies issues, do it with caution',
currentVersion: 'Current version:',
register: 'Register',
@@ -1340,12 +1418,16 @@ var messages = {
xosanSuggestions: 'Suggestions',
xosanName: 'Name',
xosanHost: 'Host',
xosanHosts: 'Hosts',
xosanHosts: 'Connected Hosts',
xosanPool: 'Pool',
xosanVolumeId: 'Volume ID',
xosanSize: 'Size',
xosanUsedSpace: 'Used space',
xosanNeedPack: 'XOSAN pack needs to be installed on each host of the pool.',
xosanInstallIt: 'Install it now!',
xosanNeedRestart: 'Some hosts need their toolstack to be restarted before you can create an XOSAN',
xosanRestartAgents: 'Restart toolstacks',
xosanMasterOffline: 'Pool master is not running',
xosanInstallPackTitle: 'Install XOSAN pack on {pool}',
xosanSelect2Srs: 'Select at least 2 SRs',
xosanLayout: 'Layout',
@@ -1354,8 +1436,15 @@ var messages = {
xosanAvailableSpace: 'Available space',
xosanDiskLossLegend: '* Can fail without data loss',
xosanCreate: 'Create',
xosanAdd: 'Add',
xosanInstalling: 'Installing XOSAN. Please wait...',
xosanCommunity: 'No XOSAN available for Community Edition',
xosanNew: 'New',
xosanAdvanced: 'Advanced',
xosanRemoveSubvolumes: 'Remove subvolumes',
xosanAddSubvolume: 'Add subvolume',
xosanWarning: 'This version of XOSAN SR is from the first beta phase. You can keep using it, but to modify it you\'ll have to save your disks and re-create it.',
xosanVlan: 'VLAN',
// Pack download modal
xosanInstallCloudPlugin: 'Install cloud plugin first',
xosanLoadCloudPlugin: 'Load cloud plugin first',
@@ -1366,8 +1455,38 @@ var messages = {
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:'
xosanPackRequirements: 'At least one of these version requirements must be satisfied by all the hosts in this pool:',
// SR tab XOSAN
xosanVmsNotRunning: 'Some XOSAN Virtual Machines are not running',
xosanVmsNotFound: 'Some XOSAN Virtual Machines could not be found',
xosanFilesNeedHealing: 'Some XOSAN Virtual Machines have files needing healing',
xosanVm: 'VM controller',
xosanUnderlyingStorage: 'SR',
xosanReplace: 'Replace',
xosanOnSameVm: 'On same VM',
xosanBrickUuid: 'Brick UUID',
xosanBrickSize: 'Brick size',
xosanMemorySize: 'Memory size',
xosanStatus: 'Status',
xosanArbiter: 'Arbiter',
xosanUsedInodes: 'Used Inodes',
xosanBlockSize: 'Block size',
xosanDevice: 'Device',
xosanFsName: 'FS name',
xosanMountOptions: 'Mount options',
xosanPath: 'Path',
xosanJob: 'Job',
xosanPid: 'PID',
xosanPort: 'Port',
xosanReplaceBrickErrorTitle: 'Missing values',
xosanReplaceBrickErrorMessage: 'You need to select a SR and a size',
xosanAddSubvolumeErrorTitle: 'Bad values',
xosanAddSubvolumeErrorMessage: 'You need to select {nSrs, number} and a size',
xosanSelectNSrs: 'Select {nSrs, number} SRs',
xosanRun: 'Run',
xosanRemove: 'Remove',
xosanVolume: 'Volume',
xosanVolumeOptions: 'Volume options'
}
forEach(messages, function (message, id) {
if (isString(message)) {

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

@@ -4,7 +4,7 @@ import _ from 'intl'
import ActionButton from './action-button'
import Component from './base-component'
import Icon from 'icon'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
import Tooltip from 'tooltip'
import { alert } from 'modal'
import { connectStore } from './utils'
@@ -55,8 +55,9 @@ export default class IsoDevice extends Component {
const samePool = vmPool === sr.$pool
return (
samePool && (vmRunning ? sr.shared || sameHost : true) &&
sr.SR_type === 'iso' || sr.SR_type === 'udev' && sr.size
samePool &&
(vmRunning ? sr.shared || sameHost : true) &&
(sr.SR_type === 'iso' || (sr.SR_type === 'udev' && sr.size))
)
}
)
@@ -87,7 +88,6 @@ export default class IsoDevice extends Component {
/>
<span className='input-group-btn'>
<ActionButton
btnStyle='secondary'
disabled={!mountedIso}
handler={this._handleEject}
icon='vm-eject'

View File

@@ -3,8 +3,9 @@ import uncontrollableInput from 'uncontrollable-input'
import { filter, map } 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 { EMPTY_ARRAY } from '../utils'
import GenericInput from './generic-input'
@@ -96,26 +97,26 @@ export default class ObjectInput extends Component {
uiSchema={itemUiSchema}
value={value}
/>
<button
className='btn btn-danger pull-right'
<Button
btnStyle='danger'
className='pull-right'
disabled={disabled}
name={key}
onClick={() => this._onRemoveItem(key)}
type='button'
>
{_('remove')}
</button>
</Button>
</li>
)}
</ul>
<button
className='btn btn-primary pull-right mt-1 mr-1'
<Button
btnStyle='primary'
className='pull-right mt-1 mr-1'
disabled={disabled}
onClick={this._onAddItem}
type='button'
>
{_('add')}
</button>
</Button>
</div>}
</div>
)

View File

@@ -1,7 +1,7 @@
import React, { Component } from 'react'
import getEventValue from '../get-event-value'
import propTypes from '../prop-types'
import propTypes from '../prop-types-decorator'
import uncontrollableInput from 'uncontrollable-input'
import { EMPTY_OBJECT } from '../utils'

View File

@@ -5,7 +5,7 @@ import { keyBy, map } from 'lodash'
import _ from '../intl'
import Component from '../base-component'
import propTypes from '../prop-types'
import propTypes from '../prop-types-decorator'
import { EMPTY_OBJECT } from '../utils'
import GenericInput from './generic-input'
@@ -57,7 +57,7 @@ export default class ObjectInput extends Component {
} = this
const childDepth = depth + 2
const properties = uiSchema && uiSchema.properties || EMPTY_OBJECT
const properties = (uiSchema != null && uiSchema.properties) || EMPTY_OBJECT
const requiredProps = this._getRequiredProps()
return (

View File

@@ -1,9 +1,10 @@
import React from 'react'
import uncontrollableInput from 'uncontrollable-input'
import Combobox from '../combobox'
import Component from '../base-component'
import propTypes from '../prop-types'
import getEventValue from '../get-event-value'
import propTypes from '../prop-types-decorator'
import { PrimitiveInputWrapper } from './helpers'
@@ -14,23 +15,30 @@ import { PrimitiveInputWrapper } from './helpers'
})
@uncontrollableInput()
export default class StringInput extends Component {
// the value of this input is undefined not '' when empty to make
// it homogenous with when the user has never touched this input
_onChange = event => {
const value = getEventValue(event)
this.props.onChange(value !== '' ? value : undefined)
}
render () {
const { required, schema } = this.props
const {
disabled,
onChange,
password,
placeholder = schema.default,
value,
...props
} = this.props
delete props.onChange
return (
<PrimitiveInputWrapper {...props}>
<Combobox
value={value || ''}
value={value !== undefined ? value : ''}
disabled={disabled}
onChange={onChange}
onChange={this._onChange}
options={schema.defaults}
placeholder={placeholder || schema.default}
required={required}

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'
// ===================================================================
@@ -44,11 +44,21 @@ export class BlockLink extends Component {
}
}
_addAuxClickListener = ref => {
// FIXME: when https://github.com/facebook/react/issues/8529 is fixed,
// remove and use onAuxClickCapture.
// In Chrome ^55, middle-clicking triggers auxclick event instead of click
if (ref !== null) {
ref.addEventListener('auxclick', this._onClickCapture)
}
}
render () {
const { children, tagName = 'div' } = this.props
const Component = tagName
return (
<Component
ref={this._addAuxClickListener}
style={this._style}
onClickCapture={this._onClickCapture}
>

View File

@@ -1,11 +1,14 @@
import _ from 'intl'
import Icon from 'icon'
import isArray from 'lodash/isArray'
import isString from 'lodash/isString'
import map from 'lodash/map'
import React, { Component, cloneElement } from 'react'
import { Button, Modal as ReactModal } from 'react-bootstrap-4/lib'
import { Modal as ReactModal } from 'react-bootstrap-4/lib'
import propTypes from './prop-types'
import _ from './intl'
import Button from './button'
import Icon from './icon'
import propTypes from './prop-types-decorator'
import Tooltip from './tooltip'
import {
disable as disableShortcuts,
enable as enableShortcuts
@@ -22,28 +25,108 @@ const modal = (content, onClose) => {
instance.setState({ content, onClose, showModal: true }, disableShortcuts)
}
export const alert = (title, body) => {
return new Promise(resolve => {
const { Body, Footer, Header, Title } = ReactModal
modal(
<div>
<Header closeButton>
<Title>{title}</Title>
</Header>
<Body>{body}</Body>
<Footer>
<Button bsStyle='primary' onClick={() => {
resolve()
instance.close()
}}>
{_('alertOk')}
@propTypes({
buttons: propTypes.arrayOf(propTypes.shape({
btnStyle: propTypes.string,
icon: propTypes.string,
label: propTypes.node.isRequired,
tooltip: propTypes.node,
value: propTypes.any
})).isRequired,
children: propTypes.node.isRequired,
icon: propTypes.string,
title: propTypes.node.isRequired
})
class GenericModal extends Component {
_getBodyValue = () => {
const { body } = this.refs
if (body !== undefined) {
return body.getWrappedInstance === undefined
? body.value
: body.getWrappedInstance().value
}
}
_resolve = (value = this._getBodyValue()) => {
this.props.resolve(value)
instance.close()
}
_reject = () => {
this.props.reject()
instance.close()
}
render () {
const {
buttons,
icon,
title
} = this.props
const body = _addRef(this.props.children, 'body')
return <div>
<ReactModal.Header closeButton>
<ReactModal.Title>
{icon
? <span><Icon icon={icon} /> {title}</span>
: title
}
</ReactModal.Title>
</ReactModal.Header>
<ReactModal.Body>
{body}
</ReactModal.Body>
<ReactModal.Footer>
{map(buttons, ({
label,
tooltip,
value,
icon,
...props
}, key) => {
const button = <Button
onClick={() => this._resolve(value)}
{...props}
>
{icon !== undefined && <Icon icon={icon} fixedWidth />}
{label}
</Button>
</Footer>
</div>,
return <span key={key}>
{tooltip !== undefined
? <Tooltip content={tooltip}>{button}</Tooltip>
: button
}
{' '}
</span>
})}
{this.props.reject !== undefined &&
<Button onClick={this._reject} >
{_('genericCancel')}
</Button>
}
</ReactModal.Footer>
</div>
}
}
const ALERT_BUTTONS = [ { label: _('alertOk'), value: 'ok' } ]
export const alert = (title, body) => (
new Promise(resolve => {
modal(
<GenericModal
buttons={ALERT_BUTTONS}
resolve={resolve}
title={title}
>
{body}
</GenericModal>,
resolve
)
})
}
)
const _addRef = (component, ref) => {
if (isString(component) || isArray(component)) {
@@ -56,79 +139,38 @@ const _addRef = (component, ref) => {
return component
}
@propTypes({
children: propTypes.node.isRequired,
title: propTypes.node.isRequired,
icon: propTypes.string
})
class Confirm extends Component {
_resolve = () => {
const { body } = this.refs
this.props.resolve(body && (body.getWrappedInstance
? body.getWrappedInstance().value
: body.value
))
instance.close()
}
_reject = () => {
this.props.reject()
instance.close()
}
_style = { marginRight: '0.5em' }
render () {
const { Body, Footer, Header, Title } = ReactModal
const { title, icon } = this.props
const body = _addRef(this.props.children, 'body')
return <div>
<Header closeButton>
<Title>
{icon
? <span><Icon icon={icon} /> {title}</span>
: title
}
</Title>
</Header>
<Body>
{body}
</Body>
<Footer>
<Button
bsStyle='primary'
onClick={this._resolve}
style={this._style}
>
{_('confirmOk')}
</Button>
<Button
bsStyle='secondary'
onClick={this._reject}
>
{_('confirmCancel')}
</Button>
</Footer>
</div>
}
}
const CONFIRM_BUTTONS = [ { btnStyle: 'primary', label: _('confirmOk') } ]
export const confirm = ({
body,
title,
icon = 'alarm'
icon = 'alarm',
title
}) => (
chooseAction({
body,
buttons: CONFIRM_BUTTONS,
icon,
title
})
)
export const chooseAction = ({
body,
buttons,
icon,
title
}) => {
return new Promise((resolve, reject) => {
modal(
<Confirm
title={title}
resolve={resolve}
reject={reject}
<GenericModal
buttons={buttons}
icon={icon}
reject={reject}
resolve={resolve}
title={title}
>
{body}
</Confirm>,
</GenericModal>,
reject
)
})
@@ -164,14 +206,8 @@ export default class Modal extends Component {
}
render () {
const { showModal } = this.state
/* TODO: remove this work-around and use
* ReactModal.Body, ReactModal.Header, ...
* after this issue has been fixed:
* https://phabricator.babeljs.io/T6976
*/
return (
<ReactModal show={showModal} onHide={this._onHide}>
<ReactModal show={this.state.showModal} onHide={this._onHide}>
{this.state.content}
</ReactModal>
)

30
src/common/no-objects.js Normal file
View File

@@ -0,0 +1,30 @@
import React from 'react'
import { isEmpty } from 'lodash'
import propTypes from './prop-types-decorator'
// This component returns :
// - A loading icon when the objects are not fetched
// - A default message if the objects are fetched and the collection is empty
// - The children if the objects are fetched and the collection is not empty
//
// ```js
// <NoObjects collection={collection} emptyMessage={message}>
// {children}
// </NoObjects>
// ````
const NoObjects = ({ children, collection, emptyMessage }) => collection == null
? <img src='assets/loading.svg' alt='loading' />
: isEmpty(collection)
? <p>{emptyMessage}</p>
: <div>{children}</div>
propTypes(NoObjects)({
children: propTypes.node.isRequired,
collection: propTypes.oneOfType([
propTypes.array,
propTypes.object
]).isRequired,
emptyMessage: propTypes.node.isRequired
})
export default NoObjects

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

@@ -1,14 +1,13 @@
import React, { Component } from 'react'
import RFB from '@nraynaud/novnc/lib/rfb'
import { createBackoff } from 'jsonrpc-websocket-client'
import { RFB } from 'novnc-node'
import {
format as formatUrl,
parse as parseUrl,
resolve as resolveUrl
} from 'url'
import { enable as enableShortcuts, disable as disableShortcuts } from 'shortcuts'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
const parseRelativeUrl = url => parseUrl(resolveUrl(String(window.location), url))
@@ -42,7 +41,7 @@ export default class NoVnc extends Component {
}
}
if (state !== 'disconnected') {
if (state !== 'disconnected' || this.refs.canvas == null) {
return
}
@@ -91,14 +90,23 @@ export default class NoVnc extends Component {
const rfb = this._rfb = new RFB({
encrypt: isSecure,
target: this.refs.canvas,
wsProtocols: [ 'chat' ],
onClipboard: onClipboardChange && ((_, text) => {
onClipboardChange(text)
}),
onUpdateState: this._onUpdateState
})
rfb.connect(formatUrl(url))
// remove leading slashes from the path
//
// a leading slassh will be added by noVNC
const clippedPath = url.path.replace(/^\/+/, '')
// a port is required
//
// if not available from the URL, use the default ones
const port = url.port || (isSecure ? 443 : 80)
rfb.connect(url.hostname, port, null, clippedPath)
disableShortcuts()
}

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} ({pif.deviceName})
<Icon icon='network' color={pif.carrier ? 'text-success' : 'text-danger'} /> {pif.device} ({pif.deviceName})
</span>
),
@@ -222,7 +222,9 @@ const GenericXoItem = connectStore(() => {
})
})(({ xoItem, ...props }) => xoItem
? renderXoItem(xoItem, props)
: <span className='text-muted'>{_('errorNoSuchItem')}</span>
: renderXoUnknownItem()
)
export const renderXoItemFromId = (id, props) => <GenericXoItem {...props} id={id} />
export const renderXoUnknownItem = () => <span className='text-muted'>{_('errorNoSuchItem')}</span>

View File

@@ -1,9 +1,6 @@
import classNames from 'classnames'
import Icon from 'icon'
import later from 'later'
import React from 'react'
import Tooltip from 'tooltip'
import { Toggle } from 'form'
import { FormattedDate, FormattedTime } from 'react-intl'
import {
forEach,
@@ -14,12 +11,15 @@ import {
} 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'
// ===================================================================
@@ -259,9 +259,12 @@ class TableSelect extends Component {
))}
</tbody>
</table>
<button className='btn btn-secondary pull-right' onClick={this._reset}>
<Button
className='pull-right'
onClick={this._reset}
>
{_(`selectTableAll${labelId}`)} {value && !value.length && <Icon icon='success' />}
</button>
</Button>
</div>
}
}
@@ -447,23 +450,27 @@ class DayPicker extends Component {
// ===================================================================
@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 {
constructor (props) {
super(props)
this._onCronChange = newCrons => {
const cronPattern = this.props.cronPattern.split(' ')
const cronPattern = this._getCronPattern().split(' ')
forEach(newCrons, (cron, unit) => {
cronPattern[PICKTIME_TO_ID[unit]] = cron
})
this.props.onChange({
cronPattern: cronPattern.join(' '),
timezone: this.props.timezone
timezone: this._getTimezone()
})
}
@@ -475,17 +482,24 @@ export default class Scheduler extends Component {
_onTimezoneChange = timezone => {
this.props.onChange({
cronPattern: this.props.cronPattern,
cronPattern: this._getCronPattern(),
timezone
})
}
_getCronPattern = () => {
const { value, cronPattern = value.cronPattern } = this.props
return cronPattern
}
_getTimezone = () => {
const { value, timezone = value && value.timezone } = this.props
return timezone
}
render () {
const {
cronPattern,
timezone
} = this.props
const cronPatternArr = cronPattern.split(' ')
const cronPatternArr = this._getCronPattern().split(' ')
const timezone = this._getTimezone()
return (
<div className='card-block'>

View File

@@ -1,7 +1,7 @@
import _ from 'intl'
import Component from 'base-component'
import Icon from 'icon'
import propTypes from 'prop-types'
import propTypes from 'prop-types-decorator'
import React from 'react'
import { omit } from 'lodash'

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 uncontrollableInput from 'uncontrollable-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,
@@ -150,19 +150,17 @@ export class GenericSelect extends Component {
(containers, objects) => { // createCollectionWrapper with a depth?
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,
@@ -173,11 +171,13 @@ export class GenericSelect extends Component {
options.push(getOption(object, container))
})
})
const values = this._getSelectValue()
const objectsById = this._getObjectsById()
forEach(values, val => {
if (!objectsById[val]) {
const addIfMissing = val => {
if (val && !objectsById[val]) {
options.push({
disabled: true,
id: val,
label: val,
value: val,
@@ -187,7 +187,14 @@ export class GenericSelect extends Component {
}
})
}
})
}
if (isArray(values)) {
forEach(values, addIfMissing)
} else {
addIfMissing(values)
}
return options
}
)
@@ -271,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>

View File

@@ -1,18 +1,21 @@
import add from 'lodash/add'
import checkPermissions from 'xo-acl-resolver'
import filter from 'lodash/filter'
import find from 'lodash/find'
import forEach from 'lodash/forEach'
import groupBy from 'lodash/groupBy'
import isArray from 'lodash/isArray'
import isArrayLike from 'lodash/isArrayLike'
import isFunction from 'lodash/isFunction'
import keys from 'lodash/keys'
import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
import pickBy from 'lodash/pickBy'
import size from 'lodash/size'
import slice from 'lodash/slice'
import { createSelector as create } from 'reselect'
import {
filter,
find,
forEach,
groupBy,
isArray,
isArrayLike,
isFunction,
keys,
map,
orderBy,
pickBy,
size,
slice
} from 'lodash'
import invoke from './invoke'
import shallowEqual from './shallow-equal'
@@ -126,9 +129,9 @@ export const createCounter = (collection, predicate) =>
//
// Should only be used with a reasonable number of properties.
export const createPicker = (object, props) =>
_createCollectionWrapper(
_create2(
object, props,
_create2(
object, props,
_createCollectionWrapper(
(object, props) => {
const values = {}
forEach(props, prop => {
@@ -191,6 +194,13 @@ export const createSort = (
order = 'asc'
) => _create2(collection, getter, order, orderBy)
export const createSumBy = (itemsSelector, iterateeSelector) =>
_create2(
itemsSelector,
iterateeSelector,
(items, iteratee) => map(items, iteratee).reduce(add, 0)
)
export const createTop = (collection, iteratee, n) =>
_create2(
collection,
@@ -347,7 +357,7 @@ export const createSortForType = invoke(() => {
return (type, collection) => createSort(
collection,
autoSelector(type, getIteratees),
autoSelector(type, getOrders),
autoSelector(type, getOrders)
)
})
@@ -448,6 +458,24 @@ export const createGetTags = collectionSelectors => {
return _extendCollectionSelector(getTags, 'tag')
}
export const createGetVmLastShutdownTime = (getVmId = (_, {vm}) => vm != null ? vm.id : undefined) => create(
getVmId,
createGetObjectsOfType('message'),
(vmId, messages) => {
let max = null
forEach(messages, message => {
if (
message.$object === vmId &&
message.name === 'VM_SHUTDOWN' &&
(max === null || message.time > max)
) {
max = message.time
}
})
return max
}
)
export const createGetObjectMessages = objectSelector =>
createGetObjectsOfType('message').filter(
create(
@@ -463,9 +491,10 @@ export const createGetObjectMessages = objectSelector =>
export const getObject = createGetObject((_, id) => id)
export const createDoesHostNeedRestart = hostSelector => {
// Returns the first patch of the host which requires it to be
// restarted.
const restartPoolPatch = createGetObjectsOfType('pool_patch').pick(
// XS < 7.1
const patchRequiresReboot = createGetObjectsOfType('pool_patch').pick(
// Returns the first patch of the host which requires it to be
// restarted.
create(
createGetObjectsOfType('host_patch').pick(
(state, props) => {
@@ -485,7 +514,11 @@ export const createDoesHostNeedRestart = hostSelector => {
action === 'restartHost' || action === 'restartXapi'
) ])
return (state, props) => restartPoolPatch(state, props) !== undefined
return create(
hostSelector,
(...args) => args,
(host, args) => host.rebootRequired || !!patchRequiresReboot(...args)
)
}
export const createGetHostMetrics = hostSelector =>
@@ -509,3 +542,17 @@ export const createGetHostMetrics = hostSelector =>
}
)
)
export const createGetVmDisks = vmSelector =>
createGetObjectsOfType('VDI').pick(
create(
createGetObjectsOfType('VBD').pick(
(state, props) => vmSelector(state, props).$VBDs
),
_createCollectionWrapper(vbds => map(vbds, vbd =>
vbd.is_cd_drive
? undefined
: vbd.VDI
))
)
)

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

@@ -1,20 +1,30 @@
import _ from 'intl'
import ceil from 'lodash/ceil'
import classNames from 'classnames'
import debounce from 'lodash/debounce'
import findIndex from 'lodash/findIndex'
import isEmpty from 'lodash/isEmpty'
import isFunction from 'lodash/isFunction'
import map from 'lodash/map'
import React from 'react'
import { Dropdown, MenuItem, Pagination } from 'react-bootstrap-4/lib'
import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
import React from 'react'
import { Portal } from 'react-overlays'
import { Set } from 'immutable'
import {
Dropdown,
MenuItem,
Pagination
} from 'react-bootstrap-4/lib'
import {
ceil,
debounce,
findIndex,
isEmpty,
isFunction,
map
} from 'lodash'
import ActionRowButton from 'action-row-button'
import Button from '../button'
import ButtonGroup from '../button-group'
import Component from '../base-component'
import 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'
@@ -32,6 +42,7 @@ import styles from './index.css'
// ===================================================================
@propTypes({
defaultFilter: propTypes.string,
filters: propTypes.object,
nFilteredItems: propTypes.number.isRequired,
nItems: propTypes.number.isRequired,
@@ -74,15 +85,15 @@ class TableFilter extends Component {
</Dropdown>
</div>}
<input
type='text'
ref='filter'
onChange={this._onChange}
className='form-control'
defaultValue={props.defaultFilter}
onChange={this._onChange}
ref='filter'
/>
<div className='input-group-btn'>
<button className='btn btn-secondary' onClick={this._cleanFilter}>
<Button onClick={this._cleanFilter}>
<Icon icon='clear-search' />
</button>
</Button>
</div>
</div>
)
@@ -132,26 +143,65 @@ class ColumnHead extends Component {
// ===================================================================
@propTypes({
indeterminate: propTypes.bool.isRequired
})
class Checkbox extends Component {
componentDidUpdate () {
const { props: { indeterminate }, ref } = this
if (ref !== null) {
ref.indeterminate = indeterminate
}
}
_ref = ref => {
this.ref = ref
this.componentDidUpdate()
}
render () {
const { indeterminate, ...props } = this.props
props.ref = this._ref
props.type = 'checkbox'
return <input {...props} />
}
}
// ===================================================================
const DEFAULT_ITEMS_PER_PAGE = 10
const actionsShape = propTypes.arrayOf(propTypes.shape({
// groupedActions: the function will be called with an array of the selected items` ids in parameters
// individualActions: the function will be called with the related item's id in parameters
handler: propTypes.func.isRequired,
icon: propTypes.string.isRequired,
label: propTypes.node.isRequired,
level: propTypes.oneOf([ 'warning', 'danger' ])
}))
@propTypes({
defaultColumn: propTypes.number,
defaultFilter: propTypes.string,
collection: propTypes.oneOfType([
propTypes.array,
propTypes.object
]).isRequired,
columns: propTypes.arrayOf(propTypes.shape({
component: propTypes.func,
default: propTypes.bool,
name: propTypes.node,
itemRenderer: propTypes.func.isRequired,
itemRenderer: propTypes.func,
sortCriteria: propTypes.oneOfType([
propTypes.func,
propTypes.string
]),
sortOrder: propTypes.string
sortOrder: propTypes.string,
textAlign: propTypes.string
})).isRequired,
filterContainer: propTypes.func,
filters: propTypes.object,
groupedActions: actionsShape,
individualActions: actionsShape,
itemsPerPage: propTypes.number,
paginationContainer: propTypes.func,
rowAction: propTypes.func,
@@ -174,7 +224,10 @@ export default class SortedTable extends Component {
}
}
const { defaultFilter } = props
this.state = {
filter: defaultFilter !== undefined ? props.filters[defaultFilter] : undefined,
selectedColumn,
itemsPerPage: props.itemsPerPage || DEFAULT_ITEMS_PER_PAGE
}
@@ -190,7 +243,7 @@ export default class SortedTable extends Component {
createFilter(
() => this.props.collection,
createSelector(
() => this.state.filter || '',
() => this.state.filter,
createMatcher
)
),
@@ -212,6 +265,8 @@ export default class SortedTable extends Component {
() => this.state.activePage,
this.state.itemsPerPage
)
this.state.selectedItemsIds = new Set()
}
componentWillMount () {
@@ -248,10 +303,60 @@ export default class SortedTable extends Component {
})
}
componentDidUpdate () {
const { selectedItemsIds } = this.state
// Unselect items that are no longer visible
if ((this._visibleItemsRecomputations || 0) < (this._visibleItemsRecomputations = this._getVisibleItems.recomputations())) {
const newSelectedItems = selectedItemsIds.intersect(map(this._getVisibleItems(), 'id'))
if (newSelectedItems.size < selectedItemsIds.size) {
this.setState({ selectedItemsIds: newSelectedItems })
}
}
}
_onPageSelection = (_, event) => this.setState({
activePage: event.eventKey
})
_selectAllVisibleItems = event => {
this.setState({
selectedItemsIds: event.target.checked
? this.state.selectedItemsIds.union(map(this._getVisibleItems(), 'id'))
: this.state.selectedItemsIds.clear()
})
}
_selectItem = event => {
const { selectedItemsIds } = this.state
const { target } = event
const visibleItems = this._getVisibleItems()
const current = +target.name
let method = target.checked ? 'add' : 'delete'
let previous
this.setState({ selectedItemsIds:
(
event.nativeEvent.shiftKey &&
(previous = this._previous) !== undefined
) ? selectedItemsIds.withMutations(selectedItemsIds => {
let i = previous
let end = current
if (previous > current) {
i = current
end = previous
}
for (; i <= end; ++i) {
selectedItemsIds[method](visibleItems[i].id)
}
})
: selectedItemsIds[method](visibleItems[current].id)
})
this._previous = current
}
_onFilterChange = debounce(filter => {
this.setState({
filter,
@@ -259,18 +364,27 @@ export default class SortedTable extends Component {
})
}, 500)
_executeGroupedAction = handler => handler(this.state.selectedItemsIds.toArray())
render () {
const { props, state } = this
const {
paginationContainer,
filterContainer,
filters,
groupedActions,
individualActions,
paginationContainer,
rowAction,
rowLink,
userData
} = props
const nFilteredItems = this._getAllItems().length
const nVisibleItems = this._getVisibleItems().length
const nSelectedItems = state.selectedItemsIds.size
const hasGroupedActions = !isEmpty(groupedActions)
const hasIndividualActions = !isEmpty(individualActions)
const paginationInstance = (
<Pagination
@@ -289,6 +403,7 @@ export default class SortedTable extends Component {
const filterInstance = (
<TableFilter
defaultFilter={state.filter}
filters={filters}
nFilteredItems={nFilteredItems}
nItems={this._getTotalNumberOfItems()}
@@ -301,6 +416,13 @@ export default class SortedTable extends Component {
<table className='table'>
<thead className='thead-default'>
<tr>
{hasGroupedActions && <th>
<Checkbox
onChange={this._selectAllVisibleItems}
checked={nSelectedItems === nVisibleItems}
indeterminate={nSelectedItems !== 0 && nSelectedItems !== nVisibleItems}
/>
</th>}
{map(props.columns, (column, key) => (
<ColumnHead
textAlign={column.textAlign}
@@ -312,30 +434,88 @@ export default class SortedTable extends Component {
sortIcon={state.selectedColumn === key ? state.sortOrder : 'sort'}
/>
))}
{hasIndividualActions && <th />}
</tr>
</thead>
<tbody>
{nSelectedItems !== 0 && <tr className='bg-faded'>
<td colSpan={props.columns.length + (individualActions != null ? 2 : 1)}>
{_('sortedTableSelectedItems', {
selected: nSelectedItems,
total: nVisibleItems
})}
<div className='pull-right'>
<ButtonGroup>
{map(groupedActions, ({ icon, label, level, handler }, key) => <ActionRowButton
btnStyle={level}
handler={this._executeGroupedAction}
handlerParam={handler}
icon={icon}
key={key}
tooltip={label}
/>)}
</ButtonGroup>
</div>
</td>
</tr>}
{map(this._getVisibleItems(), (item, i) => {
const columns = map(props.columns, (column, key) => (
<td key={key} className={column.textAlign && `text-xs-${column.textAlign}`}>
{column.itemRenderer(item, userData)}
const columns = map(props.columns, ({
component: Component,
itemRenderer,
textAlign
}, key) =>
<td
className={textAlign && `text-xs-${textAlign}`}
key={key}
>
{Component !== undefined
? <Component item={item} userData={userData} />
: itemRenderer(item, userData)
}
</td>
))
)
const { id = i } = item
return rowLink
const selectionColumn = hasGroupedActions && <td>
<input
checked={state.selectedItemsIds.has(id)}
name={i} // position in visible items
onChange={this._selectItem}
type='checkbox'
/>
</td>
const actionsColumn = hasIndividualActions && <td><div className='pull-right'>
<ButtonGroup>
{map(individualActions, ({ icon, label, level, handler }, key) => <ActionRowButton
btnStyle={level}
handler={handler}
handlerParam={id}
icon={icon}
key={key}
tooltip={label}
/>)}
</ButtonGroup>
</div></td>
return rowLink != null
? <BlockLink
key={id}
tagName='tr'
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
>{columns}</BlockLink>
>
{selectionColumn}
{columns}
{actionsColumn}
</BlockLink>
: <tr
className={rowAction && styles.clickableRow}
key={id}
onClick={rowAction && (() => rowAction(item, userData))}
>
{selectionColumn}
{columns}
{actionsColumn}
</tr>
})}
</tbody>

View File

@@ -1,29 +1,36 @@
import React from 'react'
import styled from 'styled-components'
import {
omit
} from 'lodash'
import ActionButton from './action-button'
import propTypes from './prop-types'
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`]}
// do not forward `state` to ActionButton
const Button = styled(p => <ActionButton {...omit(p, 'state')} />)`
background-color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateBg`]};
border: 2px solid ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};
color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};
`
const StateButton = ({
disabledHandler,
disabledHandlerParam,
disabledLabel,
disabledTooltip,
enabledLabel,
enabledTooltip,
enabledHandler,
enabledHandlerParam,
state,
...props
}) =>
<Button
handler={state ? enabledHandler : disabledHandler}
handlerParam={state ? enabledHandlerParam : disabledHandlerParam}
tooltip={state ? enabledTooltip : disabledTooltip}
{...props}
icon={state ? 'running' : 'halted'}

View File

@@ -18,7 +18,9 @@ const TabButton = ({
{...props}
size='large'
style={STYLE}
><span className='hidden-md-down'>{_(labelId)}</span></ActionButton>
>
{labelId !== undefined && <span className='hidden-md-down'>{_(labelId)}</span>}
</ActionButton>
)
export { TabButton as default }

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

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

@@ -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,8 @@ import mapValues from 'lodash/mapValues'
import React from 'react'
import ReadableStream from 'readable-stream'
import replace from 'lodash/replace'
import sample from 'lodash/sample'
import startsWith from 'lodash/startsWith'
import { connect } from 'react-redux'
import _ from './intl'
@@ -62,14 +64,27 @@ export const addSubscriptions = subscriptions => Component => {
}
componentWillMount () {
this._unsubscribes = map(isFunction(subscriptions) ? subscriptions() : subscriptions, (subscribe, prop) =>
subscribe(value => this.setState({ [prop]: value }))
this._unsubscribes = map(
isFunction(subscriptions)
? subscriptions(this.props)
: subscriptions,
(subscribe, prop) =>
subscribe(value => this._setState({ [prop]: value }))
)
}
componentDidMount () {
this._setState = this.setState
}
componentWillUnmount () {
forEach(this._unsubscribes, unsubscribe => unsubscribe())
this._unsubscribes = null
delete this._setState
}
_setState (nextState) {
this.state = { ...this.state, nextState }
}
render () {
@@ -180,7 +195,7 @@ export { default as Debug } from './debug'
// -------------------------------------------------------------------
// Returns the first defined (non-null, non-undefined) value.
// Returns the first defined (non-undefined) value.
export const firstDefined = function () {
const n = arguments.length
for (let i = 0; i < n; ++i) {
@@ -258,6 +273,11 @@ export const formatSize = bytes => humanFormat(bytes, { scale: 'binary', unit: '
export const formatSizeRaw = bytes => humanFormat.raw(bytes, { scale: 'binary', unit: 'B' })
export const formatSpeed = (bytes, milliseconds) => humanFormat(
bytes * 1e3 / milliseconds,
{ scale: 'binary', unit: 'B/s' }
)
export const parseSize = size => {
let bytes = humanFormat.parse.raw(size, { scale: 'binary' })
if (bytes.unit && bytes.unit !== 'B') {
@@ -349,20 +369,6 @@ export const throwFn = error => () => {
)
}
// -------------------------------------------------------------------
export function tap (cb) {
return this.then(value =>
Promise.resolve(cb(value)).then(() => value)
)
}
export function rethrow (cb) {
return this.catch(error =>
Promise.resolve(cb(error)).then(() => { throw error })
)
}
// ===================================================================
export const resolveResourceSet = resourceSet => {
@@ -524,3 +530,38 @@ export const compareVersions = makeNiceCompare((v1, v2) => {
return 0
})
export const isXosanPack = ({ name }) =>
startsWith(name, 'XOSAN')
// ===================================================================
export const getCoresPerSocketPossibilities = (maxCoresPerSocket, vCPUs) => {
// According to : https://www.citrix.com/blogs/2014/03/11/citrix-xenserver-setting-more-than-one-vcpu-per-vm-to-improve-application-performance-and-server-consolidation-e-g-for-cad3-d-graphical-applications/
const maxVCPUs = 16
const options = []
if (maxCoresPerSocket !== undefined && vCPUs !== '') {
const ratio = vCPUs / maxVCPUs
for (let coresPerSocket = maxCoresPerSocket; coresPerSocket >= ratio; coresPerSocket--) {
if (vCPUs % coresPerSocket === 0) options.push(coresPerSocket)
}
}
return options
}
// Generates a random human-readable string of length `length`
// Useful to generate random default names intended for the UI user
export const generateReadableRandomString = (() => {
const CONSONANTS = 'bdfgklmnprtvz'.split('')
const VOWELS = 'aeiou'.split('')
return (length = 8) => {
const result = new Array(length)
for (let i = 0; i < length; ++i) {
result[i] = sample((i & 1) === 0 ? VOWELS : CONSONANTS)
}
return result.join('')
}
})()

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

@@ -5,7 +5,7 @@ import getEventValue from '../get-event-value'
// ===================================================================
const getId = value => value != null && value.id || value
const getId = value => (value != null && value.id) || value
export default class XoAbstractInput extends PureComponent {
_onChange = event => {

View File

@@ -15,7 +15,7 @@ import {
values
} from 'lodash'
import propTypes from '../prop-types'
import propTypes from '../prop-types-decorator'
import { computeArraysSum } from '../xo-stats'
import { formatSize } from '../utils'

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,12 +1,16 @@
import _ from 'intl'
import BaseComponent from 'base-component'
import Icon from 'icon'
import React from 'react'
import SingleLineRow from 'single-line-row'
import { Col } from 'grid'
import { connectStore } from 'utils'
import { createCollectionWrapper, createGetObjectsOfType, createSelector } from 'selectors'
import { forEach } from 'lodash'
import { createCollectionWrapper, createGetObjectsOfType, createSelector, createGetObject } from 'selectors'
import { SelectHost } from 'select-objects'
import {
differenceBy,
forEach
} from 'lodash'
@connectStore(() => ({
singleHosts: createSelector(
@@ -30,10 +34,20 @@ import { SelectHost } from 'select-objects'
})
return singleHosts
})
),
poolMasterPatches: createSelector(
createGetObject(
(_, props) => props.pool.master
),
({ patches }) => patches
)
}), { withRef: true })
export default class AddHostModal extends BaseComponent {
get value () {
if (process.env.XOA_PLAN < 2 && this.state.nMissingPatches) {
return {}
}
return this.state
}
@@ -42,18 +56,40 @@ export default class AddHostModal extends BaseComponent {
singleHosts => host => singleHosts[host.id]
)
_onChangeHost = host => {
this.setState({
host,
nMissingPatches: host
? differenceBy(this.props.poolMasterPatches, host.patches, 'name').length
: undefined
})
}
render () {
const { nMissingPatches } = this.state
return <div>
<SingleLineRow>
<Col size={6}>{_('addHostSelectHost')}</Col>
<Col size={6}>
<SelectHost
onChange={this.linkState('host')}
onChange={this._onChangeHost}
predicate={this._getHostPredicate()}
value={this.state.host}
/>
</Col>
</SingleLineRow>
<br />
{nMissingPatches > 0 && <SingleLineRow>
<Col>
<span className='text-danger'>
<Icon icon='error' /> {process.env.XOA_PLAN > 1
? _('hostNeedsPatchUpdate', { patches: nMissingPatches })
: _('hostNeedsPatchUpdateNoInstall')
}
</span>
</Col>
</SingleLineRow>}
</div>
}
}

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

@@ -0,0 +1,139 @@
import Collapse from 'collapse'
import Component from 'base-component'
import React from 'react'
import { every, forEach, map } from 'lodash'
import _ from '../../intl'
import propTypes from '../../prop-types-decorator'
import SingleLineRow from '../../single-line-row'
import { createSelector } from '../../selectors'
import { SelectSr } from '../../select-objects'
import { isSrWritable } from 'xo'
import {
Container,
Col
} from 'grid'
// Can 2 SRs on the same pool have 2 VDIs used by the same VM
const areSrsCompatible = (sr1, sr2) =>
sr1.shared || sr2.shared || sr1.$container === sr2.$container
const Collapsible = ({collapsible, children, ...props}) => collapsible
? <Collapse {...props}>{children}</Collapse>
: <div>
<span>{props.buttonText}</span>
<br />
{children}
</div>
Collapsible.propTypes = {
collapsible: propTypes.bool.isRequired,
children: propTypes.node.isRequired
}
@propTypes({
vdis: propTypes.array.isRequired,
predicate: propTypes.func
})
export default class ChooseSrForEachVdisModal extends Component {
state = {
mapVdisSrs: {}
}
componentWillReceiveProps (newProps) {
if (
this.props.predicate !== undefined &&
newProps.predicate !== this.props.predicate
) {
this.state = {
mainSr: undefined,
mapVdisSrs: {}
}
}
}
_onChange = props => {
this.setState(props)
this.props.onChange(props)
}
_onChangeMainSr = newSr => {
const oldSr = this.state.mainSr
if (oldSr == null || newSr == null || oldSr.$pool !== newSr.$pool) {
this.setState({
mapVdisSrs: {}
})
} else if (!newSr.shared) {
const mapVdisSrs = {...this.state.mapVdisSrs}
forEach(mapVdisSrs, (sr, vdi) => {
if (sr != null && newSr !== sr && sr.$container !== newSr.$container && !sr.shared) {
delete mapVdisSrs[vdi]
}
})
this._onChange({mapVdisSrs})
}
this._onChange({
mainSr: newSr
})
}
_getSrPredicate = createSelector(
() => this.state.mainSr,
() => this.state.mapVdisSrs,
(mainSr, mapVdisSrs) => sr =>
isSrWritable(sr) &&
mainSr.$pool === sr.$pool &&
areSrsCompatible(mainSr, sr) &&
every(mapVdisSrs, selectedSr => selectedSr == null || areSrsCompatible(selectedSr, sr))
)
render () {
const { props, state } = this
const { vdis } = props
const {
mainSr,
mapVdisSrs
} = state
const srPredicate = props.predicate || this._getSrPredicate()
return <div>
<SelectSr
onChange={mainSr => props.predicate !== undefined
? this._onChange({mainSr})
: this._onChangeMainSr(mainSr)
}
predicate={props.predicate || isSrWritable}
placeholder={_('chooseSrForEachVdisModalMainSr')}
value={mainSr}
/>
<br />
{vdis != null && mainSr != null &&
<Collapsible collapsible={vdis.length >= 3} buttonText={_('chooseSrForEachVdisModalSelectSr')}>
<br />
<Container>
<SingleLineRow>
<Col size={6}><strong>{_('chooseSrForEachVdisModalVdiLabel')}</strong></Col>
<Col size={6}><strong>{_('chooseSrForEachVdisModalSrLabel')}</strong></Col>
</SingleLineRow>
{map(vdis, vdi =>
<SingleLineRow key={vdi.uuid}>
<Col size={6}>{ vdi.name_label || vdi.name }</Col>
<Col size={6}>
<SelectSr
onChange={sr => this._onChange({ mapVdisSrs: { ...mapVdisSrs, [vdi.uuid]: sr } })}
value={mapVdisSrs[vdi.uuid]}
predicate={srPredicate}
/>
</Col>
</SingleLineRow>
)}
<i>{_('chooseSrForEachVdisModalOptionalEntry')}</i>
</Container>
</Collapsible>
}
</div>
}
}

View File

@@ -15,16 +15,18 @@ import sortBy from 'lodash/sortBy'
import throttle from 'lodash/throttle'
import Xo from 'xo-lib'
import { createBackoff } from 'jsonrpc-websocket-client'
import { noHostsAvailable } from 'xo-common/api-errors'
import { reflect } from 'promise-toolbox'
import { lastly, reflect, tap } from 'promise-toolbox'
import { forbiddenOperation, noHostsAvailable } from 'xo-common/api-errors'
import { resolve } from 'url'
import _ from '../intl'
import invoke from '../invoke'
import logError from '../log-error'
import { alert, confirm } from '../modal'
import store from 'store'
import { getObject } from 'selectors'
import { alert, chooseAction, confirm } from '../modal'
import { error, info, success } from '../notification'
import { noop, rethrow, tap, resolveId, resolveIds } from '../utils'
import { noop, resolveId, resolveIds } from '../utils'
import {
connected,
disconnected,
@@ -41,8 +43,12 @@ 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 isSrShared = sr => sr && sr.shared
export const isVmRunning = vm => vm && vm.power_state === 'Running'
// ===================================================================
@@ -86,7 +92,7 @@ const _call = (method, params) => {
let promise = _signIn.then(() => xo.call(method, params))
if (process.env.NODE_ENV !== 'production') {
promise = promise::rethrow(error => {
promise = promise::tap(null, error => {
console.error('XO error', {
method,
params,
@@ -157,6 +163,11 @@ const createSubscription = cb => {
let running = false
const uninstall = () => {
clearTimeout(timeout)
cache = undefined
}
const loop = () => {
if (running) {
return
@@ -165,6 +176,11 @@ const createSubscription = cb => {
running = true
_signIn.then(() => cb()).then(result => {
running = false
if (n === 0) {
return uninstall()
}
timeout = setTimeout(loop, delay)
if (!isEqual(result, cache)) {
@@ -181,6 +197,11 @@ const createSubscription = cb => {
}
}, error => {
running = false
if (n === 0) {
return uninstall()
}
console.error(error)
})
}
@@ -189,8 +210,10 @@ const createSubscription = cb => {
const id = nextId++
subscribers[id] = cb
if (n++) {
cache !== undefined && asap(() => cb(cache))
if (n++ !== 0) {
if (cache !== undefined) {
asap(() => cb(cache))
}
} else {
loop()
}
@@ -198,9 +221,8 @@ const createSubscription = cb => {
return once(() => {
delete subscribers[id]
if (!--n) {
clearTimeout(timeout)
cache = undefined
if (--n === 0) {
uninstall()
}
})
}
@@ -280,6 +302,40 @@ export const subscribeIsInstallingXosan = (pool, cb) => {
return xosanSubscriptions[poolId](cb)
}
const missingPatchesByHost = {}
export const subscribeHostMissingPatches = (host, cb) => {
const hostId = resolveId(host)
if (missingPatchesByHost[hostId] == null) {
missingPatchesByHost[hostId] = createSubscription(() => _call('host.listMissingPatches', { host: hostId }))
}
return missingPatchesByHost[hostId](cb)
}
subscribeHostMissingPatches.forceRefresh = host => {
if (host === undefined) {
forEach(missingPatchesByHost, subscription => subscription.forceRefresh())
return
}
const subscription = missingPatchesByHost[resolveId(host)]
if (subscription !== undefined) {
subscription.forceRefresh()
}
}
const unhealthyVdiChainsLengthSubscriptionsBySr = {}
export const createSrUnhealthyVdiChainsLengthSubscription = sr => {
sr = resolveId(sr)
let subscription = unhealthyVdiChainsLengthSubscriptionsBySr[sr]
if (subscription === undefined) {
subscription = unhealthyVdiChainsLengthSubscriptionsBySr[sr] = createSubscription(
() => _call('sr.getUnhealthyVdiChainsLength', { sr })
)
}
return subscription
}
// System ============================================================
export const apiMethods = _call('system.getMethodsInfo')
@@ -308,8 +364,9 @@ export const exportConfig = () => (
export const addServer = (host, username, password, label) => (
_call('server.add', { host, label, password, username })::tap(
subscribeServers.forceRefresh
)::rethrow(() => error(_('serverError'), _('serverAddFailed')))
subscribeServers.forceRefresh,
() => error(_('serverError'), _('serverAddFailed'))
)
)
export const editServer = (server, props) => (
@@ -319,7 +376,7 @@ export const editServer = (server, props) => (
)
export const connectServer = server => (
_call('server.connect', { id: resolveId(server) })::tap(
_call('server.connect', { id: resolveId(server) })::lastly(
subscribeServers.forceRefresh
)
)
@@ -342,10 +399,11 @@ 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({
icon: 'add',
title: _('addHostModalTitle'),
body: _('addHostModalMessage', { pool: pool.name_label, host: host.name_label })
}).then(() =>
@@ -354,6 +412,7 @@ export const addHostToPool = (pool, host) => {
}
return confirm({
icon: 'add',
title: _('addHostModalTitle'),
body: <AddHostModalBody pool={pool} />
}).then(
@@ -362,7 +421,13 @@ export const addHostToPool = (pool, host) => {
error(_('addHostNoHost'), _('addHostNoHostMessage'))
return
}
_call('pool.mergeInto', { source: params.host.$pool, target: pool.id, force: true })
return _call('pool.mergeInto', { source: params.host.$pool, target: pool.id, force: true }).catch(error => {
if (error.code !== 'HOSTS_NOT_HOMOGENEOUS') {
throw error
}
error(_('addHostErrorTitle'), _('addHostNotHomogeneousErrorMessage'))
})
},
noop
)
@@ -378,10 +443,30 @@ export const detachHost = host => (
)
)
export const forgetHost = host => (
confirm({
icon: 'host-forget',
title: _('forgetHostModalTitle'),
body: _('forgetHostModalMessage', { host: <strong>{host.name_label}</strong> })
}).then(
() => _call('host.forget', { host: resolveId(host) })
)
)
export const setDefaultSr = sr => (
_call('pool.setDefaultSr', { sr: resolveId(sr) })
)
export const setPoolMaster = host => (
confirm({
title: _('setPoolMasterModalTitle'),
body: _('setPoolMasterModalMessage', { host: <strong>{host.name_label}</strong> })
}).then(
() => _call('pool.setPoolMaster', { host: resolveId(host) }),
noop
)
)
// Host --------------------------------------------------------------
export const editHost = (host, props) => (
@@ -436,7 +521,7 @@ export const restartHostsAgents = hosts => {
title: _('restartHostsAgentsModalTitle', { nHosts }),
body: _('restartHostsAgentsModalMessage', { nHosts })
}).then(
() => map(hosts, restartHostAgent),
() => Promise.all(map(hosts, restartHostAgent)),
noop
)
}
@@ -494,15 +579,21 @@ export const emergencyShutdownHosts = hosts => {
}
export const installHostPatch = (host, { uuid }) => (
_call('host.installPatch', { host: resolveId(host), patch: uuid })
_call('host.installPatch', { host: resolveId(host), patch: uuid })::tap(
() => subscribeHostMissingPatches.forceRefresh(host)
)
)
export const installAllHostPatches = host => (
_call('host.installAllPatches', { host: resolveId(host) })
_call('host.installAllPatches', { host: resolveId(host) })::tap(
() => subscribeHostMissingPatches.forceRefresh(host)
)
)
export const installAllPatchesOnPool = pool => (
_call('pool.installAllPatches', { pool: resolveId(pool) })
_call('pool.installAllPatches', { pool: resolveId(pool) })::tap(
() => subscribeHostMissingPatches.forceRefresh()
)
)
export const installSupplementalPack = (host, file) => {
@@ -567,8 +658,38 @@ export const unpauseContainer = (vm, container) => (
// VM ----------------------------------------------------------------
const chooseActionToUnblockForbiddenStartVm = props => (
chooseAction({
icon: 'alarm',
buttons: [
{ label: _('cloneAndStartVM'), value: 'clone', btnStyle: 'success' },
{ label: _('forceStartVm'), value: 'force', btnStyle: 'danger' }
],
...props
})
)
const cloneAndStartVM = async vm => (
_call('vm.start', { id: await cloneVm(vm) })
)
export const startVm = vm => (
_call('vm.start', { id: resolveId(vm) })
_call('vm.start', { id: resolveId(vm) }).catch(async reason => {
if (!forbiddenOperation.is(reason)) {
throw reason
}
const choice = await chooseActionToUnblockForbiddenStartVm({
body: _('blockedStartVmModalMessage'),
title: _('forceStartVmModalTitle')
})
if (choice === 'clone') {
return cloneAndStartVM(vm)
}
return _call('vm.start', { id: resolveId(vm), force: true })
})
)
export const startVms = vms => (
@@ -576,7 +697,52 @@ export const startVms = vms => (
title: _('startVmsModalTitle', { vms: vms.length }),
body: _('startVmsModalMessage', { vms: vms.length })
}).then(
() => map(vms, vmId => startVm({ id: vmId })),
async () => {
const forbiddenStart = []
let nErrors = 0
await Promise.all(map(
vms,
id => _call('vm.start', { id }).catch(reason => {
if (forbiddenOperation.is(reason)) {
forbiddenStart.push(id)
} else {
nErrors++
}
})
))
if (forbiddenStart.length === 0) {
if (nErrors === 0) {
return
}
return error(_('failedVmsErrorTitle'), _('failedVmsErrorMessage', {nVms: nErrors}))
}
const choice = await chooseActionToUnblockForbiddenStartVm({
body: _('blockedStartVmsModalMessage', {nVms: forbiddenStart.length}),
title: _('forceStartVmModalTitle')
}).catch(noop)
if (nErrors !== 0) {
error(_('failedVmsErrorTitle'), _('failedVmsErrorMessage', {nVms: nErrors}))
}
if (choice === 'clone') {
return Promise.all(map(
forbiddenStart,
async id => cloneAndStartVM(getObject(store.getState(), id))
))
}
if (choice === 'force') {
return Promise.all(map(
forbiddenStart,
id => _call('vm.start', { id, force: true })
))
}
},
noop
)
)
@@ -643,7 +809,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({
@@ -667,7 +833,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({
@@ -685,7 +851,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
@@ -739,7 +905,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'),
@@ -755,7 +921,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'),
@@ -834,11 +1000,16 @@ 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, mapVdisSrs }) => (
_call('vm.importDeltaBackup', resolveIds({
remote,
filePath: file,
sr,
mapVdisSrs: resolveIds(mapVdisSrs)
}))
)
import RevertSnapshotModalBody from './revert-snapshot-modal'
import RevertSnapshotModalBody from './revert-snapshot-modal' // eslint-disable-line import/first
export const revertSnapshot = vm => (
confirm({
title: _('revertVmModalTitle'),
@@ -911,7 +1082,7 @@ 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)
})
@@ -943,6 +1114,16 @@ export const deleteVdi = vdi => (
)
)
export const deleteVdis = vdis => (
confirm({
title: _('deleteVdisModalTitle', { nVdis: vdis.length }),
body: _('deleteVdisModalMessage', { nVdis: vdis.length })
}).then(
() => Promise.all(map(vdis, id => _call('vdi.delete', { id }))),
noop
)
)
export const deleteOrphanedVdis = vdis => (
confirm({
title: _('removeAllOrphanedObject'),
@@ -960,7 +1141,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) })
@@ -1010,7 +1191,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',
@@ -1030,7 +1211,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',
@@ -1319,17 +1500,15 @@ export const getSchedule = id => (
export const loadPlugin = async id => (
_call('plugin.load', { id })::tap(
subscribePlugins.forceRefresh
)::rethrow(
err => error(_('pluginError'), err && err.message || _('unknownPluginError'))
subscribePlugins.forceRefresh,
err => error(_('pluginError'), (err && err.message) || _('unknownPluginError'))
)
)
export const unloadPlugin = id => (
_call('plugin.unload', { id })::tap(
subscribePlugins.forceRefresh
)::rethrow(
err => error(_('pluginError'), err && err.message || _('unknownPluginError'))
subscribePlugins.forceRefresh,
err => error(_('pluginError'), (err && err.message) || _('unknownPluginError'))
)
)
@@ -1350,8 +1529,7 @@ export const configurePlugin = (id, configuration) =>
() => {
info(_('pluginConfigurationSuccess'), _('pluginConfigurationChanges'))
subscribePlugins.forceRefresh()
}
)::rethrow(
},
err => error(_('pluginError'), JSON.stringify(err.data) || _('unknownPluginError'))
)
@@ -1399,7 +1577,8 @@ export const recomputeResourceSetsLimits = () => (
// Remote ------------------------------------------------------------
export const getRemote = remote => (
_call('remote.get', resolveIds({ id: remote }))::rethrow(
_call('remote.get', resolveIds({ id: remote }))::tap(
null,
err => error(_('getRemote'), err.message || String(err))
)
)
@@ -1436,20 +1615,21 @@ export const editRemote = (remote, { name, url }) => (
export const listRemote = remote => (
_call('remote.list', resolveIds({ id: remote }))::tap(
subscribeRemotes.forceRefresh
)::rethrow(
subscribeRemotes.forceRefresh,
err => error(_('listRemote'), err.message || String(err))
)
)
export const listRemoteBackups = remote => (
_call('backup.list', resolveIds({ remote }))::rethrow(
_call('backup.list', resolveIds({ remote }))::tap(
null,
err => error(_('listRemote'), err.message || String(err))
)
)
export const testRemote = remote => (
_call('remote.test', resolveIds({ id: remote }))::rethrow(
_call('remote.test', resolveIds({ id: remote }))::tap(
null,
err => error(_('testRemote'), err.message || String(err))
)
)
@@ -1556,16 +1736,14 @@ export const deleteApiLog = id => (
export const addAcl = ({ subject, object, action }) => (
_call('acl.add', resolveIds({ subject, object, action }))::tap(
subscribeAcls.forceRefresh
)::rethrow(
subscribeAcls.forceRefresh,
err => error('Add ACL', err.message || String(err))
)
)
export const removeAcl = ({ subject, object, action }) => (
_call('acl.remove', resolveIds({ subject, object, action }))::tap(
subscribeAcls.forceRefresh
)::rethrow(
subscribeAcls.forceRefresh,
err => error('Remove ACL', err.message || String(err))
)
)
@@ -1580,14 +1758,15 @@ export const editAcl = (
) => (
_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)))
::tap(
subscribeAcls.forceRefresh,
err => error('Edit ACL', err.message || String(err))
)
)
export const createGroup = name => (
_call('group.create', { name })::tap(
subscribeGroups.forceRefresh
):: rethrow(
subscribeGroups.forceRefresh,
err => error(_('createGroup'), err.message || String(err))
)
)
@@ -1598,35 +1777,35 @@ export const setGroupName = (group, name) => (
)
)
export const deleteGroup = group => (
export const deleteGroup = group =>
confirm({
title: _('deleteGroup'),
body: <p>{_('deleteGroupConfirm')}</p>
}).then(() => _call('group.delete', resolveIds({ id: group })))
::tap(subscribeGroups.forceRefresh)
::rethrow(err => error(_('deleteGroup'), err.message || String(err)))
)
}).then(() =>
_call('group.delete', resolveIds({ id: group }))::tap(
subscribeGroups.forceRefresh,
err => error(_('deleteGroup'), err.message || String(err))
),
noop
)
export const removeUserFromGroup = (user, group) => (
_call('group.removeUser', resolveIds({ id: group, userId: user }))::tap(
subscribeGroups.forceRefresh
)::rethrow(
subscribeGroups.forceRefresh,
err => error(_('removeUserFromGroup'), err.message || String(err))
)
)
export const addUserToGroup = (user, group) => (
_call('group.addUser', resolveIds({ id: group, userId: user }))::tap(
subscribeGroups.forceRefresh
)::rethrow(
subscribeGroups.forceRefresh,
err => error('Add User', err.message || String(err))
)
)
export const createUser = (email, password, permission) => (
_call('user.create', { email, password, permission })::tap(
subscribeUsers.forceRefresh
)::rethrow(
subscribeUsers.forceRefresh,
err => error('Create user', err.message || String(err))
)
)
@@ -1636,9 +1815,10 @@ export const deleteUser = user => (
title: _('deleteUser'),
body: <p>{_('deleteUserConfirm')}</p>
}).then(() =>
_call('user.delete', { id: resolveId(user) })
::tap(subscribeUsers.forceRefresh)
::rethrow(err => error(_('deleteUser'), err.message || String(err)))
_call('user.delete', { id: resolveId(user) })::tap(
subscribeUsers.forceRefresh,
err => error(_('deleteUser'), err.message || String(err))
)
)
)
@@ -1667,10 +1847,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,
@@ -1713,7 +1893,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({
@@ -1810,20 +1990,28 @@ export const setIpPool = (ipPool, { name, addresses, networks }) => (
// XO SAN ----------------------------------------------------------------------
export const getVolumeInfo = (xosanSr) => _call('xosan.getVolumeInfo', { sr: xosanSr })
export const getVolumeInfo = (xosanSr, infoType) => _call('xosan.getVolumeInfo', { sr: xosanSr, infoType })
export const createXosanSR = ({ template, pif, vlan, srs, glusterType, redundancy }) => _call('xosan.createSR', {
export const createXosanSR = ({ template, pif, vlan, srs, glusterType, redundancy, brickSize, memorySize }) => _call('xosan.createSR', {
template,
pif: pif.id,
vlan: String(vlan),
srs: resolveIds(srs),
glusterType,
redundancy: Number.parseInt(redundancy)
redundancy: Number.parseInt(redundancy),
brickSize,
memorySize
})
export const computeXosanPossibleOptions = lvmSrs => _call('xosan.computeXosanPossibleOptions', { lvmSrs })
export const addXosanBricks = (xosansr, lvmsrs, brickSize) => _call('xosan.addBricks', {xosansr, lvmsrs, brickSize})
import InstallXosanPackModal from './install-xosan-pack-modal'
export const replaceXosanBrick = (xosansr, previousBrick, newLvmSr, brickSize, onSameVM = false) =>
_call('xosan.replaceBrick', {xosansr, previousBrick, newLvmSr, brickSize, onSameVM})
export const removeXosanBricks = (xosansr, bricks) => _call('xosan.removeBricks', {xosansr, bricks})
export const computeXosanPossibleOptions = (lvmSrs, brickSize) => _call('xosan.computeXosanPossibleOptions', { lvmSrs, brickSize })
import InstallXosanPackModal from './install-xosan-pack-modal' // eslint-disable-line import/first
export const downloadAndInstallXosanPack = pool =>
confirm({
title: _('xosanInstallPackTitle', { pool: pool.name_label }),

View File

@@ -1,7 +1,7 @@
import _ from 'intl'
import Component from 'base-component'
import React from 'react'
import { connectStore, compareVersions } from 'utils'
import { connectStore, compareVersions, isXosanPack } from 'utils'
import { subscribeResourceCatalog, subscribePlugins } from 'xo'
import { createGetObjectsOfType, createSelector, createCollectionWrapper } from 'selectors'
import { satisfies as versionSatisfies } from 'semver'
@@ -9,7 +9,8 @@ import {
every,
filter,
forEach,
map
map,
some
} from 'lodash'
const findLatestPack = (packs, hostsVersions) => {
@@ -37,11 +38,16 @@ const findLatestPack = (packs, hostsVersions) => {
return latestPack
}
@connectStore({
@connectStore(() => ({
hosts: createGetObjectsOfType('host').filter(
(_, { pool }) => host => pool && host.$pool === pool.id && !host.supplementalPacks['vates:XOSAN']
createSelector(
(_, { pool }) => pool != null && pool.id,
poolId => poolId
? host => host.$pool === poolId && !some(host.supplementalPacks, isXosanPack)
: false
)
)
}, { withRef: true })
}), { withRef: true })
export default class InstallXosanPackModal extends Component {
componentDidMount () {
this._unsubscribePlugins = subscribePlugins(plugins => this.setState({ plugins }))
@@ -87,15 +93,14 @@ export default class InstallXosanPackModal extends Component {
</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>
{_('xosanNoPackFound')}
<br />
{_('xosanPackRequirements')}
<ul>
{map(this._getXosanPacks(), ({ name, requirements }, key) => <li key={key}>
{_.keyValue(name, requirements && requirements.xenserver ? requirements.xenserver : '/')}
</li>)}
</ul>
</div>
}
</div>

View File

@@ -3,23 +3,23 @@ import every from 'lodash/every'
import forEach from 'lodash/forEach'
import find from 'lodash/find'
import map from 'lodash/map'
import mapValues from 'lodash/mapValues'
import React from 'react'
import store from 'store'
import _ from '../../intl'
import ChooseSrForEachVdisModal from '../choose-sr-for-each-vdis-modal'
import invoke from '../../invoke'
import SingleLineRow from '../../single-line-row'
import { Col } from '../../grid'
import { getDefaultNetworkForVif } from '../utils'
import {
SelectHost,
SelectNetwork,
SelectSr
SelectNetwork
} from '../../select-objects'
import {
connectStore,
mapPlus
mapPlus,
resolveIds
} from '../../utils'
import {
createGetObjectsOfType,
@@ -138,7 +138,8 @@ export default class MigrateVmModalBody extends BaseComponent {
get value () {
return {
targetHost: this.state.host && this.state.host.id,
mapVdisSrs: this.state.mapVdisSrs,
sr: this.state.mainSr && this.state.mainSr.id,
mapVdisSrs: resolveIds(this.state.mapVdisSrs),
mapVifsNetworks: this.state.mapVifsNetworks,
migrationNetwork: this.state.migrationNetworkId
}
@@ -158,11 +159,10 @@ export default class MigrateVmModalBody extends BaseComponent {
return
}
const { pools, vbds, vdis, vm } = this.props
const { vbds, vm } = this.props
const intraPool = vm.$pool === host.$pool
// Intra-pool
const defaultSr = pools[host.$pool].default_SR
if (intraPool) {
let doNotMigrateVdis
if (vm.$container === host.id) {
@@ -181,7 +181,6 @@ export default class MigrateVmModalBody extends BaseComponent {
doNotMigrateVdis,
host,
intraPool,
mapVdisSrs: doNotMigrateVdis ? undefined : mapValues(vdis, vdi => defaultSr),
mapVifsNetworks: undefined,
migrationNetwork: undefined
})
@@ -212,7 +211,6 @@ export default class MigrateVmModalBody extends BaseComponent {
doNotMigrateVdis: false,
host,
intraPool,
mapVdisSrs: mapValues(vdis, vdi => defaultSr),
mapVifsNetworks: defaultNetworksForVif,
migrationNetworkId: defaultMigrationNetworkId
})
@@ -226,7 +224,6 @@ export default class MigrateVmModalBody extends BaseComponent {
doNotMigrateVdis,
host,
intraPool,
mapVdisSrs,
mapVifsNetworks,
migrationNetworkId
} = this.state
@@ -245,25 +242,14 @@ export default class MigrateVmModalBody extends BaseComponent {
</div>
{host && !doNotMigrateVdis && <div className={styles.groupBlock}>
<SingleLineRow>
<Col>{_('migrateVmSelectSrs')}</Col>
<Col size={12}>
<ChooseSrForEachVdisModal
onChange={props => this.setState(props)}
predicate={this._getSrPredicate()}
vdis={vdis}
/>
</Col>
</SingleLineRow>
<br />
<SingleLineRow>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmName')}</span></Col>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmSr')}</span></Col>
</SingleLineRow>
{map(vdis, vdi => <div className={styles.listItem} key={vdi.id}>
<SingleLineRow>
<Col size={6}>{vdi.name_label}</Col>
<Col size={6}>
<SelectSr
onChange={sr => this.setState({ mapVdisSrs: { ...mapVdisSrs, [vdi.id]: sr.id } })}
predicate={this._getSrPredicate()}
value={mapVdisSrs[vdi.id]}
/>
</Col>
</SingleLineRow>
</div>)}
</div>}
{intraPool !== undefined &&
(!intraPool &&

View File

@@ -12,8 +12,10 @@ import some from 'lodash/some'
import store from 'store'
import _ from '../../intl'
import Icon from 'icon'
import invoke from '../../invoke'
import SingleLineRow from '../../single-line-row'
import Tooltip from '../../tooltip'
import { Col } from '../../grid'
import { getDefaultNetworkForVif } from '../utils'
import {
@@ -217,6 +219,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
const { pools, pifs } = this.props
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
const defaultSrId = pools[host.$pool].default_SR
const defaultSrConnectedToHost = some(host.$PBDs, pbd => this._getObject(pbd).SR === defaultSrId)
const doNotMigrateVmVdis = {}
const doNotMigrateVdi = {}
forEach(this.props.vbdsByVm, (vbds, vm) => {
@@ -234,6 +237,8 @@ export default class MigrateVmsModalBody extends BaseComponent {
})
const noVdisMigration = every(doNotMigrateVmVdis)
this.setState({
defaultSrConnectedToHost,
defaultSrId,
host,
intraPool: every(this.props.vms, vm => vm.$pool === host.$pool),
doNotMigrateVdi,
@@ -242,7 +247,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
networkId: defaultMigrationNetworkId,
noVdisMigration,
smartVifMapping: true,
srId: defaultSrId
srId: defaultSrConnectedToHost ? defaultSrId : undefined
})
}
_selectMigrationNetwork = migrationNetwork => this.setState({ migrationNetworkId: migrationNetwork.id })
@@ -252,6 +257,8 @@ export default class MigrateVmsModalBody extends BaseComponent {
render () {
const {
defaultSrConnectedToHost,
defaultSrId,
host,
intraPool,
migrationNetworkId,
@@ -290,7 +297,24 @@ export default class MigrateVmsModalBody extends BaseComponent {
{host && (!intraPool || !noVdisMigration) &&
<div key='sr' style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>{!intraPool ? _('migrateVmsSelectSr') : _('migrateVmsSelectSrIntraPool')}</Col>
<Col size={6}>
{!intraPool ? _('migrateVmsSelectSr') : _('migrateVmsSelectSrIntraPool')}
{' '}
{(defaultSrId === undefined || !defaultSrConnectedToHost) &&
<Tooltip
content={defaultSrId !== undefined
? _('migrateVmNotConnectedDefaultSrError')
: _('migrateVmNoDefaultSrError')
}
>
<Icon
icon={defaultSrId !== undefined ? 'alarm' : 'info'}
className={defaultSrId !== undefined ? 'text-warning' : 'text-info'}
size='lg'
/>
</Tooltip>
}
</Col>
<Col size={6}>
<SelectSr
onChange={this._selectSr}

View File

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

View File

@@ -3,7 +3,7 @@ import React from 'react'
import _ from './intl'
import Icon from './icon'
import Link from './link'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
import { Card, CardHeader, CardBlock } from './card'
import { connectStore, getXoaPlan } from './utils'
import { isAdmin } from 'selectors'

View File

@@ -79,6 +79,14 @@
@extend .fa;
@extend .fa-ellipsis-v;
},
&-previous {
@extend .fa;
@extend .fa-chevron-left;
},
&-next {
@extend .fa;
@extend .fa-chevron-right;
},
&-caret {
@extend .fa;
@extend .fa-caret-down;
@@ -375,6 +383,12 @@
@extend .xo-status-busy;
}
&-disabled {
@extend .fa;
@extend .fa-circle;
@extend .xo-status-busy;
}
&-all-connected {
@extend .fa;
@extend .fa-circle;
@@ -441,6 +455,15 @@
@extend .fa-server;
@extend .text-danger;
}
&-disabled {
@extend .fa;
@extend .fa-server;
@extend .text-warning;
}
&-forget {
@extend .fa;
@extend .fa-ban;
}
&-working {
@extend .fa;
@extend .fa-circle;
@@ -940,4 +963,11 @@
@extend .fa;
@extend .fa-star;
}
// XOSAN related
&-health {
@extend .fa;
@extend .fa-heartbeat;
}
}

View File

@@ -18,7 +18,7 @@
}
.usage-element-highlight {
background-color: $brand-primary;
background-color: $brand-warning;
}
.usage-element-others {

View File

@@ -0,0 +1,3 @@
.listRestoreBackupInfos {
list-style-type: none;
}

View File

@@ -27,6 +27,7 @@ import {
} from 'xo'
import RestoreFileModalBody from './restore-file-modal'
import styles from './index.css'
const VM_COLUMNS = [
{
@@ -118,9 +119,18 @@ export default class FileRestore extends Component {
? <Container>
<h2>{_('restoreFiles')}</h2>
{isEmpty(backupInfoByVm)
? _('noBackup')
? <div>
<em><Icon icon='info' /> {_('restoreDeltaBackupsInfo')}</em>
<div>
<a>{_('noBackup')}</a>
</div>
</div>
: <div>
<em><Icon icon='info' /> {_('restoreBackupsInfo')}</em>
<ul className={styles.listRestoreBackupInfos}>
<li><em><Icon icon='info' /> {_('restoreBackupsInfo')}</em></li>
<li><em><Icon icon='info' /> {_('restoreDeltaBackupsInfo')}</em></li>
</ul>
<SortedTable collection={backupInfoByVm} columns={VM_COLUMNS} rowAction={openImportModal} defaultColumn={2} />
</div>
}

View File

@@ -85,7 +85,8 @@ export default class RestoreFileModalBody extends Component {
return scanFiles(backup.remoteId, disk, path, partition).then(
rawFiles => this.setState({
files: formatFilesOptions(rawFiles, path),
scanningFiles: false
scanningFiles: false,
scanFilesError: false
}),
error => {
this.setState({
@@ -104,7 +105,8 @@ export default class RestoreFileModalBody extends Component {
partition: undefined,
file: undefined,
selectedFiles: undefined,
scanDiskError: false
scanDiskError: false,
scanFilesError: false
})
}
@@ -113,7 +115,8 @@ export default class RestoreFileModalBody extends Component {
partition: undefined,
file: undefined,
selectedFiles: undefined,
scanDiskError: false
scanDiskError: false,
scanFilesError: false
})
if (!disk) {
@@ -268,7 +271,7 @@ export default class RestoreFileModalBody extends Component {
value={partition}
/>
]}
{(partition || disk && !scanDiskError && noPartitions) && [
{(partition || (disk && !scanDiskError && noPartitions)) && [
<br />,
<Container>
<Row>
@@ -280,7 +283,7 @@ export default class RestoreFileModalBody extends Component {
<Col size={2}>
<span className='pull-right'>
<Tooltip content={_('restoreFilesSelectAllFiles')}>
<ActionButton btnStyle='secondary' handler={this._selectAllFolderFiles} icon='add' size='small' />
<ActionButton handler={this._selectAllFolderFiles} icon='add' size='small' />
</Tooltip>
</span>
</Col>
@@ -322,12 +325,13 @@ export default class RestoreFileModalBody extends Component {
<Col className='pl-0 pb-1' size={10}>
<em>{_('restoreFilesSelectedFiles', { files: selectedFiles.length })}</em>
</Col>
<Col size={2}>
<span className='pull-right'>
<Tooltip content={_('restoreFilesUnselectAll')}>
<ActionButton btnStyle='secondary' handler={this._unselectAllFiles} icon='remove' size='small' />
</Tooltip>
</span>
<Col size={2} className='text-xs-right'>
<ActionButton
handler={this._unselectAllFiles}
icon='remove'
size='small'
tooltip={_('restoreFilesUnselectAll')}
/>
</Col>
</Row>
{map(selectedFiles, file =>
@@ -335,10 +339,8 @@ export default class RestoreFileModalBody extends Component {
<Col size={10}>
<pre>{file.path}</pre>
</Col>
<Col size={2}>
<span className='pull-right'>
<ActionButton btnStyle='secondary' handler={this._unselectFile} handlerParam={file} icon='remove' size='small' />
</span>
<Col size={2} className='text-xs-right'>
<ActionButton handler={this._unselectFile} handlerParam={file} icon='remove' size='small' />
</Col>
</Row>
)}

View File

@@ -1,32 +1,39 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Button from 'button'
import Component from 'base-component'
import delay from 'lodash/delay'
import forEach from 'lodash/forEach'
import GenericInput from 'json-schema-input'
import getEventValue from 'get-event-value'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import moment from 'moment-timezone'
import React from 'react'
import Scheduler, { SchedulePreview } from 'scheduling'
import startsWith from 'lodash/startsWith'
import uncontrollableInput from 'uncontrollable-input'
import Upgrade from 'xoa-upgrade'
import Wizard, { Section } from 'wizard'
import { addSubscriptions } from 'utils'
import { confirm } from 'modal'
import { error } from 'notification'
import { generateUiSchema } from 'xo-json-schema-input'
import { SelectSubject } from 'select-objects'
import { connectStore, EMPTY_OBJECT } from 'utils'
import { Container, Row, Col } from 'grid'
import { createSelector } from 'reselect'
import { generateUiSchema } from 'xo-json-schema-input'
import { getUser } from 'selectors'
import { SelectSubject } from 'select-objects'
import {
forEach,
identity,
isArray,
map,
mapValues,
noop,
startsWith
} from 'lodash'
import {
createJob,
createSchedule,
getRemote,
editJob,
editSchedule,
subscribeCurrentUser
editSchedule
} from 'xo'
// ===================================================================
@@ -52,13 +59,13 @@ const NO_SMART_UI_SCHEMA = generateUiSchema(NO_SMART_SCHEMA)
const SMART_SCHEMA = {
type: 'object',
properties: {
status: {
power_state: {
default: 'All', // FIXME: can't translate
enum: [ 'All', 'Running', 'Halted' ], // FIXME: can't translate
title: _('editBackupSmartStatusTitle'),
description: 'The statuses of VMs to backup.' // FIXME: can't translate
},
poolsOptions: {
$pool: {
type: 'object',
title: _('editBackupSmartPools'),
properties: {
@@ -67,7 +74,7 @@ const SMART_SCHEMA = {
title: _('editBackupNot'),
description: 'Toggle on to backup VMs that are NOT resident on these pools'
},
pools: {
values: {
type: 'array',
items: {
type: 'string',
@@ -78,7 +85,7 @@ const SMART_SCHEMA = {
}
}
},
tagsOptions: {
tags: {
type: 'object',
title: _('editBackupSmartTags'),
properties: {
@@ -87,7 +94,7 @@ const SMART_SCHEMA = {
title: _('editBackupNot'),
description: 'Toggle on to backup VMs that do NOT contain these tags'
},
tags: {
values: {
type: 'array',
items: {
type: 'string',
@@ -99,7 +106,7 @@ const SMART_SCHEMA = {
}
}
},
required: [ 'status', 'poolsOptions', 'tagsOptions' ]
required: [ 'power_state', '$pool', 'tags' ]
}
const SMART_UI_SCHEMA = generateUiSchema(SMART_SCHEMA)
@@ -125,7 +132,7 @@ const COMMON_SCHEMA = {
},
enabled: {
type: 'boolean',
title: _('editBackupReportEnable')
title: _('editBackupScheduleEnabled')
}
},
required: [ 'tag', 'vms', '_reportWhen' ]
@@ -183,6 +190,15 @@ const DISASTER_RECOVERY_SCHEMA = {
properties: {
...COMMON_SCHEMA.properties,
depth: DEPTH_PROPERTY,
deleteOldBackupsFirst: {
type: 'boolean',
title: _('deleteOldBackupsFirst'),
description: [
'Delete the old backups before copy the vms.',
'',
'If the backup fails, you will lose your old backups.'
].join('\n')
},
sr: {
type: 'string',
'xo:type': 'sr',
@@ -196,6 +212,7 @@ const CONTINUOUS_REPLICATION_SCHEMA = {
type: 'object',
properties: {
...COMMON_SCHEMA.properties,
retention: DEPTH_PROPERTY,
sr: {
type: 'string',
'xo:type': 'sr',
@@ -261,198 +278,187 @@ const BACKUP_METHOD_TO_INFO = {
// ===================================================================
const DEFAULT_CRON_PATTERN = '0 0 * * *'
@uncontrollableInput()
class TimeoutInput extends Component {
_onChange = event => {
const value = getEventValue(event).trim()
this.props.onChange(value === '' ? null : +value * 1e3)
}
function negatePattern (pattern, not = true) {
render () {
const { props } = this
const { value } = props
return <input
{...props}
onChange={this._onChange}
min='1'
type='number'
value={value == null ? '' : String(value / 1e3)}
/>
}
}
// ===================================================================
const DEFAULT_CRON_PATTERN = '0 0 * * *'
const DEFAULT_TIMEZONE = moment.tz.guess()
// xo-web v5.7.1 introduced a bug where an extra level
// ({ id: { id: <id> } }) was introduced for the VM param.
//
// This code automatically unbox the ids.
const extractId = value => {
while (typeof value === 'object') {
value = value.id
}
return value
}
const destructPattern = (pattern, valueTransform = identity) => pattern && ({
not: !!pattern.__not,
values: valueTransform((pattern.__not || pattern).__or)
})
const constructPattern = ({ not, values } = EMPTY_OBJECT, valueTransform = identity) => {
if (values == null || !values.length) {
return
}
const pattern = { __or: valueTransform(values) }
return not
? { __not: pattern }
: pattern
}
@addSubscriptions({
currentUser: subscribeCurrentUser
@connectStore({
currentUser: getUser
})
export default class New extends Component {
constructor (props) {
super(props)
this.state.cronPattern = DEFAULT_CRON_PATTERN
}
componentWillReceiveProps (props) {
const { currentUser } = props
const { owner } = this.state
if (currentUser && !owner) {
this.setState({ owner: currentUser.id })
}
}
componentWillMount () {
const { job, schedule } = this.props
if (!job || !schedule) {
if (job || schedule) { // Having only one of them is unexpected incomplete information
error(_('backupEditNotFoundTitle'), _('backupEditNotFoundMessage'))
_getParams = createSelector(
() => this.props.job,
() => this.props.schedule,
(job, schedule) => {
if (!job) {
return { main: {}, vms: { vms: [] } }
}
this.setState({
timezone: moment.tz.guess()
})
return
}
this.setState({
backupInfo: BACKUP_METHOD_TO_INFO[job.method],
cronPattern: schedule.cron,
owner: job.userId,
timeout: job.timeout && job.timeout / 1e3,
timezone: schedule.timezone || null
}, () => delay(this._populateForm, 250, job)) // Work around.
// Without the delay, some selects are not always ready to load a value
// Values are displayed, but html5 compliant browsers say the value is required and empty on submit
}
const { items } = job.paramsVector
const enabled = schedule != null && schedule.enabled
_populateForm = job => {
let values = job.paramsVector.items
const {
backupInput,
vmsInput
} = this.refs
if (values.length === 1) {
// Older versions of XenOrchestra uses only values[0].
const array = values[0].values
const config = array[0]
const reportWhen = config._reportWhen
backupInput.value = {
...config,
_reportWhen:
// Fix old reportWhen values...
(reportWhen === 'fail' && 'failure') ||
(reportWhen === 'alway' && 'always') ||
reportWhen
// legacy backup jobs
if (items.length === 1) {
return {
main: {
enabled,
...items[0].values[0]
},
vms: { vms: map(items[0].values.slice(1), extractId) }
}
}
vmsInput.value = { vms: map(array, ({ id, vm }) => id || vm) }
} else {
if (values[1].type === 'map') {
// Smart backup.
const {
$pool: poolsOptions = {},
tags: tagsOptions = {},
power_state: status = 'All'
} = values[1].collection.pattern
backupInput.value = values[0].values[0]
// smart backup
if (items[1].type === 'map') {
const { pattern } = items[1].collection
const { $pool, tags } = pattern
this.setState({
smartBackupMode: true
}, () => {
vmsInput.value = {
poolsOptions: {
pools: poolsOptions.__not ? poolsOptions.__not.__or : poolsOptions.__or,
not: !!poolsOptions.__not
},
status,
tagsOptions: {
tags: map(tagsOptions.__not ? tagsOptions.__not.__or : tagsOptions.__or, tag => tag[0]),
not: !!tagsOptions.__not
}
return {
main: {
enabled,
...items[0].values[0]
},
vms: {
$pool: destructPattern($pool),
power_state: pattern.power_state,
tags: destructPattern(tags, tags => map(tags, tag => isArray(tag) ? tag[0] : tag))
}
})
} else {
// Normal backup.
backupInput.value = values[1].values[0]
}
}
// xo-web v5.7.1 introduced a bug where an extra level ({ id: { id: <id> } }) was introduced for the VM param.
//
// This code automatically unbox the ids.
const vms = map(values[0].values, id => {
while (typeof id === 'object') {
id = id.id
}
return id
})
vmsInput.value = { vms }
// normal backup
return {
main: {
enabled,
...items[1].values[0]
},
vms: { vms: map(items[0].values, extractId) }
}
}
}
)
_getMainParams = () => this.state.mainParams || this._getParams().main
_getVmsParam = () => this.state.vmsParam || this._getParams().vms
_getScheduling = createSelector(
() => this.props.schedule,
() => this.state.scheduling,
(schedule, scheduling) => {
if (scheduling !== undefined) {
return scheduling
}
const {
cron = DEFAULT_CRON_PATTERN,
timezone = DEFAULT_TIMEZONE
} = schedule || EMPTY_OBJECT
return {
cronPattern: cron,
timezone
}
}
)
_handleSubmit = async () => {
const { props, state } = this
const method = this._getValue('job', 'method')
const backupInfo = BACKUP_METHOD_TO_INFO[method]
const {
enabled,
...callArgs
} = this.refs.backupInput.value
const vmsInputValue = this.refs.vmsInput.value
const {
backupInfo,
smartBackupMode,
timeout,
timezone,
owner
} = this.state
const { pools, not: notPools } = vmsInputValue.poolsOptions || {}
const { tags, not: notTags } = vmsInputValue.tagsOptions || {}
const formattedTags = map(tags, tag => [ tag ])
const paramsVector = !smartBackupMode
? {
type: 'crossProduct',
items: [{
type: 'set',
values: map(vmsInputValue.vms, vm => ({ id: vm.id || vm }))
}, {
type: 'set',
values: [ callArgs ]
}]
} : {
type: 'crossProduct',
items: [{
type: 'set',
values: [ callArgs ]
}, {
type: 'map',
collection: {
type: 'fetchObjects',
pattern: {
$pool: isEmpty(pools)
? undefined
: negatePattern({ __or: pools }, notPools),
power_state: vmsInputValue.status === 'All' ? undefined : vmsInputValue.status,
tags: isEmpty(tags)
? undefined
: negatePattern({ __or: formattedTags }, notTags),
type: 'VM'
}
},
iteratee: {
type: 'extractProperties',
mapping: { id: 'id' }
}
}]
}
...mainParams
} = this._getMainParams()
const vms = this._getVmsParam()
const job = {
...state.job,
type: 'call',
key: backupInfo.jobKey,
method: backupInfo.method,
paramsVector,
userId: owner,
timeout: timeout ? timeout * 1e3 : undefined
paramsVector: {
type: 'crossProduct',
items: isArray(vms.vms)
? [{
type: 'set',
values: map(vms.vms, vm => ({ id: extractId(vm) }))
}, {
type: 'set',
values: [ mainParams ]
}]
: [{
type: 'set',
values: [ mainParams ]
}, {
type: 'map',
collection: {
type: 'fetchObjects',
pattern: {
$pool: constructPattern(vms.$pool),
power_state: vms.power_state === 'All' ? undefined : vms.power_state,
tags: constructPattern(vms.tags, tags => map(tags, tag => [ tag ])),
type: 'VM'
}
},
iteratee: {
type: 'extractProperties',
mapping: { id: 'id' }
}
}]
}
}
// Update backup schedule.
const { job: oldJob, schedule: oldSchedule } = this.props
if (oldJob && oldSchedule) {
job.id = oldJob.id
return editJob(job).then(() => editSchedule({
...oldSchedule,
cron: this.state.cronPattern,
timezone
}))
}
const scheduling = this._getScheduling()
let remoteId
if (job.type === 'call') {
@@ -485,101 +491,128 @@ export default class New extends Component {
}
}
// Update backup schedule.
const oldJob = props.job
if (oldJob) {
job.id = oldJob.id
await editJob(job)
return editSchedule({
id: props.schedule.id,
cron: scheduling.cronPattern,
enabled,
timezone: scheduling.timezone
})
}
if (job.timeout === null) {
delete job.timeout // only needed for job edition
}
// Create backup schedule.
return createSchedule(await createJob(job), { cron: this.state.cronPattern, enabled, timezone })
return createSchedule(await createJob(job), {
cron: scheduling.cronPattern,
enabled,
timezone: scheduling.timezone
})
}
_handleReset = () => {
const { backupInput } = this.refs
if (backupInput) {
backupInput.value = undefined
}
this.setState({
cronPattern: DEFAULT_CRON_PATTERN
})
}
_updateCronPattern = value => {
this.setState(value)
}
_handleBackupSelection = event => {
const method = event.target.value
this.setState({
showVersionWarning: method === 'vm.rollingDeltaBackup' || method === 'vm.deltaCopy',
backupInfo: BACKUP_METHOD_TO_INFO[method]
})
this.setState(mapValues(this.state, noop))
}
_handleSmartBackupMode = event => {
this.setState({
smartBackupMode: event.target.value === 'smart'
})
this.setState(
event.target.value === 'smart'
? { vmsParam: {} }
: { vmsParam: { vms: [] } }
)
}
_subjectPredicate = ({ type, permission }) =>
type === 'user' && permission === 'admin'
render () {
const { state } = this
const {
backupInfo,
cronPattern,
smartBackupMode,
timezone,
owner,
showVersionWarning
} = state
_getValue = (ns, key, defaultValue) => {
let tmp
return process.env.XOA_PLAN > 1
? (
<Wizard>
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
<Container>
<Row>
<Col>
<fieldset className='form-group'>
<label>{_('backupOwner')}</label>
<SelectSubject
onChange={this.linkState('owner', 'id')}
predicate={this._subjectPredicate}
required
value={owner || null}
/>
</fieldset>
<fieldset className='form-group'>
<label>{_('jobTimeoutPlaceHolder')}</label>
<input type='number' onChange={this.linkState('timeout')} value={state.timeout} className='form-control' />
</fieldset>
<fieldset className='form-group'>
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
<select
className='form-control'
value={(backupInfo && backupInfo.method) || ''}
id='selectBackup'
onChange={this._handleBackupSelection}
required
>
{_('noSelectedValue', message => <option value=''>{message}</option>)}
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
_(info.label, message => <option key={key} value={key}>{message}</option>)
)}
</select>
</fieldset>
{showVersionWarning && <div className='alert alert-warning' role='alert'>
<Icon icon='error' /> {_('backupVersionWarning')}
</div>}
<form id='form-new-vm-backup'>
// look in the state
if (
(tmp = this.state[ns]) != null &&
(tmp = tmp[key]) !== undefined
) {
return tmp
}
// look in the props
if (
(tmp = this.props[ns]) != null &&
(tmp = tmp[key]) !== undefined
) {
return tmp
}
return defaultValue
}
render () {
const method = this._getValue('job', 'method', '')
const scheduling = this._getScheduling()
const vms = this._getVmsParam()
const backupInfo = BACKUP_METHOD_TO_INFO[method]
const smartBackupMode = !isArray(vms.vms)
return (
<Upgrade place='newBackup' required={2}>
<form id='form-new-vm-backup'>
<Wizard>
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
<Container>
<Row>
<Col>
<fieldset className='form-group'>
<label>{_('backupOwner')}</label>
<SelectSubject
onChange={this.linkState('job.userId', 'id')}
predicate={this._subjectPredicate}
required
value={this._getValue('job', 'userId', this.props.currentUser.id)}
/>
</fieldset>
<fieldset className='form-group'>
<label>{_('jobTimeoutPlaceHolder')}</label>
<TimeoutInput
className='form-control'
onChange={this.linkState('job.timeout')}
value={this._getValue('job', 'timeout')}
/>
</fieldset>
<fieldset className='form-group'>
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
<select
className='form-control'
id='selectBackup'
onChange={this.linkState('job.method')}
required
value={method}
>
{_('noSelectedValue', message => <option value=''>{message}</option>)}
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
_({ key }, info.label, message => <option value={key}>{message}</option>)
)}
</select>
</fieldset>
{(method === 'vm.rollingDeltaBackup' || method === 'vm.deltaCopy') && <div className='alert alert-warning' role='alert'>
<Icon icon='error' /> {_('backupVersionWarning')}
</div>}
{backupInfo && <div>
<GenericInput
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
ref='backupInput'
required
schema={backupInfo.schema}
uiSchema={backupInfo.uiSchema}
onChange={this.linkState('mainParams')}
value={this._getMainParams()}
/>
<fieldset className='form-group'>
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
@@ -598,65 +631,67 @@ export default class New extends Component {
? <Upgrade place='newBackup' required={3}>
<GenericInput
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
ref='vmsInput'
onChange={this.linkState('vmsParam')}
required
schema={SMART_SCHEMA}
uiSchema={SMART_UI_SCHEMA}
value={vms}
/>
</Upgrade>
: <GenericInput
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
ref='vmsInput'
onChange={this.linkState('vmsParam')}
required
schema={NO_SMART_SCHEMA}
uiSchema={NO_SMART_UI_SCHEMA}
value={vms}
/>
}
</div>}
</form>
</Col>
</Row>
</Container>
</Section>
<Section icon='schedule' title='schedule'>
<Scheduler
cronPattern={cronPattern}
onChange={this._updateCronPattern}
timezone={timezone}
/>
</Section>
<Section icon='preview' title='preview' summary>
<Container>
<Row>
<Col>
<SchedulePreview cronPattern={cronPattern} />
{process.env.XOA_PLAN < 4 && backupInfo && process.env.XOA_PLAN < REQUIRED_XOA_PLAN[backupInfo.jobKey]
? <Upgrade place='newBackup' available={REQUIRED_XOA_PLAN[backupInfo.jobKey]} />
: (smartBackupMode && process.env.XOA_PLAN < 3
? <Upgrade place='newBackup' available={3} />
: <fieldset className='pull-right pt-1'>
<ActionButton
btnStyle='primary'
className='btn-lg mr-1'
disabled={!backupInfo}
form='form-new-vm-backup'
handler={this._handleSubmit}
icon='save'
redirectOnSuccess='/backup/overview'
>
{_('saveBackupJob')}
</ActionButton>
<button type='button' className='btn btn-lg btn-secondary' onClick={this._handleReset}>
{_('selectTableReset')}
</button>
</fieldset>)
}
</Col>
</Row>
</Container>
</Section>
</Wizard>
)
: <Container><Upgrade place='newBackup' available={2} /></Container>
</Col>
</Row>
</Container>
</Section>
<Section icon='schedule' title='schedule'>
<Scheduler
onChange={this.linkState('scheduling')}
value={scheduling}
/>
</Section>
<Section icon='preview' title='preview' summary>
<Container>
<Row>
<Col>
<SchedulePreview cronPattern={scheduling.cronPattern} />
{process.env.XOA_PLAN < 4 && backupInfo && process.env.XOA_PLAN < REQUIRED_XOA_PLAN[backupInfo.jobKey]
? <Upgrade place='newBackup' available={REQUIRED_XOA_PLAN[backupInfo.jobKey]} />
: (smartBackupMode && process.env.XOA_PLAN < 3
? <Upgrade place='newBackup' available={3} />
: <fieldset className='pull-right pt-1'>
<ActionButton
btnStyle='primary'
className='mr-1'
disabled={!backupInfo}
form='form-new-vm-backup'
handler={this._handleSubmit}
icon='save'
redirectOnSuccess='/backup/overview'
size='large'
>
{_('saveBackupJob')}
</ActionButton>
<Button onClick={this._handleReset} size='large'>
{_('selectTableReset')}
</Button>
</fieldset>)
}
</Col>
</Row>
</Container>
</Section>
</Wizard>
</form>
</Upgrade>
)
}
}

View File

@@ -1,27 +1,30 @@
import _ from 'intl'
import ActionRowButton from 'action-row-button'
import ButtonGroup from 'button-group'
import Component from 'base-component'
import filter from 'lodash/filter'
import find from 'lodash/find'
import forEach from 'lodash/forEach'
import get from 'lodash/get'
import Icon from 'icon'
import Link from 'link'
import LogList from '../../logs'
import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
import NoObjects from 'no-objects'
import React from 'react'
import SortedTable from 'sorted-table'
import StateButton from 'state-button'
import Tooltip from 'tooltip'
import { addSubscriptions } from 'utils'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { createSelector } from 'selectors'
import {
Card,
CardHeader,
CardBlock
} from 'card'
import {
filter,
find,
forEach,
get,
map,
orderBy
} from 'lodash'
import {
deleteBackupSchedule,
disableSchedule,
@@ -122,7 +125,6 @@ export default class Overview extends Component {
constructor (props) {
super(props)
this.state = {
schedules: [],
scheduleTable: {}
}
}
@@ -212,12 +214,12 @@ export default class Overview extends Component {
<div>
<Card>
<CardHeader>
<h5><Icon icon='schedule' /> {_('backupSchedules')}</h5>
<Icon icon='schedule' /> {_('backupSchedules')}
</CardHeader>
<CardBlock>
{schedules.length ? (
<NoObjects collection={schedules} emptyMessage={_('noScheduledJobs')}>
<SortedTable columns={JOB_COLUMNS} collection={this._getScheduleCollection()} userData={isScheduleUserMissing} />
) : <p>{_('noScheduledJobs')}</p>}
</NoObjects>
</CardBlock>
</Card>
<LogList jobKeys={Object.keys(jobKeyToLabel)} />

View File

@@ -1,8 +1,11 @@
import _, { messages } from 'intl'
import ChooseSrForEachVdisModal from 'xo/choose-sr-for-each-vdis-modal'
import Component from 'base-component'
import every from 'lodash/every'
import filter from 'lodash/filter'
import find from 'lodash/find'
import forEach from 'lodash/forEach'
import getEventValue from 'get-event-value'
import groupBy from 'lodash/groupBy'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
@@ -15,22 +18,27 @@ import SortedTable from 'sorted-table'
import uniq from 'lodash/uniq'
import Upgrade from 'xoa-upgrade'
import { confirm } from 'modal'
import { createSelector } from 'selectors'
import { addSubscriptions, noop } from 'utils'
import { Container, Row, Col } from 'grid'
import { FormattedDate, injectIntl } from 'react-intl'
import { info, error } from 'notification'
import { SelectPlainObject, Toggle } from 'form'
import { SelectSr } from 'select-objects'
import {
importBackup,
importDeltaBackup,
isSrWritable,
listRemote,
listRemoteBackups,
startVm,
subscribeRemotes
} from 'xo'
// Can 2 SRs on the same pool have 2 VDIs used by the same VM
const areSrsCompatible = (sr1, sr2) =>
sr1.shared || sr2.shared || sr1.$container === sr2.$container
const parseDate = date => +moment(date, 'YYYYMMDDTHHmmssZ').format('x')
const backupOptionRenderer = backup => <span>
@@ -49,7 +57,7 @@ const VM_COLUMNS = [
{
name: _('backupTags'),
itemRenderer: ({ tagsByRemote }) => <Container>
{map(tagsByRemote, ({ tags, remoteName }) => <Row>
{map(tagsByRemote, ({ tags, remoteName }, key) => <Row key={key}>
<Col mediumSize={3}><strong>{remoteName}</strong></Col>
<Col mediumSize={9}>{tags.join(', ')}</Col>
</Row>)}
@@ -76,8 +84,8 @@ const openImportModal = ({ backups }) => confirm({
body: <ImportModalBody vmName={backups[0].name} backups={backups} />
}).then(doImport)
const doImport = ({ backup, sr, start }) => {
if (!sr || !backup) {
const doImport = ({ backup, mainSr, start, mapVdisSrs }) => {
if (!mainSr || !backup) {
error(_('backupRestoreErrorTitle'), _('backupRestoreErrorMessage'))
return
}
@@ -87,7 +95,7 @@ const doImport = ({ backup, sr, start }) => {
}
info(_('importBackupTitle'), _('importBackupMessage'))
try {
const importPromise = importMethods[backup.type]({remote: backup.remoteId, sr, file: backup.path}).then(id => {
const importPromise = importMethods[backup.type]({remote: backup.remoteId, sr: mainSr, file: backup.path, mapVdisSrs}).then(id => {
return id
})
if (start) {
@@ -99,16 +107,59 @@ const doImport = ({ backup, sr, start }) => {
}
class _ModalBody extends Component {
constructor () {
super()
this.state = {
mapVdisSrs: {}
}
}
get value () {
return this.state
}
_getSrPredicate = createSelector(
() => this.state.sr,
() => this.state.mapVdisSrs,
(defaultSr, mapVdisSrs) => sr =>
sr !== defaultSr &&
isSrWritable(sr) &&
defaultSr.$pool === sr.$pool &&
areSrsCompatible(defaultSr, sr) &&
every(mapVdisSrs, selectedSr => selectedSr == null || areSrsCompatible(selectedSr, sr))
)
_onChangeDefaultSr = event => {
const oldSr = this.state.sr
const newSr = getEventValue(event)
if (oldSr == null || newSr == null || oldSr.$pool !== newSr.$pool) {
this.setState({
mapVdisSrs: {}
})
} else if (!newSr.shared) {
const mapVdisSrs = {...this.state.mapVdisSrs}
forEach(mapVdisSrs, (sr, vdi) => {
if (sr != null && newSr !== sr && sr.$container !== newSr.$container && !sr.shared) {
delete mapVdisSrs[vdi]
}
})
this.setState({
mapVdisSrs
})
}
this.setState({
sr: newSr
})
}
render () {
const { backups, intl } = this.props
const vdis = this.state.backup && this.state.backup.vdis
return <div>
<SelectSr onChange={this.linkState('sr')} predicate={isSrWritable} />
<br />
<SelectPlainObject
onChange={this.linkState('backup')}
optionKey='path'
@@ -117,6 +168,11 @@ class _ModalBody extends Component {
placeholder={intl.formatMessage(messages.importBackupModalSelectBackup)}
/>
<br />
<ChooseSrForEachVdisModal
vdis={vdis}
onChange={props => this.setState(props)}
/>
<br />
<Toggle onChange={this.linkState('start')} /> {_('importBackupModalStart')}
</div>
}
@@ -136,16 +192,26 @@ export default class Restore extends Component {
}
_listAll = async remotes => {
const remotesFiles = await Promise.all(map(remotes, remote => listRemote(remote.id)))
const remotesInfo = await Promise.all(map(remotes, async remote => ({
files: await listRemote(remote.id),
backupsInfo: await listRemoteBackups(remote.id)
})))
const backupInfoByVm = {}
forEach(remotesFiles, (remoteFiles, index) => {
forEach(remotesInfo, (remoteInfo, index) => {
const remote = remotes[index]
forEach(remoteFiles, file => {
forEach(remoteInfo.files, file => {
let backup
const deltaInfo = /^vm_delta_(.*)_([^/]+)\/([^_]+)_(.*)$/.exec(file)
if (deltaInfo) {
const [ , tag, id, date, name ] = deltaInfo
const vdis = find(remoteInfo.backupsInfo, {
id: `${file}.json`
}).disks
backup = {
type: 'delta',
date: parseDate(date),
@@ -154,7 +220,8 @@ export default class Restore extends Component {
path: file,
tag,
remoteId: remote.id,
remoteName: remote.name
remoteName: remote.name,
vdis
}
} else {
const backupInfo = /^([^_]+)_([^_]+)_(.*)\.xva$/.exec(file)

View File

@@ -1,32 +1,45 @@
import _ from 'intl'
import ActionRowButton from 'action-row-button'
import Component from 'base-component'
import get from 'lodash/get'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import Link from 'link'
import map from 'lodash/map'
import mapValues from 'lodash/mapValues'
import NoObjects from 'no-objects'
import React from 'react'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Tooltip from 'tooltip'
import Upgrade from 'xoa-upgrade'
import React from 'react'
import xml2js from 'xml2js'
import { Card, CardHeader, CardBlock } from 'card'
import { confirm } from 'modal'
import { deleteMessage, deleteVdi, deleteOrphanedVdis, deleteVm, isSrWritable } from 'xo'
import { Container, Row, Col } from 'grid'
import { FormattedRelative, FormattedTime } from 'react-intl'
import { fromCallback } from 'promise-toolbox'
import { Container, Row, Col } from 'grid'
import {
deleteMessage,
deleteOrphanedVdis,
deleteVbd,
deleteVdi,
deleteVm,
isSrWritable
} from 'xo'
import {
areObjectsFetched,
createCollectionWrapper,
createGetObject,
createGetObjectsOfType,
createSelector
} from 'selectors'
import {
flatten,
get,
map,
mapValues
} from 'lodash'
import {
connectStore,
formatSize,
mapPlus,
noop
} from 'utils'
@@ -102,10 +115,21 @@ const SR_COLUMNS = [
}
]
const VDI_COLUMNS = [
const ORPHANED_VDI_COLUMNS = [
{
name: _('snapshotDate'),
itemRenderer: vdi => <span><FormattedTime value={vdi.snapshot_time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={vdi.snapshot_time * 1000} />)</span>,
itemRenderer: vdi => <span>
<FormattedTime
day='numeric'
hour='numeric'
minute='numeric'
month='long'
value={vdi.snapshot_time * 1000}
year='numeric'
/>
{' '}
(<FormattedRelative value={vdi.snapshot_time * 1000} />)
</span>,
sortCriteria: vdi => vdi.snapshot_time,
sortOrder: 'desc'
},
@@ -141,10 +165,58 @@ const VDI_COLUMNS = [
}
]
const CONTROL_DOMAIN_VDI_COLUMNS = [
{
name: _('vdiNameLabel'),
itemRenderer: vdi => vdi && vdi.name_label,
sortCriteria: vdi => vdi && vdi.name_label
},
{
name: _('vdiNameDescription'),
itemRenderer: vdi => vdi && vdi.name_description,
sortCriteria: vdi => vdi && vdi.name_description
},
{
name: _('vdiPool'),
itemRenderer: vdi => vdi && vdi.pool && <Link to={`pools/${vdi.pool.id}`}>{vdi.pool.name_label}</Link>,
sortCriteria: vdi => vdi && vdi.pool && vdi.pool.name_label
},
{
name: _('vdiSize'),
itemRenderer: vdi => vdi && formatSize(vdi.size),
sortCriteria: vdi => vdi && vdi.size
},
{
name: _('vdiSr'),
itemRenderer: vdi => vdi && vdi.sr && <Link to={`srs/${vdi.sr.id}`}>{vdi.sr.name_label}</Link>,
sortCriteria: vdi => vdi && vdi.sr && vdi.sr.name_label
},
{
name: _('vdiAction'),
itemRenderer: vdi => vdi && vdi.vbd && <ActionRowButton
btnStyle='danger'
handler={deleteVbd}
handlerParam={vdi.vbd}
icon='delete'
/>
}
]
const VM_COLUMNS = [
{
name: _('snapshotDate'),
itemRenderer: vm => <span><FormattedTime value={vm.snapshot_time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={vm.snapshot_time * 1000} />)</span>,
itemRenderer: vm => <span>
<FormattedTime
day='numeric'
hour='numeric'
minute='numeric'
month='long'
value={vm.snapshot_time * 1000}
year='numeric'
/>
{' '}
(<FormattedRelative value={vm.snapshot_time * 1000} />)
</span>,
sortCriteria: vm => vm.snapshot_time,
sortOrder: 'desc'
},
@@ -178,9 +250,18 @@ const VM_COLUMNS = [
const ALARM_COLUMNS = [
{
name: _('alarmDate'),
itemRenderer: message => (
<span><FormattedTime value={message.time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={message.time * 1000} />)</span>
),
itemRenderer: message => <span>
<FormattedTime
day='numeric'
hour='numeric'
minute='numeric'
month='long'
value={message.time * 1000}
year='numeric'
/>
{' '}
(<FormattedRelative value={message.time * 1000} />)
</span>,
sortCriteria: message => message.time,
sortOrder: 'desc'
},
@@ -224,8 +305,40 @@ const ALARM_COLUMNS = [
@connectStore(() => {
const getOrphanVdiSnapshots = createGetObjectsOfType('VDI-snapshot')
.filter([ snapshot => !snapshot.$snapshot_of ])
.filter([ _ => !_.$snapshot_of && _.$VBDs.length === 0 ])
.sort()
const getControlDomainVbds = createGetObjectsOfType('VBD')
.pick(
createSelector(
createGetObjectsOfType('VM-controller'),
createCollectionWrapper(
vmControllers => flatten(map(vmControllers, '$VBDs'))
)
)
)
.sort()
const getControlDomainVdis = createSelector(
getControlDomainVbds,
createGetObjectsOfType('VDI'),
createGetObjectsOfType('pool'),
createGetObjectsOfType('SR'),
(vbds, vdis, pools, srs) =>
mapPlus(vbds, (vbd, push) => {
const vdi = vdis[vbd.VDI]
if (vdi == null) {
return
}
push({
...vdi,
pool: pools[vbd.$pool],
sr: srs[vdi.$SR],
vbd
})
}
)
)
const getOrphanVmSnapshots = createGetObjectsOfType('VM-snapshot')
.filter([ snapshot => !snapshot.$snapshot_of ])
.sort()
@@ -240,7 +353,9 @@ const ALARM_COLUMNS = [
.filter([ message => message.name === 'ALARM' ])
return {
areObjectsFetched,
alertMessages: getAlertMessages,
controlDomainVdis: getControlDomainVdis,
userSrs: getUserSrs,
vdiOrphaned: getOrphanVdiSnapshots,
vdiSr: getVdiSrs,
@@ -315,6 +430,8 @@ export default class Health extends Component {
_getSrUrl = sr => `srs/${sr.id}`
render () {
const { props } = this
return process.env.XOA_PLAN > 3
? <Container>
<Row>
@@ -324,18 +441,20 @@ export default class Health extends Component {
<Icon icon='disk' /> {_('srStatePanel')}
</CardHeader>
<CardBlock>
{isEmpty(this.props.userSrs)
? <p className='text-xs-center'>{_('noSrs')}</p>
: <Row>
<NoObjects
collection={props.areObjectsFetched ? props.userSrs : null}
emptyMessage={_('noSrs')}
>
<Row>
<Col>
<SortedTable
collection={this.props.userSrs}
collection={props.userSrs}
columns={SR_COLUMNS}
rowLink={this._getSrUrl}
/>
</Col>
</Row>
}
</NoObjects>
</CardBlock>
</Card>
</Col>
@@ -347,9 +466,11 @@ export default class Health extends Component {
<Icon icon='disk' /> {_('orphanedVdis')}
</CardHeader>
<CardBlock>
{isEmpty(this.props.vdiOrphaned)
? <p className='text-xs-center'>{_('noOrphanedObject')}</p>
: <div>
<NoObjects
collection={props.areObjectsFetched ? props.vdiOrphaned : null}
emptyMessage={_('noOrphanedObject')}
>
<div>
<Row>
<Col className='text-xs-right'>
<TabButton
@@ -362,11 +483,28 @@ export default class Health extends Component {
</Row>
<Row>
<Col>
<SortedTable collection={this.props.vdiOrphaned} columns={VDI_COLUMNS} />
<SortedTable collection={this.props.vdiOrphaned} columns={ORPHANED_VDI_COLUMNS} />
</Col>
</Row>
</div>
}
</NoObjects>
</CardBlock>
</Card>
</Col>
</Row>
<Row>
<Col>
<Card>
<CardHeader>
<Icon icon='disk' /> {_('vdisOnControlDomain')}
</CardHeader>
<CardBlock>
<NoObjects
collection={props.areObjectsFetched ? props.controlDomainVdis : null}
emptyMessage={_('noControlDomainVdis')}
>
<SortedTable collection={props.controlDomainVdis} columns={CONTROL_DOMAIN_VDI_COLUMNS} />
</NoObjects>
</CardBlock>
</Card>
</Col>
@@ -378,10 +516,12 @@ export default class Health extends Component {
<Icon icon='vm' /> {_('orphanedVms')}
</CardHeader>
<CardBlock>
{isEmpty(this.props.vmOrphaned)
? <p className='text-xs-center'>{_('noOrphanedObject')}</p>
: <SortedTable collection={this.props.vmOrphaned} columns={VM_COLUMNS} />
}
<NoObjects
collection={props.areObjectsFetched ? props.vmOrphaned : null}
emptyMessage={_('noOrphanedObject')}
>
<SortedTable collection={props.vmOrphaned} columns={VM_COLUMNS} />
</NoObjects>
</CardBlock>
</Card>
</Col>
@@ -393,9 +533,11 @@ export default class Health extends Component {
<Icon icon='alarm' /> {_('alarmMessage')}
</CardHeader>
<CardBlock>
{isEmpty(this.props.alertMessages)
? <p className='text-xs-center'>{_('noAlarms')}</p>
: <div>
<NoObjects
collection={props.areObjectsFetched ? props.alertMessages : null}
emptyMessage={_('noAlarms')}
>
<div>
<Row>
<Col className='text-xs-right'>
<TabButton
@@ -412,7 +554,7 @@ export default class Health extends Component {
</Col>
</Row>
</div>
}
</NoObjects>
</CardBlock>
</Card>
</Col>

View File

@@ -1,16 +1,16 @@
import _ from 'intl'
import ButtonGroup from 'button-group'
import ChartistGraph from 'react-chartist'
import Component from 'base-component'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import propTypes from 'prop-types'
import propTypes from 'prop-types-decorator'
import Link, { BlockLink } from 'link'
import map from 'lodash/map'
import HostsPatchesTable from 'hosts-patches-table'
import React from 'react'
import size from 'lodash/size'
import Upgrade from 'xoa-upgrade'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { Card, CardBlock, CardHeader } from 'card'
import { Container, Row, Col } from 'grid'
import {
@@ -19,7 +19,8 @@ import {
createGetObjectsOfType,
createGetHostMetrics,
createSelector,
createTop
createTop,
isAdmin
} from 'selectors'
import {
connectStore,
@@ -68,23 +69,19 @@ class PatchesCard extends Component {
const getHostMetrics = createGetHostMetrics(getHosts)
const userSrs = createTop(
createGetObjectsOfType('SR').filter(
[ isSrWritable ]
),
[ sr => sr.physical_usage / sr.size ],
5
const writableSrs = createGetObjectsOfType('SR').filter(
[ isSrWritable ]
)
const getSrMetrics = createCollectionWrapper(
createSelector(
userSrs,
userSrs => {
writableSrs,
writableSrs => {
const metrics = {
srTotal: 0,
srUsage: 0
}
forEach(userSrs, sr => {
forEach(writableSrs, sr => {
metrics.srUsage += sr.physical_usage
metrics.srTotal += sr.size
})
@@ -136,13 +133,18 @@ class PatchesCard extends Component {
return {
hostMetrics: getHostMetrics,
hosts: getHosts,
isAdmin,
nAlarmMessages: getNumberOfAlarmMessages,
nHosts: getNumberOfHosts,
nPools: getNumberOfPools,
nTasks: getNumberOfTasks,
nVms: getNumberOfVms,
srMetrics: getSrMetrics,
userSrs: userSrs,
topWritableSrs: createTop(
writableSrs,
[ sr => sr.physical_usage / sr.size ],
5
),
vmMetrics: getVmMetrics
}
})
@@ -238,8 +240,8 @@ export default class Overview extends Component {
/>
<p className='text-xs-center'>
{_('ofUsage', {
total: `${props.vmMetrics.vcpus} vCPUs`,
usage: `${props.hostMetrics.cpus} CPUs`
total: `${props.hostMetrics.cpus} CPUs`,
usage: `${props.vmMetrics.vcpus} vCPUs`
})}
</p>
</div>
@@ -306,7 +308,10 @@ export default class Overview extends Component {
</CardHeader>
<CardBlock>
<p className={styles.bigCardContent}>
<Link to='/settings/users'>{nUsers}</Link>
{props.isAdmin
? <Link to='/settings/users'>{nUsers}</Link>
: <p>{nUsers}</p>
}
</p>
</CardBlock>
</Card>
@@ -345,8 +350,8 @@ export default class Overview extends Component {
<ChartistGraph
style={{strokeWidth: '30px'}}
data={{
labels: map(props.userSrs, 'name_label'),
series: map(props.userSrs, sr => (sr.physical_usage / sr.size) * 100)
labels: map(props.topWritableSrs, 'name_label'),
series: map(props.topWritableSrs, sr => (sr.physical_usage / sr.size) * 100)
}}
options={{ showLabel: false, showGrid: false, distributeSeries: true, high: 100 }}
type='Bar'

View File

@@ -5,7 +5,7 @@ import Component from 'base-component'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import map from 'lodash/map'
import propTypes from 'prop-types'
import propTypes from 'prop-types-decorator'
import React from 'react'
import renderXoItem from 'render-xo-item'
import sortBy from 'lodash/sortBy'
@@ -305,32 +305,22 @@ class SelectMetric extends Component {
/>
</div>
<div className='btn-group mt-1' role='group'>
<button
className='btn btn-secondary'
onClick={this._resetSelection}
tooltip={_('dashboardStatsButtonRemoveAll')}
type='button'
>
<Icon icon='remove' />
</button>
<button
className='btn btn-secondary'
onClick={this._selectAllHosts}
tooltip={_('dashboardStatsButtonAddAllHost')}
type='button'
>
<Icon icon='host' />
</button>
<button
className='btn btn-secondary'
onClick={this._selectAllVms}
tooltip={_('dashboardStatsButtonAddAllVM')}
type='button'
>
<Icon icon='vm' />
</button>
<ActionButton
btnStyle='secondary'
handler={this._resetSelection}
icon='remove'
tooltip={_('dashboardStatsButtonRemoveAll')}
/>
<ActionButton
handler={this._selectAllHosts}
icon='host'
tooltip={_('dashboardStatsButtonAddAllHost')}
/>
<ActionButton
handler={this._selectAllVms}
icon='vm'
tooltip={_('dashboardStatsButtonAddAllVM')}
/>
<ActionButton
disabled={!objects.length}
handler={this._validSelection}
icon='success'

View File

@@ -79,7 +79,7 @@ class MiniStats extends Component {
}
}
@connectStore(({
@connectStore(() => ({
container: createGetObject((_, props) => props.item.$pool),
needsRestart: createDoesHostNeedRestart((_, props) => props.item),
nVms: createGetObjectsOfType('VM').count(
@@ -106,6 +106,7 @@ export default class HostItem extends Component {
render () {
const { item: host, container, expandAll, selected, nVms } = this.props
const toolTipContent = host.power_state === `Running` && !host.enabled ? `disabled` : _(`powerState${host.power_state}`)
return <div className={styles.item}>
<BlockLink to={`/hosts/${host.id}`}>
<SingleLineRow>
@@ -115,13 +116,15 @@ export default class HostItem extends Component {
&nbsp;&nbsp;
<Tooltip
content={isEmpty(host.current_operations)
? _(`powerState${host.power_state}`)
: <div>{_(`powerState${host.power_state}`)}{' ('}{map(host.current_operations)[0]}{')'}</div>
? toolTipContent
: <div>{toolTipContent}{' ('}{map(host.current_operations)[0]}{')'}</div>
}
>
{isEmpty(host.current_operations)
? <Icon icon={`${host.power_state.toLowerCase()}`} />
: <Icon icon='busy' />
{!isEmpty(host.current_operations)
? <Icon icon='busy' />
: (host.power_state === 'Running' && !host.enabled)
? <Icon icon='disabled' />
: <Icon icon={`${host.power_state.toLowerCase()}`} />
}
</Tooltip>
&nbsp;&nbsp;

View File

@@ -2,12 +2,14 @@ import * as ComplexMatcher from 'complex-matcher'
import * as homeFilters from 'home-filters'
import _ from 'intl'
import ActionButton from 'action-button'
import Button from 'button'
import CenterPanel from 'center-panel'
import Component from 'base-component'
import Icon from 'icon'
import invoke from 'invoke'
import Link from 'link'
import Page from '../page'
import propTypes from 'prop-types-decorator'
import React from 'react'
import Shortcuts from 'shortcuts'
import SingleLineRow from 'single-line-row'
@@ -50,12 +52,14 @@ import {
startVms,
stopHosts,
stopVms,
subscribeResourceSets,
subscribeServers
} from 'xo'
import { Container, Row, Col } from 'grid'
import {
SelectHost,
SelectPool,
SelectResourceSet,
SelectTag
} from 'select-objects'
import {
@@ -72,10 +76,10 @@ import {
createPager,
createSelector,
createSort,
getUser
getUser,
isAdmin
} from 'selectors'
import {
Button,
DropdownButton,
MenuItem,
OverlayTrigger,
@@ -143,6 +147,7 @@ const OPTIONS = {
Item: VmItem,
showPoolsSelector: true,
showHostsSelector: true,
showResourceSetsSelector: true,
sortOptions: [
{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
{ labelId: 'homeSortByPowerstate', sortBy: 'power_state', sortOrder: 'desc' },
@@ -205,19 +210,117 @@ const TYPES = {
const DEFAULT_TYPE = 'VM'
@addSubscriptions({
servers: subscribeServers
noRegisteredServers: cb => subscribeServers(data => cb(isEmpty(data)))
})
@connectStore(() => {
const noServersConnected = invoke(
createGetObjectsOfType('host'),
hosts => state => isEmpty(hosts(state))
)
const type = (_, props) => props.location.query.t || DEFAULT_TYPE
return {
areObjectsFetched,
noServersConnected
}
})
@propTypes({
isAdmin: propTypes.bool.isRequired,
noResourceSets: propTypes.bool.isRequired
})
class NoObjects_ extends Component {
render () {
const {
areObjectsFetched,
isAdmin,
noRegisteredServers,
noResourceSets,
noServersConnected
} = this.props
if (!areObjectsFetched) {
return <CenterPanel>
<h2><img src='assets/loading.svg' /></h2>
</CenterPanel>
}
if (noServersConnected && isAdmin) {
return <CenterPanel>
<Card shadow>
<CardHeader>{_('homeWelcome')}</CardHeader>
<CardBlock>
<Link to='/settings/servers'>
<Icon icon='pool' size={4} />
<h4>{noRegisteredServers ? _('homeAddServer') : _('homeConnectServer')}</h4>
</Link>
<p className='text-muted'>{noRegisteredServers ? _('homeWelcomeText') : _('homeConnectServerText')}</p>
<br /><br />
<h3>{_('homeHelp')}</h3>
<Row>
<Col mediumSize={6}>
<a href='https://xen-orchestra.com/docs/' target='_blank' className='btn btn-link'>
<Icon icon='menu-about' size={4} />
<h4>{_('homeOnlineDoc')}</h4>
</a>
</Col>
<Col mediumSize={6}>
<a href='https://xen-orchestra.com/#!/member/support' target='_blank' className='btn btn-link'>
<Icon icon='menu-settings-users' size={4} />
<h4>{_('homeProSupport')}</h4>
</a>
</Col>
</Row>
</CardBlock>
</Card>
</CenterPanel>
}
return <CenterPanel>
<Card shadow>
<CardHeader>{_('homeNoVms')}</CardHeader>
{(isAdmin || !noResourceSets) && <CardBlock>
<Row>
<Col>
<Link to='/vms/new'>
<Icon icon='vm' size={4} />
<h4>{_('homeNewVm')}</h4>
</Link>
<p className='text-muted'>{_('homeNewVmMessage')}</p>
</Col>
</Row>
{isAdmin && <div>
<h2>{_('homeNoVmsOr')}</h2>
<Row>
<Col mediumSize={6}>
<Link to='/import'>
<Icon icon='menu-new-import' size={4} />
<h4>{_('homeImportVm')}</h4>
</Link>
<p className='text-muted'>{_('homeImportVmMessage')}</p>
</Col>
<Col mediumSize={6}>
<Link to='/backup/restore'>
<Icon icon='backup' size={4} />
<h4>{_('homeRestoreBackup')}</h4>
</Link>
<p className='text-muted'>{_('homeRestoreBackupMessage')}</p>
</Col>
</Row>
</div>}
</CardBlock>}
</Card>
</CenterPanel>
}
}
@addSubscriptions({
noResourceSets: cb => subscribeResourceSets(data => cb(isEmpty(data)))
})
@connectStore(() => {
const type = (_, props) => props.location.query.t || DEFAULT_TYPE
return {
isAdmin,
items: createGetObjectsOfType(type),
noServersConnected,
type,
user: getUser
}
@@ -313,7 +416,7 @@ export default class Home extends Component {
const defaultFilter = this._getDefaultFilter(props)
if (defaultFilter != null) {
this._setFilter(defaultFilter, props)
this._setFilter(defaultFilter, props, true)
}
return
}
@@ -332,6 +435,7 @@ export default class Home extends Component {
selectedHosts: properties.$container,
selectedPools: properties.$pool,
selectedTags: properties.tags,
selectedResourceSets: properties.resourceSet,
...sort
})
@@ -359,16 +463,18 @@ export default class Home extends Component {
// Optionally can take the props to be able to use it in
// componentWillReceiveProps().
_setFilter (filter, props = this.props) {
_setFilter (filter, props = this.props, replace) {
if (!isString(filter)) {
filter = filter::ComplexMatcher.toString()
}
const { pathname, query } = props.location
this.context.router.push({
this.context.router[replace ? 'replace' : 'push']({
pathname,
query: { ...query, s: filter }
})
this.page = 1
}
_clearFilter = () => this._setFilter('')
@@ -446,6 +552,19 @@ export default class Home extends Component {
: filter::ComplexMatcher.removePropertyClause('tags')
)
}
_updateSelectedResourceSets = resourceSets => {
const filter = this._getParsedFilter()
this._setFilter(resourceSets.length
? filter::ComplexMatcher.setPropertyClause(
'resourceSet',
ComplexMatcher.createOr(map(resourceSets, set =>
ComplexMatcher.createString(set.id)
))
)
: filter::ComplexMatcher.removePropertyClause('resourceSet')
)
}
_addCustomFilter = () => {
return addCustomFilter(
this._getType(),
@@ -528,7 +647,11 @@ export default class Home extends Component {
// Header --------------------------------------------------------------------
_renderHeader () {
const { type } = this.props
const {
isAdmin,
noResourceSets,
type
} = this.props
const { filters } = OPTIONS[type]
const customFilters = this._getCustomFilters()
@@ -550,7 +673,7 @@ export default class Home extends Component {
{name}
</MenuItem>
),
<MenuItem divider />
<MenuItem key='divider' divider />
]}
{map(filters, (filter, label) =>
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
@@ -568,11 +691,9 @@ export default class Home extends Component {
type='text'
/>
<div className='input-group-btn'>
<a
className='btn btn-secondary'
onClick={this._clearFilter}>
<Button onClick={this._clearFilter}>
<Icon icon='clear-search' />
</a>
</Button>
</div>
<div className='input-group-btn'>
<ActionButton
@@ -583,13 +704,14 @@ export default class Home extends Component {
</div>
</div>
</Col>
<Col mediumSize={3} className='text-xs-right'>
{(isAdmin || !noResourceSets) && <Col mediumSize={3} className='text-xs-right'>
<Link
className='btn btn-success'
to='/vms/new'>
to='/vms/new'
>
<Icon icon='vm-new' /> {_('homeNewVm')}
</Link>
</Col>
</Col>}
</Row>
</Container>
}
@@ -598,89 +720,17 @@ export default class Home extends Component {
render () {
const {
areObjectsFetched,
noServersConnected,
servers,
user
isAdmin,
noResourceSets
} = this.props
const isAdmin = user && user.permission === 'admin'
const noRegisteredServers = !servers || !servers.length
if (!areObjectsFetched) {
return <CenterPanel>
<h2><img src='assets/loading.svg' /></h2>
</CenterPanel>
}
if (noServersConnected && isAdmin) {
return <CenterPanel>
<Card shadow>
<CardHeader>{_('homeWelcome')}</CardHeader>
<CardBlock>
<Link to='/settings/servers'>
<Icon icon='pool' size={4} />
<h4>{noRegisteredServers ? _('homeAddServer') : _('homeConnectServer')}</h4>
</Link>
<p className='text-muted'>{noRegisteredServers ? _('homeWelcomeText') : _('homeConnectServerText')}</p>
<br /><br />
<h3>{_('homeHelp')}</h3>
<Row>
<Col mediumSize={6}>
<a href='https://xen-orchestra.com/docs/' target='_blank' className='btn btn-link'>
<Icon icon='menu-about' size={4} />
<h4>{_('homeOnlineDoc')}</h4>
</a>
</Col>
<Col mediumSize={6}>
<a href='https://xen-orchestra.com/#!/member/support' target='_blank' className='btn btn-link'>
<Icon icon='menu-settings-users' size={4} />
<h4>{_('homeProSupport')}</h4>
</a>
</Col>
</Row>
</CardBlock>
</Card>
</CenterPanel>
}
const nItems = this._getNumberOfItems()
if (!nItems) {
return <CenterPanel>
<Card shadow>
<CardHeader>{_('homeNoVms')}</CardHeader>
<CardBlock>
<Row>
<Col>
<Link to='/vms/new'>
<Icon icon='vm' size={4} />
<h4>{_('homeNewVm')}</h4>
</Link>
<p className='text-muted'>{_('homeNewVmMessage')}</p>
</Col>
</Row>
{isAdmin && <div>
<h2>{_('homeNoVmsOr')}</h2>
<Row>
<Col mediumSize={6}>
<Link to='/import'>
<Icon icon='menu-new-import' size={4} />
<h4>{_('homeImportVm')}</h4>
</Link>
<p className='text-muted'>{_('homeImportVmMessage')}</p>
</Col>
<Col mediumSize={6}>
<Link to='/backup/restore'>
<Icon icon='backup' size={4} />
<h4>{_('homeRestoreBackup')}</h4>
</Link>
<p className='text-muted'>{_('homeRestoreBackupMessage')}</p>
</Col>
</Row>
</div>}
</CardBlock>
</Card>
</CenterPanel>
if (nItems < 1) {
return <NoObjects_
isAdmin={isAdmin}
noResourceSets={noResourceSets}
/>
}
const filteredItems = this._getFilteredItems()
@@ -693,6 +743,7 @@ export default class Home extends Component {
selectedHosts,
selectedItems,
selectedPools,
selectedResourceSets,
selectedTags,
sortBy
} = this.state
@@ -707,7 +758,8 @@ export default class Home extends Component {
mainActions,
otherActions,
showHostsSelector,
showPoolsSelector
showPoolsSelector,
showResourceSetsSelector
} = options
// Necessary because indeterminate cannot be used as an attribute
@@ -751,7 +803,6 @@ export default class Home extends Component {
{map(mainActions, (action, key) => (
<Tooltip content={action.tooltip} key={key}>
<ActionButton
btnStyle='secondary'
{...action}
handlerParam={this._getSelectedItemsIds()}
/>
@@ -785,10 +836,9 @@ export default class Home extends Component {
</Popover>
}
>
<Button className='btn-link'><Icon icon='pool' /> {_('homeAllPools')}</Button>
<Button btnStyle='link'><Icon icon='pool' /> {_('homeAllPools')}</Button>
</OverlayTrigger>
)}
{' '}
{showHostsSelector && (
<OverlayTrigger
trigger='click'
@@ -805,10 +855,9 @@ export default class Home extends Component {
</Popover>
}
>
<Button className='btn-link'><Icon icon='host' /> {_('homeAllHosts')}</Button>
<Button btnStyle='link'><Icon icon='host' /> {_('homeAllHosts')}</Button>
</OverlayTrigger>
)}
{' '}
<OverlayTrigger
autoFocus
trigger='click'
@@ -826,9 +875,25 @@ export default class Home extends Component {
</Popover>
}
>
<Button className='btn-link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
<Button btnStyle='link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
</OverlayTrigger>
{' '}
{showResourceSetsSelector && isAdmin && !noResourceSets && <OverlayTrigger
trigger='click'
rootClose
placement='bottom'
overlay={
<Popover className={styles.selectObject} id='resourceSetPopover'>
<SelectResourceSet
autoFocus
multi
onChange={this._updateSelectedResourceSets}
value={selectedResourceSets}
/>
</Popover>
}
>
<Button btnStyle='link'><Icon icon='resource-set' /> {_('homeAllResourceSets')}</Button>
</OverlayTrigger>}
<DropdownButton bsStyle='link' id='sort' title={_('homeSortBy')}>
{map(options.sortOptions, ({ labelId, sortBy: _sortBy, sortOrder }, key) => (
<MenuItem key={key} onClick={() => this.setState({ sortBy: _sortBy, sortOrder })}>
@@ -844,10 +909,9 @@ export default class Home extends Component {
}
</Col>
<Col smallsize={1} mediumSize={1} className='text-xs-right'>
<button className='btn btn-secondary'
onClick={this._expandAll}>
<Button onClick={this._expandAll}>
<Icon icon='nav' />
</button>
</Button>
</Col>
</SingleLineRow>
{isEmpty(filteredItems)
@@ -863,7 +927,7 @@ export default class Home extends Component {
item={item}
key={item.id}
onSelect={this.toggleState(`selectedItems.${item.id}`)}
selected={selectedItems[item.id]}
selected={Boolean(selectedItems[item.id])}
/>
</div>
))

View File

@@ -31,7 +31,9 @@ import {
import {
createFinder,
createGetObject,
createSelector
createGetVmDisks,
createSelector,
createSumBy
} from 'selectors'
import styles from './index.css'
@@ -39,20 +41,19 @@ import styles from './index.css'
@addSubscriptions({
resourceSets: subscribeResourceSets
})
@connectStore({
container: createGetObject((_, props) => props.item.$container)
})
@connectStore(() => ({
container: createGetObject((_, props) => props.item.$container),
totalDiskSize: createSumBy(
createGetVmDisks((_, props) => props.item),
'size'
)
}))
export default class VmItem extends Component {
get _isRunning () {
const vm = this.props.item
return vm && vm.power_state === 'Running'
}
_getMigrationPredicate = createSelector(
() => this.props.container,
container => host => host.id !== container.id
)
_getResourceSet = createFinder(
() => this.props.resourceSets,
createSelector(
@@ -138,7 +139,6 @@ export default class VmItem extends Component {
labelProp='name_label'
onChange={this._migrateVm}
placeholder={_('homeMigrateTo')}
predicate={this._getMigrationPredicate()}
useLongClick
value={container}
xoType='host'
@@ -164,6 +164,8 @@ export default class VmItem extends Component {
{' '}&nbsp;{' '}
{formatSize(vm.memory.size)} <Icon icon='memory' />
{' '}&nbsp;{' '}
{formatSize(this.props.totalDiskSize)} <Icon icon='disk' />
{' '}&nbsp;{' '}
{isEmpty(vm.snapshots)
? null
: <span>{vm.snapshots.length}x <Icon icon='vm-snapshot' /></span>

View File

@@ -1,4 +1,5 @@
import ActionBar from 'action-bar'
import _ from 'intl'
import ActionBar, { Action } from 'action-bar'
import React from 'react'
import {
// disableHost,
@@ -12,44 +13,42 @@ import {
const hostActionBarByState = {
Running: ({ host }) => (
<ActionBar
actions={[
{
icon: 'host-stop',
label: 'stopHostLabel',
handler: stopHost
},
{
icon: 'host-restart-agent',
label: 'restartHostAgent',
handler: restartHostAgent
},
{
icon: 'host-emergency-shutdown',
label: 'emergencyModeLabel',
handler: emergencyShutdownHost
},
{
icon: 'host-reboot',
label: 'rebootHostLabel',
handler: restartHost
}
]}
display='icon'
param={host}
/>
handlerParam={host}
>
<Action
handler={stopHost}
icon='host-stop'
label={_('stopHostLabel')}
/>
<Action
handler={restartHostAgent}
icon='host-restart-agent'
label={_('restartHostAgent')}
/>
<Action
handler={emergencyShutdownHost}
icon='host-emergency-shutdown'
label={_('emergencyModeLabel')}
/>
<Action
handler={restartHost}
icon='host-reboot'
label={_('rebootHostLabel')}
/>
</ActionBar>
),
Halted: ({ host }) => (
<ActionBar
actions={[
{
icon: 'host-start',
label: 'startHostLabel',
handler: startHost
}
]}
display='icon'
param={host}
/>
handlerParam={host}
>
<Action
handler={startHost}
icon='host-start'
label={_('startHostLabel')}
/>
</ActionBar>
)
}

View File

@@ -7,8 +7,14 @@ import Page from '../page'
import React, { cloneElement, Component } from 'react'
import Tooltip from 'tooltip'
import { Text } from 'editable'
import { editHost, fetchHostStats, getHostMissingPatches, installAllHostPatches, installHostPatch } from 'xo'
import { Container, Row, Col } from 'grid'
import {
editHost,
fetchHostStats,
installAllHostPatches,
installHostPatch,
subscribeHostMissingPatches
} from 'xo'
import {
connectStore,
routes
@@ -139,6 +145,10 @@ export default class Host extends Component {
}
loop (host = this.props.host) {
if (host == null) {
return
}
if (this.cancel) {
this.cancel()
}
@@ -166,22 +176,19 @@ export default class Host extends Component {
}
loop = ::this.loop
_getMissingPatches (host) {
getHostMissingPatches(host).then(missingPatches => {
this.setState({ missingPatches: sortBy(missingPatches, (patch) => -patch.time) })
})
}
componentWillMount () {
if (!this.props.host) {
return
}
componentDidMount () {
this.loop()
this._getMissingPatches(this.props.host)
this.unsubscribeHostMissingPatches = subscribeHostMissingPatches(
this.props.routeParams.id,
missingPatches => this.setState({
missingPatches: sortBy(missingPatches, patch => -patch.time)
})
)
}
componentWillUnmount () {
clearTimeout(this.timeout)
this.unsubscribeHostMissingPatches()
}
componentWillReceiveProps (props) {
@@ -195,10 +202,6 @@ export default class Host extends Component {
this.context.router.push('/')
}
if (!hostCur) {
this._getMissingPatches(hostNext)
}
if (!isRunning(hostCur) && isRunning(hostNext)) {
this.loop(hostNext)
} else if (isRunning(hostCur) && !isRunning(hostNext)) {
@@ -210,16 +213,12 @@ export default class Host extends Component {
_installAllPatches = () => {
const { host } = this.props
return installAllHostPatches(host).then(() => {
this._getMissingPatches(host)
})
return installAllHostPatches(host)
}
_installPatch = patch => {
const { host } = this.props
return installHostPatch(host, patch).then(() => {
this._getMissingPatches(host)
})
return installHostPatch(host, patch)
}
_setNameDescription = nameDescription => editHost(this.props.host, { name_description: nameDescription })
@@ -235,7 +234,7 @@ export default class Host extends Component {
<Row>
<Col mediumSize={6} className='header-title'>
<h2>
<Icon icon={`host-${host.power_state.toLowerCase()}`} />
<Icon icon={host.power_state === 'Running' && !host.enabled ? 'host-disabled' : `host-${host.power_state.toLowerCase()}`} />
{' '}
<Text
value={host.name_label}
@@ -256,7 +255,6 @@ export default class Host extends Component {
</div>
</Col>
</Row>
<br />
<Row>
<Col>
<NavTabs>

View File

@@ -5,7 +5,7 @@ import TabButton from 'tab-button'
import SelectFiles from 'select-files'
import Upgrade from 'xoa-upgrade'
import { Toggle } from 'form'
import { enableHost, detachHost, disableHost, restartHost, installSupplementalPack } from 'xo'
import { enableHost, detachHost, disableHost, forgetHost, restartHost, installSupplementalPack } from 'xo'
import { FormattedRelative, FormattedTime } from 'react-intl'
import { Container, Row, Col } from 'grid'
import {
@@ -59,6 +59,15 @@ export default ({
icon='host-eject'
labelId='detachHost'
/>
{host.power_state !== 'Running' &&
<TabButton
btnStyle='danger'
handler={forgetHost}
handlerParam={host}
icon='host-forget'
labelId='forgetHost'
/>
}
</Col>
</Row>
<Row>
@@ -103,7 +112,7 @@ export default ({
</tr>
<tr>
<th>{_('hostXenServerVersion')}</th>
<Copiable tagName='td'>
<Copiable tagName='td' data={host.version}>
{host.license_params.sku_marketing_name} {host.version} ({host.license_params.sku_type})
</Copiable>
</tr>

View File

@@ -1,4 +1,5 @@
import _ from 'intl'
import Button from 'button'
import Component from 'base-component'
import CopyToClipboard from 'react-copy-to-clipboard'
import debounce from 'lodash/debounce'
@@ -73,26 +74,24 @@ export default class extends Component {
<input type='text' className='form-control' ref='clipboard' onChange={this._setRemoteClipboard} />
<span className='input-group-btn'>
<CopyToClipboard text={this.state.clipboard || ''}>
<button className='btn btn-secondary'>
<Button>
<Icon icon='clipboard' /> {_('copyToClipboardLabel')}
</button>
</Button>
</CopyToClipboard>
</span>
</div>
</Col>
<Col mediumSize={2}>
<button
className='btn btn-secondary'
<Button
onClick={this._sendCtrlAltDel}
>
<Icon icon='vm-keyboard' /> {_('ctrlAltDelButtonLabel')}
</button>
</Button>
</Col>
</Row>
<Row className='console'>
<Col>
<NoVnc ref='noVnc' url={resolveUrl(`consoles/${vmController.id}`)} onClipboardChange={this._getRemoteClipboard} />
<p><em><Icon icon='info' /> {_('tipLabel')} {_('tipConsoleLabel')}</em></p>
</Col>
</Row>
</Container>

View File

@@ -85,11 +85,11 @@ export default ({
<Usage total={host.memory.size}>
<UsageElement
highlight
tooltip='XenServer'
tooltip={`XenServer (${formatSize(vmController.memory.size)})`}
value={vmController.memory.size}
/>
{map(vms, vm => <UsageElement
tooltip={vm.name_label}
tooltip={`${vm.name_label} (${formatSize(vm.memory.size)})`}
key={vm.id}
value={vm.memory.size}
href={`#/vms/${vm.id}`}

View File

@@ -90,6 +90,8 @@ class ConfigureIpModal extends Component {
vifsByNetwork: createGetObjectsOfType('VIF').groupBy('$network')
}))
class PifItem extends Component {
state = { configModes: [] }
componentWillMount () {
getIpv4ConfigModes().then(configModes =>
this.setState({ configModes })
@@ -126,7 +128,7 @@ class PifItem extends Component {
const pifInUse = some(vifsByNetwork[pif.$network], vif => vif.attached)
return <tr key={pif.id}>
return <tr>
<td>{pif.device}</td>
<td>{networks[pif.$network].name_label}</td>
<td>
@@ -190,7 +192,6 @@ class PifItem extends Component {
</td>
<td className='text-xs-right'>
<ActionRowButton
btnStyle='default'
disabled={pif.physical || pif.disallowUnplug || pif.management}
handler={deletePif}
handlerParam={pif}
@@ -202,7 +203,7 @@ class PifItem extends Component {
}
}
export default (({
export default ({
host,
networks,
pifs,
@@ -239,7 +240,7 @@ export default (({
</tr>
</thead>
<tbody>
{map(pifs, pif => <PifItem pif={pif} networks={networks} />)}
{map(pifs, pif => <PifItem key={pif.id} pif={pif} networks={networks} />)}
</tbody>
</table>
</span>
@@ -247,4 +248,4 @@ export default (({
}
</Col>
</Row>
</Container>)
</Container>

View File

@@ -4,6 +4,7 @@ import React, { Component } from 'react'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Upgrade from 'xoa-upgrade'
import { chooseAction } from 'modal'
import { connectStore, formatSize } from 'utils'
import { Container, Row, Col } from 'grid'
import { createDoesHostNeedRestart, createSelector } from 'selectors'
@@ -42,11 +43,10 @@ const MISSING_PATCH_COLUMNS = [
},
{
name: _('patchAction'),
itemRenderer: (patch, installPatch) => (
itemRenderer: (patch, {installPatch, _installPatchWarning}) => (
<ActionRowButton
btnStyle='primary'
handler={installPatch}
handlerParam={patch}
handler={() => _installPatchWarning(patch, installPatch)}
icon='host-patch-update'
/>
)
@@ -111,6 +111,29 @@ const INSTALLED_PATCH_COLUMNS_2 = [
needsRestart: createDoesHostNeedRestart((_, props) => props.host)
}))
export default class HostPatches extends Component {
static contextTypes = {
router: React.PropTypes.object
}
_chooseActionPatch = async doInstall => {
const choice = await chooseAction({
body: <p>{_('installPatchWarningContent')}</p>,
buttons: [
{ label: _('installPatchWarningResolve'), value: 'install', btnStyle: 'primary' },
{ label: _('installPatchWarningReject'), value: 'goToPool' }
],
title: _('installPatchWarningTitle')
})
return choice === 'install'
? doInstall()
: this.context.router.push(`/pools/${this.props.host.$pool}/patches`)
}
_installPatchWarning = (patch, installPatch) => this._chooseActionPatch(() => installPatch(patch))
_installAllPatchesWarning = installAllPatches => this._chooseActionPatch(installAllPatches)
_getPatches = createSelector(
() => this.props.host,
() => this.props.hostPatches,
@@ -136,7 +159,7 @@ export default class HostPatches extends Component {
render () {
const { host, missingPatches, installAllPatches, installPatch } = this.props
const { patches, columns } = this._getPatches()
const hasMissingPatches = !isEmpty(missingPatches)
return process.env.XOA_PLAN > 1
? <Container>
<Row>
@@ -148,26 +171,20 @@ export default class HostPatches extends Component {
icon='host-reboot'
labelId='rebootUpdateHostLabel'
/>}
{isEmpty(missingPatches)
? <TabButton
disabled
handler={installAllPatches}
icon='success'
labelId='hostUpToDate'
/>
: <TabButton
btnStyle='primary'
handler={installAllPatches}
icon='host-patch-update'
labelId='patchUpdateButton'
/>
}
<TabButton
disabled={!hasMissingPatches}
btnStyle={hasMissingPatches ? 'primary' : undefined}
handler={this._installAllPatchesWarning}
handlerParam={installAllPatches}
icon={hasMissingPatches ? 'host-patch-update' : 'success'}
labelId={hasMissingPatches ? 'patchUpdateButton' : 'hostUpToDate'}
/>
</Col>
</Row>
{!isEmpty(missingPatches) && <Row>
{hasMissingPatches && <Row>
<Col>
<h3>{_('hostMissingPatches')}</h3>
<SortedTable collection={missingPatches} userData={installPatch} columns={MISSING_PATCH_COLUMNS} />
<SortedTable collection={missingPatches} userData={{installPatch, _installPatchWarning: this._installPatchWarning}} columns={MISSING_PATCH_COLUMNS} />
</Col>
</Row>}
<Row>

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