Compare commits
114 Commits
5.9.0
...
xo-web/v5.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb8a25cc9d | ||
|
|
54c3d843be | ||
|
|
4a1407786c | ||
|
|
f5e3aef86c | ||
|
|
37c8a7c2b2 | ||
|
|
1a788fae7e | ||
|
|
8efc083a70 | ||
|
|
f196a9ebc4 | ||
|
|
06704ce467 | ||
|
|
8524db2903 | ||
|
|
60df3bc633 | ||
|
|
5014b95206 | ||
|
|
a2464fa968 | ||
|
|
033153c8b9 | ||
|
|
a74a857ffe | ||
|
|
f0fe369cfd | ||
|
|
457ba5f24c | ||
|
|
d41b04313a | ||
|
|
34be34e7b3 | ||
|
|
dbc9fdcfa6 | ||
|
|
76b20f0fb6 | ||
|
|
80ca2052c2 | ||
|
|
3e5d8be507 | ||
|
|
114e5e1fa0 | ||
|
|
c38d4e275b | ||
|
|
8cc9dea9aa | ||
|
|
d3dcf6d305 | ||
|
|
02439bd23d | ||
|
|
a9eb1f3d27 | ||
|
|
9a0544c4aa | ||
|
|
31c365313b | ||
|
|
b44017ca95 | ||
|
|
289112af27 | ||
|
|
4d2dc4eece | ||
|
|
712101d8d6 | ||
|
|
828ba5d448 | ||
|
|
03a2ff8e8c | ||
|
|
75487203cf | ||
|
|
6ad751f079 | ||
|
|
78ddad839e | ||
|
|
812cecdcc4 | ||
|
|
4b49da7d8f | ||
|
|
fc8c37d66c | ||
|
|
0d618e6477 | ||
|
|
d7e2b12d3d | ||
|
|
2ae4ed3999 | ||
|
|
eaaf70e52e | ||
|
|
bb4ebcd198 | ||
|
|
a8b7431a02 | ||
|
|
a5ec70f7fa | ||
|
|
75dfdd4854 | ||
|
|
3f29dd129f | ||
|
|
7b19341406 | ||
|
|
838ad58946 | ||
|
|
ec4a7325da | ||
|
|
efbd588c9e | ||
|
|
e535e064fa | ||
|
|
448068178b | ||
|
|
7308d9ca96 | ||
|
|
e642f54815 | ||
|
|
ebb6cb17ea | ||
|
|
08a0aa9f98 | ||
|
|
8a933c98e3 | ||
|
|
363b22bffe | ||
|
|
79a85659aa | ||
|
|
eca145e113 | ||
|
|
04b75fb3b3 | ||
|
|
582e220a02 | ||
|
|
2e87abefc4 | ||
|
|
5ea19ee56f | ||
|
|
2ee733399e | ||
|
|
73f228c719 | ||
|
|
ba79673715 | ||
|
|
86b0962063 | ||
|
|
e2a482a6ca | ||
|
|
8924444cc1 | ||
|
|
8e83b0ffd2 | ||
|
|
451384bcdc | ||
|
|
b733164c50 | ||
|
|
72d9d8ba86 | ||
|
|
7c7646c65c | ||
|
|
b1c087451e | ||
|
|
f0f72f3bdd | ||
|
|
0ab3267541 | ||
|
|
995e76d323 | ||
|
|
62a9f805c2 | ||
|
|
84ea95a641 | ||
|
|
316de42cd9 | ||
|
|
bc2256fc86 | ||
|
|
f0d85f4c4e | ||
|
|
1801f9cb06 | ||
|
|
4be018ad15 | ||
|
|
5dcf060975 | ||
|
|
59f6b1f0c8 | ||
|
|
ae38a85b19 | ||
|
|
324dbbcfc8 | ||
|
|
9770b77df4 | ||
|
|
0f91de389a | ||
|
|
7f5a623b37 | ||
|
|
c7cf73ff05 | ||
|
|
4aab425cef | ||
|
|
0d9666639f | ||
|
|
6c26c09685 | ||
|
|
819f650b48 | ||
|
|
353eba6365 | ||
|
|
063302b91d | ||
|
|
562b51bc2f | ||
|
|
e33a6f9a05 | ||
|
|
b9db4e7704 | ||
|
|
3270d9c3a7 | ||
|
|
6d7399f96c | ||
|
|
886ef87bc5 | ||
|
|
1e5dc9efe7 | ||
|
|
28ec66bf3b |
96
CHANGELOG.md
96
CHANGELOG.md
@@ -1,5 +1,101 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.13.0** (2017-09-29)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* replace all '...' with the UTF-8 equivalent [#2391](https://github.com/vatesfr/xo-web/issues/2391)
|
||||
* [SortedTable] Explicit when no items [#2388](https://github.com/vatesfr/xo-web/issues/2388)
|
||||
* Auto select iqn or lun if there is only one [#2379](https://github.com/vatesfr/xo-web/issues/2379)
|
||||
* [Sparklines] Hide points [#2370](https://github.com/vatesfr/xo-web/issues/2370)
|
||||
* Allow xo-server-recover-account to generate a random password [#2360](https://github.com/vatesfr/xo-web/issues/2360)
|
||||
* Add disk in existing VM as self user [#2348](https://github.com/vatesfr/xo-web/issues/2348)
|
||||
* Sorted table for Settings/server [#2340](https://github.com/vatesfr/xo-web/issues/2340)
|
||||
* Sign in should be case insensitive [#2337](https://github.com/vatesfr/xo-web/issues/2337)
|
||||
* [SortedTable] Extend checkbox click to whole column [#2329](https://github.com/vatesfr/xo-web/issues/2329)
|
||||
* [SortedTable] Ability to select all items (across pages) [#2324](https://github.com/vatesfr/xo-web/issues/2324)
|
||||
* [SortedTable] Range selection [#2323](https://github.com/vatesfr/xo-web/issues/2323)
|
||||
* Warning on SMB remote creation [#2316](https://github.com/vatesfr/xo-web/issues/2316)
|
||||
* [Home | SortedTable] Add link to syntax doc in the filter input [#2305](https://github.com/vatesfr/xo-web/issues/2305)
|
||||
* [SortedTable] Add optional binding of filter to an URL query [#2301](https://github.com/vatesfr/xo-web/issues/2301)
|
||||
* [Home][Keyboard navigation] Allow selecting the objects [#2214](https://github.com/vatesfr/xo-web/issues/2214)
|
||||
* SR view / Disks: option to display non managed VDIs [#1724](https://github.com/vatesfr/xo-web/issues/1724)
|
||||
* Continuous Replication Retention [#1692](https://github.com/vatesfr/xo-web/issues/1692)
|
||||
|
||||
### Bugs
|
||||
|
||||
* iSCSI issue on LUN selector [#2374](https://github.com/vatesfr/xo-web/issues/2374)
|
||||
* Errors in VM copy are not properly reported [#2347](https://github.com/vatesfr/xo-web/issues/2347)
|
||||
* Removing a PIF IP fails [#2346](https://github.com/vatesfr/xo-web/issues/2346)
|
||||
* Review and fix creating a VM from a snapshot [#2343](https://github.com/vatesfr/xo-web/issues/2343)
|
||||
* iSCSI LUN Detection fails with authentification [#2339](https://github.com/vatesfr/xo-web/issues/2339)
|
||||
* Fix PoolActionBar to add a new SR [#2307](https://github.com/vatesfr/xo-web/issues/2307)
|
||||
* [VM migration] Error if default SR not accessible to target host [#2180](https://github.com/vatesfr/xo-web/issues/2180)
|
||||
* A job shouldn't executable more than once at the same time [#2053](https://github.com/vatesfr/xo-web/issues/2053)
|
||||
|
||||
## **5.12.0** (2017-08-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* PIF selector with physical status [#2326](https://github.com/vatesfr/xo-web/issues/2326)
|
||||
* [SortedTable] Range selection [#2323](https://github.com/vatesfr/xo-web/issues/2323)
|
||||
* Self service filter for home/VM view [#2303](https://github.com/vatesfr/xo-web/issues/2303)
|
||||
* SR/Disks Display total of VDIs to coalesce [#2300](https://github.com/vatesfr/xo-web/issues/2300)
|
||||
* Pool filter in the task view [#2293](https://github.com/vatesfr/xo-web/issues/2293)
|
||||
* "Loading" while fetching objects [#2285](https://github.com/vatesfr/xo-web/issues/2285)
|
||||
* [SortedTable] Add grouped actions feature [#2276](https://github.com/vatesfr/xo-web/issues/2276)
|
||||
* Add a filter to the backups' log [#2246](https://github.com/vatesfr/xo-web/issues/2246)
|
||||
* It should not be possible to migrate a halted VM. [#2233](https://github.com/vatesfr/xo-web/issues/2233)
|
||||
* [Home][Keyboard navigation] Allow selecting the objects [#2214](https://github.com/vatesfr/xo-web/issues/2214)
|
||||
* Allow to set pool master [#2213](https://github.com/vatesfr/xo-web/issues/2213)
|
||||
* Continuous Replication Retention [#1692](https://github.com/vatesfr/xo-web/issues/1692)
|
||||
|
||||
### Bugs
|
||||
|
||||
* Home pagination bug [#2310](https://github.com/vatesfr/xo-web/issues/2310)
|
||||
* Fix PoolActionBar to add a new SR [#2307](https://github.com/vatesfr/xo-web/issues/2307)
|
||||
* VM snapshots are not correctly deleted [#2304](https://github.com/vatesfr/xo-web/issues/2304)
|
||||
* Parallel deletion of VMs fails [#2297](https://github.com/vatesfr/xo-web/issues/2297)
|
||||
* Continous replication create multiple zombie disks [#2292](https://github.com/vatesfr/xo-web/issues/2292)
|
||||
* Add user to Group issue [#2196](https://github.com/vatesfr/xo-web/issues/2196)
|
||||
* [VM migration] Error if default SR not accessible to target host [#2180](https://github.com/vatesfr/xo-web/issues/2180)
|
||||
|
||||
## **5.11.0** (2017-07-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Storage VHD chain health [\#2178](https://github.com/vatesfr/xo-web/issues/2178)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- No web VNC console [\#2258](https://github.com/vatesfr/xo-web/issues/2258)
|
||||
- Patching issues [\#2254](https://github.com/vatesfr/xo-web/issues/2254)
|
||||
- Advanced button in VM creation for self service user [\#2202](https://github.com/vatesfr/xo-web/issues/2202)
|
||||
- Hide "new VM" menu entry if not admin or not self service user [\#2191](https://github.com/vatesfr/xo-web/issues/2191)
|
||||
|
||||
## **5.10.0** (2017-06-30)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Improve backup log display [\#2239](https://github.com/vatesfr/xo-web/issues/2239)
|
||||
- Patch SR detection improvement [\#2215](https://github.com/vatesfr/xo-web/issues/2215)
|
||||
- Less strict coalesce detection [\#2207](https://github.com/vatesfr/xo-web/issues/2207)
|
||||
- IP pool UI improvement [\#2203](https://github.com/vatesfr/xo-web/issues/2203)
|
||||
- Ability to clear "Auto power on" flag for DR-ed VM [\#2097](https://github.com/vatesfr/xo-web/issues/2097)
|
||||
- [Delta backup restoration] Choose SR for each VDIs [\#2070](https://github.com/vatesfr/xo-web/issues/2070)
|
||||
- Ability to forget an host (even if no longer present) [\#1934](https://github.com/vatesfr/xo-web/issues/1934)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Cross pool migrate fail [\#2248](https://github.com/vatesfr/xo-web/issues/2248)
|
||||
- ActionButtons with modals stay in pending state forever [\#2222](https://github.com/vatesfr/xo-web/issues/2222)
|
||||
- Permission issue for a user on self service VMs [\#2212](https://github.com/vatesfr/xo-web/issues/2212)
|
||||
- Self-Service resource loophole [\#2198](https://github.com/vatesfr/xo-web/issues/2198)
|
||||
- Backup log no longer shows the name of destination VM [\#2195](https://github.com/vatesfr/xo-web/issues/2195)
|
||||
- State not restored when exiting modal dialog [\#2194](https://github.com/vatesfr/xo-web/issues/2194)
|
||||
- [Xapi#exportDeltaVm] Cannot read property 'managed' of undefined [\#2189](https://github.com/vatesfr/xo-web/issues/2189)
|
||||
- VNC keyboard layout change [\#404](https://github.com/vatesfr/xo-web/issues/404)
|
||||
|
||||
## **5.9.0** (2017-05-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
})
|
||||
|
||||
74
package.json
74
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.9.0",
|
||||
"version": "5.13.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -31,8 +31,9 @@
|
||||
"npm": ">=3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nraynaud/novnc": "^0.6.1-1",
|
||||
"ansi_up": "^1.3.0",
|
||||
"asap": "^2.0.4",
|
||||
"asap": "^2.0.6",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"babel-plugin-dev": "^1.0.0",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
@@ -45,21 +46,21 @@
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"babel-register": "^6.16.3",
|
||||
"babel-runtime": "^6.6.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"babelify": "^7.2.0",
|
||||
"benchmark": "^2.1.0",
|
||||
"bootstrap": "4.0.0-alpha.5",
|
||||
"browserify": "^14.1.0",
|
||||
"bundle-collapser": "^1.2.1",
|
||||
"bundle-collapser": "^1.3.0",
|
||||
"chartist": "^0.10.1",
|
||||
"chartist-plugin-legend": "^0.6.1",
|
||||
"chartist-plugin-tooltip": "0.0.11",
|
||||
"classnames": "^2.2.3",
|
||||
"cookies-js": "^1.2.2",
|
||||
"d3": "^4.2.8",
|
||||
"d3": "^4.10.2",
|
||||
"dependency-check": "^2.5.1",
|
||||
"enzyme": "^2.6.0",
|
||||
"enzyme": "^2.9.1",
|
||||
"enzyme-to-json": "^1.4.4",
|
||||
"event-to-promise": "^0.8.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
@@ -67,7 +68,7 @@
|
||||
"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.8.0",
|
||||
"husky": "^0.13.1",
|
||||
"husky": "^0.14.3",
|
||||
"immutable": "^3.8.1",
|
||||
"index-modules": "^0.3.0",
|
||||
"is-ip": "^1.0.0",
|
||||
"jest": "^19.0.2",
|
||||
"jest": "^21.0.2",
|
||||
"jsonrpc-websocket-client": "^0.1.1",
|
||||
"kindof": "^2.0.0",
|
||||
"later": "^1.2.0",
|
||||
@@ -89,51 +91,51 @@
|
||||
"loose-envify": "^1.1.0",
|
||||
"make-error": "^1.2.1",
|
||||
"marked": "^0.3.5",
|
||||
"modular-css": "^4.1.1",
|
||||
"modular-css": "^6.0.2",
|
||||
"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.5",
|
||||
"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-dnd": "^2.1.4",
|
||||
"react-dnd-html5-backend": "^2.1.2",
|
||||
"react-chartist": "^0.13.0",
|
||||
"react-copy-to-clipboard": "^5.0.0",
|
||||
"react-debounce-input": "^3.0.1",
|
||||
"react-dnd": "^2.5.1",
|
||||
"react-dnd-html5-backend": "^2.5.1",
|
||||
"react-document-title": "^2.0.2",
|
||||
"react-dom": "^15.4.1",
|
||||
"react-dropzone": "^3.5.0",
|
||||
"react-intl": "^2.0.1",
|
||||
"react-key-handler": "^0.3.0",
|
||||
"react-notify": "^2.0.1",
|
||||
"react-overlays": "^0.6.0",
|
||||
"react-redux": "^5.0.0",
|
||||
"react-dropzone": "^4.1.2",
|
||||
"react-intl": "^2.4.0",
|
||||
"react-key-handler": "^1.0.0",
|
||||
"react-notify": "^3.0.0",
|
||||
"react-overlays": "^0.8.0",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-router": "^3.0.0",
|
||||
"react-select": "^1.0.0-rc.4",
|
||||
"react-shortcuts": "^1.3.1",
|
||||
"react-sparklines": "^1.5.0",
|
||||
"react-select": "^1.0.0-rc.8",
|
||||
"react-shortcuts": "^1.6.1",
|
||||
"react-sparklines": "1.6.0",
|
||||
"react-virtualized": "^8.0.8",
|
||||
"readable-stream": "^2.0.6",
|
||||
"redux": "^3.3.1",
|
||||
"readable-stream": "^2.3.3",
|
||||
"redux": "^3.7.2",
|
||||
"redux-devtools": "^3.1.1",
|
||||
"redux-devtools-dock-monitor": "^1.1.0",
|
||||
"redux-devtools-log-monitor": "^1.0.5",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.5.4",
|
||||
"semver": "^5.3.0",
|
||||
"standard": "^10.0.0",
|
||||
"styled-components": "^1.4.4",
|
||||
"superagent": "^3.5.0",
|
||||
"semver": "^5.4.1",
|
||||
"standard": "^10.0.3",
|
||||
"styled-components": "^2.1.2",
|
||||
"superagent": "^3.6.0",
|
||||
"tar-stream": "^1.5.2",
|
||||
"uglify-es": "^3.1.0",
|
||||
"uncontrollable-input": "^0.0.1",
|
||||
"vinyl": "^2.0.0",
|
||||
"vinyl": "^2.1.0",
|
||||
"watchify": "^3.7.0",
|
||||
"xml2js": "^0.4.17",
|
||||
"xml2js": "^0.4.19",
|
||||
"xo-acl-resolver": "^0.2.3",
|
||||
"xo-common": "^0.1.1",
|
||||
"xo-lib": "^0.8.0",
|
||||
|
||||
@@ -1,47 +1,50 @@
|
||||
import React from 'react'
|
||||
import { map, noop } from 'lodash'
|
||||
import ActionButton from 'action-button'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React, { cloneElement } from 'react'
|
||||
import { noop } from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
import ActionButton from './action-button'
|
||||
import ButtonGroup from './button-group'
|
||||
|
||||
const ActionBar = ({ actions, param }) => (
|
||||
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,
|
||||
icon,
|
||||
label,
|
||||
pending,
|
||||
redirectOnSuccess
|
||||
} = button
|
||||
return <ActionButton
|
||||
key={index}
|
||||
handler={handler || noop}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
pending={pending}
|
||||
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 }
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import clone from 'lodash/clone'
|
||||
import includes from 'lodash/includes'
|
||||
import isArray from 'lodash/isArray'
|
||||
import forEach from 'lodash/forEach'
|
||||
import map from 'lodash/map'
|
||||
import { PureComponent } from 'react'
|
||||
import { cowSet } from 'utils'
|
||||
import {
|
||||
includes,
|
||||
isArray,
|
||||
forEach,
|
||||
map
|
||||
} from 'lodash'
|
||||
|
||||
import getEventValue from './get-event-value'
|
||||
|
||||
@@ -12,17 +14,6 @@ import getEventValue from './get-event-value'
|
||||
// Usually set to process.env.NODE_ENV !== 'production'.
|
||||
const VERBOSE = false
|
||||
|
||||
const cowSet = (object, path, value, depth) => {
|
||||
if (depth >= path.length) {
|
||||
return value
|
||||
}
|
||||
|
||||
object = object != null ? clone(object) : {}
|
||||
const prop = path[depth]
|
||||
object[prop] = cowSet(object[prop], path, value, depth + 1)
|
||||
return object
|
||||
}
|
||||
|
||||
const get = (object, path, depth) => {
|
||||
if (depth >= path.length) {
|
||||
return object
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -390,7 +390,6 @@ const MAP_TYPE_SELECT = {
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
labelProp: propTypes.string.isRequired,
|
||||
value: propTypes.oneOfType([
|
||||
propTypes.string,
|
||||
propTypes.object
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
|
||||
import Button from '../button'
|
||||
import Component from '../base-component'
|
||||
import defined from '../xo-defined'
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import {
|
||||
firstDefined,
|
||||
formatSizeRaw,
|
||||
parseSize
|
||||
} from '../utils'
|
||||
@@ -158,7 +158,7 @@ export class SizeInput extends BaseComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = this._createStateFromBytes(firstDefined(props.value, props.defaultValue, null))
|
||||
this.state = this._createStateFromBytes(defined(props.value, props.defaultValue, null))
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,17 +4,19 @@ import React from 'react'
|
||||
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const Icon = ({ icon, size = 1, fixedWidth, ...props }) => {
|
||||
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([
|
||||
|
||||
@@ -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
@@ -204,7 +204,7 @@ export default {
|
||||
editUserProfile: undefined,
|
||||
|
||||
// Original text: "Fetching data…"
|
||||
homeFetchingData: 'מקבל נתונים, נא להמתין...',
|
||||
homeFetchingData: 'מקבל נתונים, נא להמתין…',
|
||||
|
||||
// Original text: "Welcome on Xen Orchestra!"
|
||||
homeWelcome: 'ברוכים הבאים',
|
||||
@@ -228,7 +228,7 @@ export default {
|
||||
homeNoVms: 'אין מכונות',
|
||||
|
||||
// Original text: "Or…"
|
||||
homeNoVmsOr: 'או...',
|
||||
homeNoVmsOr: 'או…',
|
||||
|
||||
// Original text: "Import VM"
|
||||
homeImportVm: 'ההלעה של מכונה',
|
||||
@@ -330,7 +330,7 @@ export default {
|
||||
homeMore: 'עוד',
|
||||
|
||||
// Original text: "Migrate to…"
|
||||
homeMigrateTo: 'העבר ל...',
|
||||
homeMigrateTo: 'העבר ל…',
|
||||
|
||||
// Original text: 'Missing patches'
|
||||
homeMissingPaths: undefined,
|
||||
|
||||
@@ -1427,8 +1427,8 @@ export default {
|
||||
// Original text: "Installation started"
|
||||
supplementalPackInstallStartedTitle: 'Installation Started',
|
||||
|
||||
// Original text: "Installing new supplemental pack..."
|
||||
supplementalPackInstallStartedMessage: 'Installing new supplemental pack...',
|
||||
// Original text: "Installing new supplemental pack…"
|
||||
supplementalPackInstallStartedMessage: 'Installing new supplemental pack…',
|
||||
|
||||
// Original text: "Installation error"
|
||||
supplementalPackInstallErrorTitle: 'Installation error',
|
||||
@@ -2894,8 +2894,8 @@ export default {
|
||||
// Original text: "Connection failed"
|
||||
serverConnectionFailed: 'Csatlakozás Sikertelen',
|
||||
|
||||
// Original text: "Connecting..."
|
||||
serverConnecting: 'Csatlakozás...',
|
||||
// Original text: "Connecting…"
|
||||
serverConnecting: 'Csatlakozás…',
|
||||
|
||||
// Original text: "Connected"
|
||||
serverConnected: 'Kapcsolódva',
|
||||
@@ -3557,7 +3557,7 @@ export default {
|
||||
// Original text: 'Create'
|
||||
xosanCreate: undefined,
|
||||
|
||||
// Original text: 'Installing XOSAN. Please wait...'
|
||||
// Original text: 'Installing XOSAN. Please wait…'
|
||||
xosanInstalling: undefined,
|
||||
|
||||
// Original text: 'You need XenServer 7.0 to install XOSAN'
|
||||
@@ -3572,7 +3572,7 @@ export default {
|
||||
// Original text: 'Load cloud plugin first'
|
||||
xosanLoadCloudPlugin: undefined,
|
||||
|
||||
// Original text: 'Loading...'
|
||||
// Original text: 'Loading…'
|
||||
xosanLoading: undefined,
|
||||
|
||||
// Original text: 'XOSAN is not available at the moment'
|
||||
|
||||
@@ -204,7 +204,7 @@ export default {
|
||||
editUserProfile: undefined,
|
||||
|
||||
// Original text: "Fetching data…"
|
||||
homeFetchingData: 'Obtendo dados...',
|
||||
homeFetchingData: 'Obtendo dados…',
|
||||
|
||||
// Original text: "Welcome on Xen Orchestra!"
|
||||
homeWelcome: 'Bem-vindo ao Xen Orchestra',
|
||||
@@ -228,7 +228,7 @@ export default {
|
||||
homeNoVms: 'Não foram encontradas VMs!',
|
||||
|
||||
// Original text: "Or…"
|
||||
homeNoVmsOr: 'Ou...',
|
||||
homeNoVmsOr: 'Ou…',
|
||||
|
||||
// Original text: "Import VM"
|
||||
homeImportVm: 'Importar VM',
|
||||
@@ -330,7 +330,7 @@ export default {
|
||||
homeMore: 'Mais',
|
||||
|
||||
// Original text: "Migrate to…"
|
||||
homeMigrateTo: 'Migrar para...',
|
||||
homeMigrateTo: 'Migrar para…',
|
||||
|
||||
// Original text: 'Missing patches'
|
||||
homeMissingPaths: undefined,
|
||||
@@ -360,28 +360,28 @@ export default {
|
||||
selectSubjects: 'Escolha um usuário(s) e/ou grupo(s)',
|
||||
|
||||
// Original text: "Select Object(s)…"
|
||||
selectObjects: 'Selecionar Objeto(s)...',
|
||||
selectObjects: 'Selecionar Objeto(s)…',
|
||||
|
||||
// Original text: "Choose a role"
|
||||
selectRole: 'Escolha uma função',
|
||||
|
||||
// Original text: "Select Host(s)…"
|
||||
selectHosts: 'Selecionar Host(s)...',
|
||||
selectHosts: 'Selecionar Host(s)…',
|
||||
|
||||
// Original text: "Select object(s)…"
|
||||
selectHostsVms: 'Selecionar Objeto(s)...',
|
||||
selectHostsVms: 'Selecionar Objeto(s)…',
|
||||
|
||||
// Original text: "Select Network(s)…"
|
||||
selectNetworks: 'Selecionar Rede(s)...',
|
||||
selectNetworks: 'Selecionar Rede(s)…',
|
||||
|
||||
// Original text: "Select PIF(s)…"
|
||||
selectPifs: 'Selecionar PIF(s)...',
|
||||
selectPifs: 'Selecionar PIF(s)…',
|
||||
|
||||
// Original text: "Select Pool(s)…"
|
||||
selectPools: 'Selecionar Pool(s)...',
|
||||
selectPools: 'Selecionar Pool(s)…',
|
||||
|
||||
// Original text: "Select Remote(s)…"
|
||||
selectRemotes: 'Selecionar Remote(s)...',
|
||||
selectRemotes: 'Selecionar Remote(s)…',
|
||||
|
||||
// Original text: 'Select resource set(s)…'
|
||||
selectResourceSets: undefined,
|
||||
@@ -402,19 +402,19 @@ export default {
|
||||
selectSshKey: undefined,
|
||||
|
||||
// Original text: "Select SR(s)…"
|
||||
selectSrs: 'Selecionar SR(s)...',
|
||||
selectSrs: 'Selecionar SR(s)…',
|
||||
|
||||
// Original text: "Select VM(s)…"
|
||||
selectVms: 'Selecionar VM(s)...',
|
||||
selectVms: 'Selecionar VM(s)…',
|
||||
|
||||
// Original text: "Select VM template(s)…"
|
||||
selectVmTemplates: 'Selecionar VM(s) modelo(s)...',
|
||||
selectVmTemplates: 'Selecionar VM(s) modelo(s)…',
|
||||
|
||||
// Original text: "Select tag(s)…"
|
||||
selectTags: 'Selecionar etiqueta(s)...',
|
||||
selectTags: 'Selecionar etiqueta(s)…',
|
||||
|
||||
// Original text: "Select disk(s)…"
|
||||
selectVdis: 'Selecionar disco(s)...',
|
||||
selectVdis: 'Selecionar disco(s)…',
|
||||
|
||||
// Original text: 'Select timezone…'
|
||||
selectTimezone: undefined,
|
||||
@@ -1968,7 +1968,7 @@ export default {
|
||||
statsDashboardSelectObjects: 'Selecionar',
|
||||
|
||||
// Original text: "Loading…"
|
||||
metricsLoading: 'Carregando...',
|
||||
metricsLoading: 'Carregando…',
|
||||
|
||||
// Original text: "Coming soon!"
|
||||
comingSoon: 'Em breve!',
|
||||
@@ -2292,10 +2292,10 @@ export default {
|
||||
vmImportFailed: 'Falha na importação',
|
||||
|
||||
// Original text: "Import starting…"
|
||||
startVmImport: 'Iniciando importação...',
|
||||
startVmImport: 'Iniciando importação…',
|
||||
|
||||
// Original text: "Export starting…"
|
||||
startVmExport: 'Iniciando exportação...',
|
||||
startVmExport: 'Iniciando exportação…',
|
||||
|
||||
// Original text: 'N CPUs'
|
||||
nCpus: undefined,
|
||||
@@ -2559,7 +2559,7 @@ export default {
|
||||
importBackupModalStart: 'Iniciar VM após restauração',
|
||||
|
||||
// Original text: "Select your backup…"
|
||||
importBackupModalSelectBackup: 'Selecionar backup...',
|
||||
importBackupModalSelectBackup: 'Selecionar backup…',
|
||||
|
||||
// Original text: "Are you sure you want to remove all orphaned snapshot VDIs?"
|
||||
removeAllOrphanedModalWarning: 'Você tem certeza que deseja remover todos as VDIs orfãs?',
|
||||
|
||||
@@ -285,7 +285,7 @@ export default {
|
||||
homeMore: '更多',
|
||||
|
||||
// Original text: "Migrate to…"
|
||||
homeMigrateTo: '迁移至...',
|
||||
homeMigrateTo: '迁移至…',
|
||||
|
||||
// Original text: "Missing patches"
|
||||
homeMissingPaths: '缺少补丁',
|
||||
@@ -1467,7 +1467,7 @@ export default {
|
||||
statsDashboardSelectObjects: '选择',
|
||||
|
||||
// Original text: "Loading…"
|
||||
metricsLoading: '加载中....',
|
||||
metricsLoading: '加载中….',
|
||||
|
||||
// Original text: "Coming soon!"
|
||||
comingSoon: '即将呈现',
|
||||
@@ -1947,7 +1947,7 @@ export default {
|
||||
importBackupModalStart: '恢复后启动虚拟机',
|
||||
|
||||
// Original text: "Select your backup…"
|
||||
importBackupModalSelectBackup: '选择你的备份...',
|
||||
importBackupModalSelectBackup: '选择你的备份…',
|
||||
|
||||
// Original text: "Are you sure you want to remove all orphaned VDIs?"
|
||||
removeAllOrphanedModalWarning: '你确定要删除所有孤立的虚拟磁盘?',
|
||||
|
||||
@@ -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…',
|
||||
@@ -23,10 +25,13 @@ var messages = {
|
||||
// ----- Filters -----
|
||||
onError: 'On error',
|
||||
successful: 'Successful',
|
||||
filterNoSnapshots: 'Full disks only',
|
||||
filterOnlyBaseCopy: 'Base copy only',
|
||||
filterOnlyRegularDisks: 'Regular disks only',
|
||||
filterOnlySnapshots: 'Snapshots only',
|
||||
filterOnlyManaged: 'Managed disks',
|
||||
filterOnlyOrphaned: 'Orphaned disks',
|
||||
filterOnlyRegular: 'Normal disks',
|
||||
filterOnlySnapshots: 'Snapshot disks',
|
||||
filterOnlyUnmanaged: 'Unmanaged disks',
|
||||
filterSaveAs: 'Save…',
|
||||
filterSyntaxLinkTooltip: 'Explore the search syntax in the documentation',
|
||||
|
||||
// ----- Copiable component -----
|
||||
copyToClipboard: 'Copy to clipboard',
|
||||
@@ -100,7 +105,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?',
|
||||
@@ -126,6 +131,7 @@ var messages = {
|
||||
homeAllPools: 'Pools',
|
||||
homeAllHosts: 'Hosts',
|
||||
homeAllTags: 'Tags',
|
||||
homeAllResourceSets: 'Resource sets',
|
||||
homeNewVm: 'New VM',
|
||||
homeFilterNone: 'None',
|
||||
homeFilterRunningHosts: 'Running hosts',
|
||||
@@ -156,6 +162,14 @@ var messages = {
|
||||
srSharedType: 'Shared {type}',
|
||||
srNotSharedType: 'Not shared {type}',
|
||||
|
||||
// ----- Common components -----
|
||||
sortedTableAllItemsSelected: 'All of them are selected',
|
||||
sortedTableNoItems: 'No items found',
|
||||
sortedTableNumberOfFilteredItems: '{nFiltered, number} of {nTotal, number} items',
|
||||
sortedTableNumberOfItems: '{nTotal, number} items',
|
||||
sortedTableNumberOfSelectedItems: '{nSelected, number} selected',
|
||||
sortedTableSelectAllItems: 'Click here to select all items',
|
||||
|
||||
// ----- Forms -----
|
||||
add: 'Add',
|
||||
selectAll: 'Select all',
|
||||
@@ -217,6 +231,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',
|
||||
@@ -249,7 +269,7 @@ var messages = {
|
||||
noJobs: 'No jobs found.',
|
||||
noSchedules: 'No schedules found',
|
||||
jobActionPlaceHolder: 'Select a xo-server API command',
|
||||
jobTimeoutPlaceHolder: ' Timeout (number of seconds after which a VM is considered failed)',
|
||||
jobTimeoutPlaceHolder: 'Timeout (number of seconds after which a VM is considered failed)',
|
||||
jobSchedules: 'Schedules',
|
||||
jobScheduleNamePlaceHolder: 'Name of your schedule',
|
||||
jobScheduleJobPlaceHolder: 'Select a Job',
|
||||
@@ -287,6 +307,7 @@ var messages = {
|
||||
remoteTypeNfs: 'NFS',
|
||||
remoteTypeSmb: 'SMB',
|
||||
remoteType: 'Type',
|
||||
remoteSmbWarningMessage: 'SMB remotes are meant to work on Windows Server. For other systems (Linux Samba, which means almost all NAS), please use NFS.',
|
||||
remoteTestTip: 'Test your remote',
|
||||
testRemote: 'Test Remote',
|
||||
remoteTestFailure: 'Test failed for {name}',
|
||||
@@ -442,7 +463,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',
|
||||
@@ -466,6 +493,7 @@ var messages = {
|
||||
poolHaStatus: 'High Availability',
|
||||
poolHaEnabled: 'Enabled',
|
||||
poolHaDisabled: 'Disabled',
|
||||
setpoolMaster: 'Master',
|
||||
// ----- Pool host tab -----
|
||||
hostNameLabel: 'Name',
|
||||
hostDescription: 'Description',
|
||||
@@ -534,7 +562,7 @@ var messages = {
|
||||
hostCpusNumber: 'Core (socket)',
|
||||
hostManufacturerinfo: 'Manufacturer info',
|
||||
hostBiosinfo: 'BIOS info',
|
||||
licenseHostSettingsLabel: 'Licence',
|
||||
licenseHostSettingsLabel: 'License',
|
||||
hostLicenseType: 'Type',
|
||||
hostLicenseSocket: 'Socket',
|
||||
hostLicenseExpiry: 'Expiry',
|
||||
@@ -543,7 +571,7 @@ var messages = {
|
||||
supplementalPackPoolNew: 'Install supplemental pack on every host',
|
||||
supplementalPackTitle: '{name} (by {author})',
|
||||
supplementalPackInstallStartedTitle: 'Installation started',
|
||||
supplementalPackInstallStartedMessage: 'Installing new supplemental pack...',
|
||||
supplementalPackInstallStartedMessage: 'Installing new supplemental pack…',
|
||||
supplementalPackInstallErrorTitle: 'Installation error',
|
||||
supplementalPackInstallErrorMessage: 'The installation of the supplemental pack failed.',
|
||||
supplementalPackInstallSuccessTitle: 'Installation success',
|
||||
@@ -595,7 +623,7 @@ var messages = {
|
||||
patchStatus: 'Status',
|
||||
patchStatusApplied: 'Applied',
|
||||
patchStatusNotApplied: 'Missing patches',
|
||||
patchNothing: 'No patch detected',
|
||||
patchNothing: 'No patches detected',
|
||||
patchReleaseDate: 'Release date',
|
||||
patchGuidance: 'Guidance',
|
||||
patchAction: 'Action',
|
||||
@@ -660,7 +688,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',
|
||||
|
||||
@@ -715,6 +742,10 @@ var messages = {
|
||||
cdDriveInstallation: 'Stop and start the VM to install the CD drive',
|
||||
saveBootOption: 'Save',
|
||||
resetBootOption: 'Reset',
|
||||
deleteSelectedVdis: 'Delete selected VDIs',
|
||||
deleteSelectedVdi: 'Delete selected VDI',
|
||||
useQuotaWarning: 'Creating this disk will use the disk space quota from the resource set {resourceSet} ({spaceLeft} left)',
|
||||
notEnoughSpaceInResourceSet: 'Not enough space in resource set {resourceSet} ({spaceLeft} left)',
|
||||
|
||||
// ----- VM network tab -----
|
||||
vifCreateDeviceButton: 'New device',
|
||||
@@ -879,7 +910,6 @@ var messages = {
|
||||
|
||||
// ----- New VM -----
|
||||
newVmCreateNewVmOn: 'Create a new VM on {select}',
|
||||
newVmCreateNewVmOn2: 'Create a new VM on {select1} or {select2}',
|
||||
newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
|
||||
newVmInfoPanel: 'Infos',
|
||||
newVmNameLabel: 'Name',
|
||||
@@ -1010,6 +1040,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',
|
||||
@@ -1073,20 +1107,26 @@ var messages = {
|
||||
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?',
|
||||
@@ -1128,7 +1168,7 @@ var messages = {
|
||||
serverAddFailed: 'Adding server failed',
|
||||
serverStatus: 'Status',
|
||||
serverConnectionFailed: 'Connection failed. Click for more information.',
|
||||
serverConnecting: 'Connecting...',
|
||||
serverConnecting: 'Connecting…',
|
||||
serverConnected: 'Connected',
|
||||
serverDisconnected: 'Disconnected',
|
||||
serverAuthFailed: 'Authentication error',
|
||||
@@ -1153,6 +1193,16 @@ var messages = {
|
||||
detachHostModalMessage: 'Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST.',
|
||||
detachHost: 'Detach',
|
||||
|
||||
// ----- Forget host -----
|
||||
forgetHostModalTitle: 'Forget host',
|
||||
forgetHostModalMessage: 'Are you sure you want to forget {host} from its pool? Be sure this host can\'t be back online, or use detach instead.',
|
||||
forgetHost: 'Forget',
|
||||
|
||||
// ----- Set pool master -----
|
||||
|
||||
setPoolMasterModalTitle: 'Designate a new master',
|
||||
setPoolMasterModalMessage: 'This operation may take several minutes. Do you want to continue?',
|
||||
|
||||
// ----- Network -----
|
||||
newNetworkCreate: 'Create network',
|
||||
newBondedNetworkCreate: 'Create bonded network',
|
||||
@@ -1222,7 +1272,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',
|
||||
@@ -1324,19 +1374,25 @@ var messages = {
|
||||
// ----- Shortcuts -----
|
||||
shortcutModalTitle: 'Keyboard shortcuts',
|
||||
shortcut_XoApp: 'Global',
|
||||
shortcut_GO_TO_HOSTS: 'Go to hosts list',
|
||||
shortcut_GO_TO_POOLS: 'Go to pools list',
|
||||
shortcut_GO_TO_VMS: 'Go to VMs list',
|
||||
shortcut_GO_TO_SRS: 'Go to SRs list',
|
||||
shortcut_CREATE_VM: 'Create a new VM',
|
||||
shortcut_UNFOCUS: 'Unfocus field',
|
||||
shortcut_HELP: 'Show shortcuts key bindings',
|
||||
shortcut_XoApp_GO_TO_HOSTS: 'Go to hosts list',
|
||||
shortcut_XoApp_GO_TO_POOLS: 'Go to pools list',
|
||||
shortcut_XoApp_GO_TO_VMS: 'Go to VMs list',
|
||||
shortcut_XoApp_GO_TO_SRS: 'Go to SRs list',
|
||||
shortcut_XoApp_CREATE_VM: 'Create a new VM',
|
||||
shortcut_XoApp_UNFOCUS: 'Unfocus field',
|
||||
shortcut_XoApp_HELP: 'Show shortcuts key bindings',
|
||||
shortcut_Home: 'Home',
|
||||
shortcut_SEARCH: 'Focus search bar',
|
||||
shortcut_NAV_DOWN: 'Next item',
|
||||
shortcut_NAV_UP: 'Previous item',
|
||||
shortcut_SELECT: 'Select item',
|
||||
shortcut_JUMP_INTO: 'Open',
|
||||
shortcut_Home_SEARCH: 'Focus search bar',
|
||||
shortcut_Home_NAV_DOWN: 'Next item',
|
||||
shortcut_Home_NAV_UP: 'Previous item',
|
||||
shortcut_Home_SELECT: 'Select item',
|
||||
shortcut_Home_JUMP_INTO: 'Open',
|
||||
shortcut_SortedTable: 'Supported tables',
|
||||
shortcut_SortedTable_SEARCH: 'Focus the table search bar',
|
||||
shortcut_SortedTable_NAV_DOWN: 'Next item',
|
||||
shortcut_SortedTable_NAV_UP: 'Previous item',
|
||||
shortcut_SortedTable_SELECT: 'Select item',
|
||||
shortcut_SortedTable_ROW_ACTION: 'Action',
|
||||
|
||||
// ----- Settings/ACLs -----
|
||||
settingsAclsButtonTooltipVM: 'VM',
|
||||
@@ -1378,7 +1434,8 @@ var messages = {
|
||||
xosanSuggestions: 'Suggestions',
|
||||
xosanName: 'Name',
|
||||
xosanHost: 'Host',
|
||||
xosanHosts: 'Hosts',
|
||||
xosanHosts: 'Connected Hosts',
|
||||
xosanPool: 'Pool',
|
||||
xosanVolumeId: 'Volume ID',
|
||||
xosanSize: 'Size',
|
||||
xosanUsedSpace: 'Used space',
|
||||
@@ -1395,20 +1452,78 @@ var messages = {
|
||||
xosanAvailableSpace: 'Available space',
|
||||
xosanDiskLossLegend: '* Can fail without data loss',
|
||||
xosanCreate: 'Create',
|
||||
xosanInstalling: 'Installing XOSAN. Please wait...',
|
||||
xosanAdd: 'Add',
|
||||
xosanInstalling: 'Installing XOSAN. Please wait…',
|
||||
xosanCommunity: 'No XOSAN available for Community Edition',
|
||||
xosanNew: 'New',
|
||||
xosanAdvanced: 'Advanced',
|
||||
xosanRemoveSubvolumes: 'Remove subvolumes',
|
||||
xosanAddSubvolume: 'Add subvolume…',
|
||||
xosanWarning: 'This version of XOSAN SR is from the first beta phase. You can keep using it, but to modify it you\'ll have to save your disks and re-create it.',
|
||||
xosanVlan: 'VLAN',
|
||||
xosanNoSrs: 'No XOSAN found',
|
||||
xosanPbdsDetached: 'Some SRs are detached from the XOSAN',
|
||||
xosanBadStatus: 'Something is wrong with: {badStatuses}',
|
||||
xosanRunning: 'Running',
|
||||
xosanDelete: 'Delete XOSAN',
|
||||
xosanFixIssue: 'Fix',
|
||||
xosanCreatingOn: 'Creating XOSAN on {pool}',
|
||||
xosanState_configuringNetwork: 'Configuring network…',
|
||||
xosanState_importingVm: 'Importing VM…',
|
||||
xosanState_copyingVms: 'Copying VMs…',
|
||||
xosanState_configuringVms: 'Configuring VMs…',
|
||||
xosanState_configuringGluster: 'Configuring gluster…',
|
||||
xosanState_creatingSr: 'Creating SR…',
|
||||
xosanState_scanningSr: 'Scanning SR…',
|
||||
// Pack download modal
|
||||
xosanInstallCloudPlugin: 'Install cloud plugin first',
|
||||
xosanLoadCloudPlugin: 'Load cloud plugin first',
|
||||
xosanLoading: 'Loading...',
|
||||
xosanLoading: 'Loading…',
|
||||
xosanNotAvailable: 'XOSAN is not available at the moment',
|
||||
xosanRegisterBeta: 'Register for the XOSAN beta',
|
||||
xosanSuccessfullyRegistered: 'You have successfully registered for the XOSAN beta. Please wait until your request has been approved.',
|
||||
xosanInstallPackOnHosts: 'Install XOSAN pack on these hosts:',
|
||||
xosanInstallPack: 'Install {pack} v{version}?',
|
||||
xosanNoPackFound: 'No compatible XOSAN pack found for your XenServer versions.',
|
||||
xosanPackRequirements: 'At least one of these version requirements must be satisfied by all the hosts in this pool:'
|
||||
|
||||
xosanPackRequirements: 'At least one of these version requirements must be satisfied by all the hosts in this pool:',
|
||||
// SR tab XOSAN
|
||||
xosanVmsNotRunning: 'Some XOSAN Virtual Machines are not running',
|
||||
xosanVmsNotFound: 'Some XOSAN Virtual Machines could not be found',
|
||||
xosanFilesNeedingHealing: 'Files needing healing',
|
||||
xosanFilesNeedHealing: 'Some XOSAN Virtual Machines have files needing healing',
|
||||
xosanHostNotInNetwork: 'Host {hostName} is not in XOSAN network',
|
||||
xosanVm: 'VM controller',
|
||||
xosanUnderlyingStorage: 'SR',
|
||||
xosanReplace: 'Replace…',
|
||||
xosanOnSameVm: 'On same VM',
|
||||
xosanBrickName: 'Brick name',
|
||||
xosanBrickUuid: 'Brick UUID',
|
||||
xosanBrickSize: 'Brick size',
|
||||
xosanMemorySize: 'Memory size',
|
||||
xosanStatus: 'Status',
|
||||
xosanArbiter: 'Arbiter',
|
||||
xosanUsedInodes: 'Used Inodes',
|
||||
xosanBlockSize: 'Block size',
|
||||
xosanDevice: 'Device',
|
||||
xosanFsName: 'FS name',
|
||||
xosanMountOptions: 'Mount options',
|
||||
xosanPath: 'Path',
|
||||
xosanJob: 'Job',
|
||||
xosanPid: 'PID',
|
||||
xosanPort: 'Port',
|
||||
xosanReplaceBrickErrorTitle: 'Missing values',
|
||||
xosanReplaceBrickErrorMessage: 'You need to select a SR and a size',
|
||||
xosanAddSubvolumeErrorTitle: 'Bad values',
|
||||
xosanAddSubvolumeErrorMessage: 'You need to select {nSrs, number} and a size',
|
||||
xosanSelectNSrs: 'Select {nSrs, number} SRs',
|
||||
xosanRun: 'Run',
|
||||
xosanRemove: 'Remove',
|
||||
xosanVolume: 'Volume',
|
||||
xosanVolumeOptions: 'Volume options',
|
||||
xosanCouldNotFindVM: 'Could not find VM',
|
||||
xosanUnderlyingStorageUsage: 'Using {usage}',
|
||||
xosanCustomIpNetwork: 'Custom IP network (/24)',
|
||||
xosanIssueHostNotInNetwork: 'Will configure the host xosan network device with a static IP address and plug it in.'
|
||||
}
|
||||
forEach(messages, function (message, id) {
|
||||
if (isString(message)) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
|
||||
import Combobox from '../combobox'
|
||||
import Component from '../base-component'
|
||||
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}
|
||||
|
||||
@@ -19,6 +19,7 @@ const _IGNORED_TAGNAMES = {
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
className: propTypes.string,
|
||||
tagName: propTypes.string
|
||||
})
|
||||
export class BlockLink extends Component {
|
||||
@@ -44,11 +45,22 @@ export class BlockLink extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
_addAuxClickListener = ref => {
|
||||
// FIXME: when https://github.com/facebook/react/issues/8529 is fixed,
|
||||
// remove and use onAuxClickCapture.
|
||||
// In Chrome ^55, middle-clicking triggers auxclick event instead of click
|
||||
if (ref !== null) {
|
||||
ref.addEventListener('auxclick', this._onClickCapture)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, tagName = 'div' } = this.props
|
||||
const { children, tagName = 'div', className } = this.props
|
||||
const Component = tagName
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
ref={this._addAuxClickListener}
|
||||
style={this._style}
|
||||
onClickCapture={this._onClickCapture}
|
||||
>
|
||||
|
||||
@@ -29,7 +29,7 @@ const modal = (content, onClose) => {
|
||||
buttons: propTypes.arrayOf(propTypes.shape({
|
||||
btnStyle: propTypes.string,
|
||||
icon: propTypes.string,
|
||||
label: propTypes.string.isRequired,
|
||||
label: propTypes.node.isRequired,
|
||||
tooltip: propTypes.node,
|
||||
value: propTypes.any
|
||||
})).isRequired,
|
||||
@@ -58,8 +58,6 @@ class GenericModal extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { Body, Footer, Header, Title } = ReactModal
|
||||
|
||||
const {
|
||||
buttons,
|
||||
icon,
|
||||
@@ -69,34 +67,33 @@ class GenericModal extends Component {
|
||||
const body = _addRef(this.props.children, 'body')
|
||||
|
||||
return <div>
|
||||
<Header closeButton>
|
||||
<Title>
|
||||
<ReactModal.Header closeButton>
|
||||
<ReactModal.Title>
|
||||
{icon
|
||||
? <span><Icon icon={icon} /> {title}</span>
|
||||
: title
|
||||
}
|
||||
</Title>
|
||||
</Header>
|
||||
<Body>
|
||||
</ReactModal.Title>
|
||||
</ReactModal.Header>
|
||||
<ReactModal.Body>
|
||||
{body}
|
||||
</Body>
|
||||
<Footer>
|
||||
</ReactModal.Body>
|
||||
<ReactModal.Footer>
|
||||
{map(buttons, ({
|
||||
label,
|
||||
tooltip,
|
||||
value,
|
||||
icon,
|
||||
...props
|
||||
}) => {
|
||||
}, key) => {
|
||||
const button = <Button
|
||||
onClick={() => this._resolve(value)}
|
||||
key={value}
|
||||
{...props}
|
||||
>
|
||||
{icon !== undefined && <Icon icon={icon} fixedWidth />}
|
||||
{label}
|
||||
</Button>
|
||||
return <span>
|
||||
return <span key={key}>
|
||||
{tooltip !== undefined
|
||||
? <Tooltip content={tooltip}>{button}</Tooltip>
|
||||
: button
|
||||
@@ -109,7 +106,7 @@ class GenericModal extends Component {
|
||||
{_('genericCancel')}
|
||||
</Button>
|
||||
}
|
||||
</Footer>
|
||||
</ReactModal.Footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -125,7 +122,8 @@ export const alert = (title, body) => (
|
||||
title={title}
|
||||
>
|
||||
{body}
|
||||
</GenericModal>
|
||||
</GenericModal>,
|
||||
resolve
|
||||
)
|
||||
})
|
||||
)
|
||||
@@ -208,14 +206,8 @@ export default class Modal extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { showModal } = this.state
|
||||
/* TODO: remove this work-around and use
|
||||
* ReactModal.Body, ReactModal.Header, ...
|
||||
* after this issue has been fixed:
|
||||
* https://phabricator.babeljs.io/T6976
|
||||
*/
|
||||
return (
|
||||
<ReactModal show={showModal} onHide={this._onHide}>
|
||||
<ReactModal show={this.state.showModal} onHide={this._onHide}>
|
||||
{this.state.content}
|
||||
</ReactModal>
|
||||
)
|
||||
|
||||
30
src/common/no-objects.js
Normal file
30
src/common/no-objects.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
// This component returns :
|
||||
// - A loading icon when the objects are not fetched
|
||||
// - A default message if the objects are fetched and the collection is empty
|
||||
// - The children if the objects are fetched and the collection is not empty
|
||||
//
|
||||
// ```js
|
||||
// <NoObjects collection={collection} emptyMessage={message}>
|
||||
// {children}
|
||||
// </NoObjects>
|
||||
// ````
|
||||
const NoObjects = ({ children, collection, emptyMessage }) => collection == null
|
||||
? <img src='assets/loading.svg' alt='loading' />
|
||||
: isEmpty(collection)
|
||||
? <p>{emptyMessage}</p>
|
||||
: <div>{children}</div>
|
||||
|
||||
propTypes(NoObjects)({
|
||||
children: propTypes.node.isRequired,
|
||||
collection: propTypes.oneOfType([
|
||||
propTypes.array,
|
||||
propTypes.object
|
||||
]).isRequired,
|
||||
emptyMessage: propTypes.node.isRequired
|
||||
})
|
||||
export default NoObjects
|
||||
18
src/common/react-novnc.js
vendored
18
src/common/react-novnc.js
vendored
@@ -1,8 +1,7 @@
|
||||
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'
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
@@ -532,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
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -10,3 +10,8 @@
|
||||
.clickableRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
outline: 2px solid #366e98;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
import _ from 'intl'
|
||||
import ceil from 'lodash/ceil'
|
||||
import classNames from 'classnames'
|
||||
import debounce from 'lodash/debounce'
|
||||
import findIndex from 'lodash/findIndex'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import { Dropdown, MenuItem, Pagination } from 'react-bootstrap-4/lib'
|
||||
import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
|
||||
import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
|
||||
import React from 'react'
|
||||
import Shortcuts from 'shortcuts'
|
||||
import { Portal } from 'react-overlays'
|
||||
import { routerShape } from 'react-router/lib/PropTypes'
|
||||
import { Set } from 'immutable'
|
||||
import {
|
||||
Dropdown,
|
||||
MenuItem,
|
||||
Pagination
|
||||
} from 'react-bootstrap-4/lib'
|
||||
import {
|
||||
ceil,
|
||||
debounce,
|
||||
findIndex,
|
||||
forEach,
|
||||
isEmpty,
|
||||
isFunction,
|
||||
map
|
||||
} from 'lodash'
|
||||
|
||||
import ActionRowButton from '../action-row-button'
|
||||
import Button from '../button'
|
||||
import ButtonGroup from '../button-group'
|
||||
import Component from '../base-component'
|
||||
import defined from '../xo-defined'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import SingleLineRow from '../single-line-row'
|
||||
import Tooltip from '../tooltip'
|
||||
import { BlockLink } from '../link'
|
||||
import { Container, Col } from '../grid'
|
||||
import { create as createMatcher } from '../complex-matcher'
|
||||
@@ -33,9 +47,8 @@ import styles from './index.css'
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
defaultFilter: propTypes.string,
|
||||
filters: propTypes.object,
|
||||
nFilteredItems: propTypes.number.isRequired,
|
||||
nItems: propTypes.number.isRequired,
|
||||
onChange: propTypes.func.isRequired
|
||||
})
|
||||
class TableFilter extends Component {
|
||||
@@ -57,10 +70,9 @@ class TableFilter extends Component {
|
||||
|
||||
return (
|
||||
<div className='input-group'>
|
||||
<span className='input-group-addon'>{props.nFilteredItems} / {props.nItems}</span>
|
||||
{isEmpty(props.filters)
|
||||
? <span className='input-group-addon'><Icon icon='search' /></span>
|
||||
: <div className='input-group-btn'>
|
||||
: <span className='input-group-btn'>
|
||||
<Dropdown id='filter'>
|
||||
<DropdownToggle bsStyle='info'>
|
||||
<Icon icon='search' />
|
||||
@@ -73,18 +85,27 @@ class TableFilter extends Component {
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>}
|
||||
</span>}
|
||||
<input
|
||||
type='text'
|
||||
ref='filter'
|
||||
onChange={this._onChange}
|
||||
className='form-control'
|
||||
defaultValue={props.defaultFilter}
|
||||
onChange={this._onChange}
|
||||
ref='filter'
|
||||
/>
|
||||
<div className='input-group-btn'>
|
||||
<Tooltip content={_('filterSyntaxLinkTooltip')}>
|
||||
<a
|
||||
className='input-group-addon'
|
||||
href='https://xen-orchestra.com/docs/search.html#filter-syntax'
|
||||
target='_blank'
|
||||
>
|
||||
<Icon icon='info' />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<span className='input-group-btn'>
|
||||
<Button onClick={this._cleanFilter}>
|
||||
<Icon icon='clear-search' />
|
||||
</Button>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -133,18 +154,53 @@ class ColumnHead extends Component {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const DEFAULT_ITEMS_PER_PAGE = 10
|
||||
@propTypes({
|
||||
indeterminate: propTypes.bool.isRequired
|
||||
})
|
||||
class Checkbox extends Component {
|
||||
componentDidUpdate () {
|
||||
const { props: { indeterminate }, ref } = this
|
||||
if (ref !== null) {
|
||||
ref.indeterminate = indeterminate
|
||||
}
|
||||
}
|
||||
|
||||
_ref = ref => {
|
||||
this.ref = ref
|
||||
this.componentDidUpdate()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { indeterminate, ...props } = this.props
|
||||
props.ref = this._ref
|
||||
props.type = 'checkbox'
|
||||
return <input {...props} />
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const actionsShape = propTypes.arrayOf(propTypes.shape({
|
||||
// groupedActions: the function will be called with an array of the selected items` ids in parameters
|
||||
// individualActions: the function will be called with the related item's id in parameters
|
||||
handler: propTypes.func.isRequired,
|
||||
icon: propTypes.string.isRequired,
|
||||
label: propTypes.node.isRequired,
|
||||
level: propTypes.oneOf([ 'warning', 'danger' ])
|
||||
}))
|
||||
|
||||
@propTypes({
|
||||
defaultColumn: propTypes.number,
|
||||
defaultFilter: propTypes.string,
|
||||
collection: propTypes.oneOfType([
|
||||
propTypes.array,
|
||||
propTypes.object
|
||||
]).isRequired,
|
||||
columns: propTypes.arrayOf(propTypes.shape({
|
||||
component: propTypes.func,
|
||||
default: propTypes.bool,
|
||||
name: propTypes.node,
|
||||
itemRenderer: propTypes.func.isRequired,
|
||||
itemRenderer: propTypes.func,
|
||||
sortCriteria: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
@@ -153,7 +209,10 @@ const DEFAULT_ITEMS_PER_PAGE = 10
|
||||
textAlign: propTypes.string
|
||||
})).isRequired,
|
||||
filterContainer: propTypes.func,
|
||||
filterUrlParam: propTypes.string,
|
||||
filters: propTypes.object,
|
||||
groupedActions: actionsShape,
|
||||
individualActions: actionsShape,
|
||||
itemsPerPage: propTypes.number,
|
||||
paginationContainer: propTypes.func,
|
||||
rowAction: propTypes.func,
|
||||
@@ -161,11 +220,20 @@ const DEFAULT_ITEMS_PER_PAGE = 10
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
]),
|
||||
// DOM node selector like body or .my-class
|
||||
// The shortcuts will be enabled when the node is focused
|
||||
shortcutsTarget: propTypes.string,
|
||||
userData: propTypes.any
|
||||
}, {
|
||||
router: routerShape
|
||||
})
|
||||
export default class SortedTable extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
static defaultProps = {
|
||||
itemsPerPage: 10
|
||||
}
|
||||
|
||||
constructor (props, context) {
|
||||
super(props, context)
|
||||
|
||||
let selectedColumn = props.defaultColumn
|
||||
if (selectedColumn == null) {
|
||||
@@ -177,8 +245,15 @@ export default class SortedTable extends Component {
|
||||
}
|
||||
|
||||
this.state = {
|
||||
all: false, // whether all items are selected (accross pages)
|
||||
filter: defined(
|
||||
() => context.router.location.query[props.filterUrlParam],
|
||||
() => props.filters[props.defaultFilter]
|
||||
),
|
||||
selectedColumn,
|
||||
itemsPerPage: props.itemsPerPage || DEFAULT_ITEMS_PER_PAGE
|
||||
sortOrder: props.columns[selectedColumn].sortOrder === 'desc'
|
||||
? 'desc'
|
||||
: 'asc'
|
||||
}
|
||||
|
||||
this._getSelectedColumn = () =>
|
||||
@@ -188,11 +263,11 @@ export default class SortedTable extends Component {
|
||||
() => this.props.collection
|
||||
)
|
||||
|
||||
this._getAllItems = createSort(
|
||||
this._getItems = createSort(
|
||||
createFilter(
|
||||
() => this.props.collection,
|
||||
createSelector(
|
||||
() => this.state.filter || '',
|
||||
() => this.state.filter,
|
||||
createMatcher
|
||||
)
|
||||
),
|
||||
@@ -210,16 +285,67 @@ export default class SortedTable extends Component {
|
||||
this.state.activePage = 1
|
||||
|
||||
this._getVisibleItems = createPager(
|
||||
this._getAllItems,
|
||||
this._getItems,
|
||||
() => this.state.activePage,
|
||||
this.state.itemsPerPage
|
||||
this.props.itemsPerPage
|
||||
)
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.setState({
|
||||
sortOrder: this.props.columns[this.state.selectedColumn].sortOrder === 'desc' ? 'desc' : 'asc'
|
||||
})
|
||||
this.state.selectedItemsIds = new Set()
|
||||
|
||||
this._hasGroupedActions = createSelector(
|
||||
() => this.props.groupedActions,
|
||||
actions => !isEmpty(actions)
|
||||
)
|
||||
|
||||
this._getShortcutsHandler = createSelector(
|
||||
this._getVisibleItems,
|
||||
this._hasGroupedActions,
|
||||
() => this.state.highlighted,
|
||||
() => this.props.rowLink,
|
||||
() => this.props.rowAction,
|
||||
() => this.props.userData,
|
||||
(visibleItems, hasGroupedActions, itemIndex, rowLink, rowAction, userData) => (command, event) => {
|
||||
event.preventDefault()
|
||||
const item = itemIndex !== undefined ? visibleItems[itemIndex] : undefined
|
||||
|
||||
switch (command) {
|
||||
case 'SEARCH':
|
||||
this.refs.filterInput.refs.filter.focus()
|
||||
break
|
||||
case 'NAV_DOWN':
|
||||
if (hasGroupedActions || rowAction !== undefined || rowLink !== undefined) {
|
||||
this.setState({
|
||||
highlighted: (itemIndex + visibleItems.length + 1) % visibleItems.length || 0
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'NAV_UP':
|
||||
if (hasGroupedActions || rowAction !== undefined || rowLink !== undefined) {
|
||||
this.setState({
|
||||
highlighted: (itemIndex + visibleItems.length - 1) % visibleItems.length || 0
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'SELECT':
|
||||
if (itemIndex !== undefined && hasGroupedActions) {
|
||||
this._selectItem(itemIndex)
|
||||
}
|
||||
break
|
||||
case 'ROW_ACTION':
|
||||
if (item !== undefined) {
|
||||
if (rowLink !== undefined) {
|
||||
this.context.router.push(isFunction(rowLink)
|
||||
? rowLink(item, userData)
|
||||
: rowLink
|
||||
)
|
||||
} else if (rowAction !== undefined) {
|
||||
rowAction(item, userData)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@@ -250,31 +376,238 @@ export default class SortedTable extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const { selectedItemsIds } = this.state
|
||||
|
||||
// Unselect items that are no longer visible
|
||||
if ((this._visibleItemsRecomputations || 0) < (this._visibleItemsRecomputations = this._getVisibleItems.recomputations())) {
|
||||
const newSelectedItems = selectedItemsIds.intersect(map(this._getVisibleItems(), 'id'))
|
||||
if (newSelectedItems.size < selectedItemsIds.size) {
|
||||
this.setState({ selectedItemsIds: newSelectedItems })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onPageSelection = (_, event) => this.setState({
|
||||
activePage: event.eventKey
|
||||
activePage: event.eventKey,
|
||||
highlighted: undefined
|
||||
})
|
||||
|
||||
_onFilterChange = debounce(filter => {
|
||||
_selectAllVisibleItems = event => {
|
||||
this.setState({
|
||||
all: false,
|
||||
selectedItemsIds: event.target.checked
|
||||
? this.state.selectedItemsIds.union(map(this._getVisibleItems(), 'id'))
|
||||
: this.state.selectedItemsIds.clear()
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: figure out why it's necessary
|
||||
_toggleNestedCheckboxGuard = false
|
||||
|
||||
_toggleNestedCheckbox = event => {
|
||||
const child = event.target.firstElementChild
|
||||
if (child != null && child.tagName === 'INPUT') {
|
||||
if (this._toggleNestedCheckboxGuard) {
|
||||
return
|
||||
}
|
||||
this._toggleNestedCheckboxGuard = true
|
||||
child.dispatchEvent(
|
||||
new window.MouseEvent('click', event.nativeEvent)
|
||||
)
|
||||
this._toggleNestedCheckboxGuard = false
|
||||
}
|
||||
}
|
||||
|
||||
_selectAll = () => this.setState({ all: true })
|
||||
|
||||
_selectItem (current, selected, range = false) {
|
||||
const { all, selectedItemsIds } = this.state
|
||||
const visibleItems = this._getVisibleItems()
|
||||
const item = visibleItems[current]
|
||||
|
||||
if (all) {
|
||||
return this.setState({
|
||||
all: false,
|
||||
selectedItemsIds: new Set().withMutations(selectedItemsIds => {
|
||||
forEach(visibleItems, item => {
|
||||
selectedItemsIds.add(item.id)
|
||||
})
|
||||
selectedItemsIds.delete(item.id)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
let method = (
|
||||
selected === undefined ? !selectedItemsIds.has(item.id) : selected
|
||||
) ? 'add' : 'delete'
|
||||
|
||||
let previous
|
||||
this.setState({ selectedItemsIds:
|
||||
(
|
||||
range &&
|
||||
(previous = this._previous) !== undefined
|
||||
) ? selectedItemsIds.withMutations(selectedItemsIds => {
|
||||
let i = previous
|
||||
let end = current
|
||||
if (previous > current) {
|
||||
i = current
|
||||
end = previous
|
||||
}
|
||||
for (; i <= end; ++i) {
|
||||
selectedItemsIds[method](visibleItems[i].id)
|
||||
}
|
||||
})
|
||||
: selectedItemsIds[method](item.id)
|
||||
})
|
||||
|
||||
this._previous = current
|
||||
}
|
||||
|
||||
_onSelectItemCheckbox = event => {
|
||||
const { target } = event
|
||||
this._selectItem(+target.name, target.checked, event.nativeEvent.shiftKey)
|
||||
}
|
||||
|
||||
_onFilterChange = debounce(filter => {
|
||||
const { filterUrlParam } = this.props
|
||||
if (filterUrlParam !== undefined) {
|
||||
const { router } = this.context
|
||||
const { location } = router
|
||||
router.replace({
|
||||
...location,
|
||||
query: {
|
||||
...location.query,
|
||||
[filterUrlParam]: filter
|
||||
}
|
||||
})
|
||||
}
|
||||
this.setState({
|
||||
activePage: 1,
|
||||
filter,
|
||||
activePage: 1
|
||||
highlighted: undefined
|
||||
})
|
||||
}, 500)
|
||||
|
||||
_executeGroupedAction = handler => {
|
||||
const { state } = this
|
||||
return handler(
|
||||
state.all
|
||||
? map(this._getItems(), 'id')
|
||||
: state.selectedItemsIds.toArray()
|
||||
)
|
||||
}
|
||||
|
||||
_executeRowAction = event => {
|
||||
const { props } = this
|
||||
const item = this._getVisibleItems()[event.currentTarget.dataset.index]
|
||||
props.rowAction(item, props.userData)
|
||||
}
|
||||
|
||||
_renderItem = (item, i) => {
|
||||
const { props, state } = this
|
||||
|
||||
const { individualActions, rowAction, rowLink, userData } = props
|
||||
|
||||
const hasGroupedActions = this._hasGroupedActions()
|
||||
const hasIndividualActions = !isEmpty(individualActions)
|
||||
|
||||
const columns = map(props.columns, ({
|
||||
component: Component,
|
||||
itemRenderer,
|
||||
textAlign
|
||||
}, key) =>
|
||||
<td
|
||||
className={textAlign && `text-xs-${textAlign}`}
|
||||
key={key}
|
||||
>
|
||||
{Component !== undefined
|
||||
? <Component item={item} userData={userData} />
|
||||
: itemRenderer(item, userData)
|
||||
}
|
||||
</td>
|
||||
)
|
||||
|
||||
const { id = i } = item
|
||||
|
||||
const selectionColumn = hasGroupedActions && <td
|
||||
className='text-xs-center'
|
||||
onClick={this._toggleNestedCheckbox}
|
||||
>
|
||||
<input
|
||||
checked={state.all || state.selectedItemsIds.has(id)}
|
||||
name={i} // position in visible items
|
||||
onChange={this._selectItem}
|
||||
type='checkbox'
|
||||
/>
|
||||
</td>
|
||||
const actionsColumn = hasIndividualActions && <td><div className='pull-right'>
|
||||
<ButtonGroup>
|
||||
{map(individualActions, ({ icon, label, level, handler }, key) => <ActionRowButton
|
||||
btnStyle={level}
|
||||
handler={handler}
|
||||
handlerParam={id}
|
||||
icon={icon}
|
||||
key={key}
|
||||
tooltip={label}
|
||||
/>)}
|
||||
</ButtonGroup>
|
||||
</div></td>
|
||||
|
||||
return rowLink != null
|
||||
? <BlockLink
|
||||
className={state.highlighted === i ? styles.highlight : undefined}
|
||||
key={id}
|
||||
tagName='tr'
|
||||
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
|
||||
>
|
||||
{selectionColumn}
|
||||
{columns}
|
||||
{actionsColumn}
|
||||
</BlockLink>
|
||||
: <tr
|
||||
className={classNames(
|
||||
rowAction && styles.clickableRow,
|
||||
state.highlighted === i && styles.highlight
|
||||
)}
|
||||
key={id}
|
||||
onClick={rowAction && (() => rowAction(item, userData))}
|
||||
>
|
||||
{selectionColumn}
|
||||
{columns}
|
||||
{actionsColumn}
|
||||
</tr>
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const {
|
||||
paginationContainer,
|
||||
filterContainer,
|
||||
filters,
|
||||
rowAction,
|
||||
rowLink,
|
||||
userData
|
||||
groupedActions,
|
||||
itemsPerPage,
|
||||
paginationContainer,
|
||||
shortcutsTarget
|
||||
} = props
|
||||
const { all } = state
|
||||
|
||||
const nFilteredItems = this._getAllItems().length
|
||||
const nAllItems = this._getTotalNumberOfItems()
|
||||
const nItems = this._getItems().length
|
||||
const nSelectedItems = state.selectedItemsIds.size
|
||||
const nVisibleItems = this._getVisibleItems().length
|
||||
|
||||
const paginationInstance = (
|
||||
const hasGroupedActions = this._hasGroupedActions()
|
||||
const hasIndividualActions = !isEmpty(props.individualActions)
|
||||
|
||||
const nColumns = props.columns.length + (hasIndividualActions ? 2 : 1)
|
||||
|
||||
const displayPagination =
|
||||
paginationContainer === undefined &&
|
||||
itemsPerPage < nAllItems
|
||||
const displayFilter =
|
||||
filterContainer === undefined &&
|
||||
nAllItems !== 0
|
||||
|
||||
const paginationInstance = displayPagination && (
|
||||
<Pagination
|
||||
first
|
||||
last
|
||||
@@ -283,26 +616,89 @@ export default class SortedTable extends Component {
|
||||
ellipsis
|
||||
boundaryLinks
|
||||
maxButtons={10}
|
||||
items={ceil(nFilteredItems / state.itemsPerPage)}
|
||||
activePage={this.state.activePage}
|
||||
items={ceil(nItems / itemsPerPage)}
|
||||
activePage={state.activePage}
|
||||
onSelect={this._onPageSelection}
|
||||
/>
|
||||
)
|
||||
|
||||
const filterInstance = (
|
||||
const filterInstance = displayFilter && (
|
||||
<TableFilter
|
||||
filters={filters}
|
||||
nFilteredItems={nFilteredItems}
|
||||
nItems={this._getTotalNumberOfItems()}
|
||||
defaultFilter={state.filter}
|
||||
filters={props.filters}
|
||||
onChange={this._onFilterChange}
|
||||
ref='filterInput'
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{shortcutsTarget !== undefined && <Shortcuts
|
||||
handler={this._getShortcutsHandler()}
|
||||
name='SortedTable'
|
||||
stopPropagation
|
||||
targetNodeSelector={shortcutsTarget}
|
||||
/>}
|
||||
<table className='table'>
|
||||
<thead className='thead-default'>
|
||||
<tr>
|
||||
<th colSpan={nColumns}>
|
||||
{nItems === nAllItems
|
||||
? _('sortedTableNumberOfItems', { nTotal: nItems })
|
||||
: _('sortedTableNumberOfFilteredItems', {
|
||||
nFiltered: nItems,
|
||||
nTotal: nAllItems
|
||||
})
|
||||
}
|
||||
{all
|
||||
? <span>
|
||||
{' '}-{' '}
|
||||
<span className='text-danger'>
|
||||
{_('sortedTableAllItemsSelected')}
|
||||
</span>
|
||||
</span>
|
||||
: nSelectedItems !== 0 && <span>
|
||||
{' '}-{' '}
|
||||
{_('sortedTableNumberOfSelectedItems', {
|
||||
nSelected: nSelectedItems
|
||||
})}
|
||||
{nSelectedItems === nVisibleItems && nSelectedItems < nItems &&
|
||||
<Button
|
||||
btnStyle='info'
|
||||
className='ml-1'
|
||||
onClick={this._selectAll}
|
||||
size='small'
|
||||
>
|
||||
{_('sortedTableSelectAllItems')}
|
||||
</Button>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
{nSelectedItems !== 0 && <div className='pull-right'>
|
||||
<ButtonGroup>
|
||||
{map(groupedActions, ({ icon, label, level, handler }, key) => <ActionRowButton
|
||||
btnStyle={level}
|
||||
handler={this._executeGroupedAction}
|
||||
handlerParam={handler}
|
||||
icon={icon}
|
||||
key={key}
|
||||
tooltip={label}
|
||||
/>)}
|
||||
</ButtonGroup>
|
||||
</div>}
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{hasGroupedActions && <th
|
||||
className='text-xs-center'
|
||||
onClick={this._toggleNestedCheckbox}
|
||||
>
|
||||
<Checkbox
|
||||
onChange={this._selectAllVisibleItems}
|
||||
checked={all || nSelectedItems !== 0}
|
||||
indeterminate={!all && nSelectedItems !== 0 && nSelectedItems !== nVisibleItems}
|
||||
/>
|
||||
</th>}
|
||||
{map(props.columns, (column, key) => (
|
||||
<ColumnHead
|
||||
textAlign={column.textAlign}
|
||||
@@ -314,55 +710,39 @@ export default class SortedTable extends Component {
|
||||
sortIcon={state.selectedColumn === key ? state.sortOrder : 'sort'}
|
||||
/>
|
||||
))}
|
||||
{hasIndividualActions && <th />}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(this._getVisibleItems(), (item, i) => {
|
||||
const columns = map(props.columns, (column, key) => (
|
||||
<td key={key} className={column.textAlign && `text-xs-${column.textAlign}`}>
|
||||
{column.itemRenderer(item, userData)}
|
||||
</td>
|
||||
))
|
||||
|
||||
const { id = i } = item
|
||||
|
||||
return rowLink
|
||||
? <BlockLink
|
||||
key={id}
|
||||
tagName='tr'
|
||||
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
|
||||
>{columns}</BlockLink>
|
||||
: <tr
|
||||
className={rowAction && styles.clickableRow}
|
||||
key={id}
|
||||
onClick={rowAction && (() => rowAction(item, userData))}
|
||||
>
|
||||
{columns}
|
||||
</tr>
|
||||
})}
|
||||
{nVisibleItems !== 0
|
||||
? map(this._getVisibleItems(), this._renderItem)
|
||||
: <tr><td className='text-info text-xs-center' colSpan={nColumns}>
|
||||
{_('sortedTableNoItems')}
|
||||
</td></tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
{(!paginationContainer || !filterContainer) && (
|
||||
{(displayFilter || displayPagination) && (
|
||||
<Container>
|
||||
<SingleLineRow>
|
||||
<Col mediumSize={8}>
|
||||
{paginationContainer
|
||||
? (
|
||||
{displayPagination && (
|
||||
paginationContainer !== undefined
|
||||
// Rebuild container function to refresh Portal component.
|
||||
<Portal container={() => paginationContainer()}>
|
||||
? <Portal container={() => paginationContainer()}>
|
||||
{paginationInstance}
|
||||
</Portal>
|
||||
) : paginationInstance
|
||||
}
|
||||
: paginationInstance
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
{filterContainer
|
||||
? (
|
||||
<Portal container={() => filterContainer()}>
|
||||
{displayFilter && (
|
||||
filterContainer
|
||||
? <Portal container={() => filterContainer()}>
|
||||
{filterInstance}
|
||||
</Portal>
|
||||
) : filterInstance
|
||||
}
|
||||
: filterInstance
|
||||
)}
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</Container>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
omit
|
||||
} from 'lodash'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const Button = styled(ActionButton)`
|
||||
background-color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateBg`]}
|
||||
border: 2px solid ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]}
|
||||
color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]}
|
||||
// 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 = ({
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
import every from 'lodash/every'
|
||||
import forEach from 'lodash/forEach'
|
||||
import getStream from 'get-stream'
|
||||
import humanFormat from 'human-format'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import isPlainObject from 'lodash/isPlainObject'
|
||||
import isString from 'lodash/isString'
|
||||
import join from 'lodash/join'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import React from 'react'
|
||||
import ReadableStream from 'readable-stream'
|
||||
import replace from 'lodash/replace'
|
||||
import startsWith from 'lodash/startsWith'
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
clone,
|
||||
escapeRegExp,
|
||||
every,
|
||||
forEach,
|
||||
isArray,
|
||||
isEmpty,
|
||||
isFunction,
|
||||
isPlainObject,
|
||||
isString,
|
||||
join,
|
||||
keys,
|
||||
map,
|
||||
mapValues,
|
||||
replace,
|
||||
sample,
|
||||
startsWith
|
||||
} from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
import * as actions from './store/actions'
|
||||
@@ -63,8 +67,12 @@ 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 }))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -190,19 +198,6 @@ export { default as Debug } from './debug'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Returns the first defined (non-undefined) value.
|
||||
export const firstDefined = function () {
|
||||
const n = arguments.length
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const arg = arguments[i]
|
||||
if (arg !== undefined) {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Returns the current XOA Plan or the Plan name if number given
|
||||
export const getXoaPlan = plan => {
|
||||
switch (plan || +process.env.XOA_PLAN) {
|
||||
@@ -268,6 +263,11 @@ export const formatSize = bytes => humanFormat(bytes, { scale: 'binary', unit: '
|
||||
|
||||
export const formatSizeRaw = bytes => humanFormat.raw(bytes, { scale: 'binary', unit: 'B' })
|
||||
|
||||
export const formatSpeed = (bytes, milliseconds) => humanFormat(
|
||||
bytes * 1e3 / milliseconds,
|
||||
{ scale: 'binary', unit: 'B/s' }
|
||||
)
|
||||
|
||||
export const parseSize = size => {
|
||||
let bytes = humanFormat.parse.raw(size, { scale: 'binary' })
|
||||
if (bytes.unit && bytes.unit !== 'B') {
|
||||
@@ -359,20 +359,6 @@ export const throwFn = error => () => {
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export function tap (cb) {
|
||||
return this.then(value =>
|
||||
Promise.resolve(cb(value)).then(() => value)
|
||||
)
|
||||
}
|
||||
|
||||
export function rethrow (cb) {
|
||||
return this.catch(error =>
|
||||
Promise.resolve(cb(error)).then(() => { throw error })
|
||||
)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const resolveResourceSet = resourceSet => {
|
||||
@@ -555,3 +541,43 @@ export const getCoresPerSocketPossibilities = (maxCoresPerSocket, vCPUs) => {
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// Generates a random human-readable string of length `length`
|
||||
// Useful to generate random default names intended for the UI user
|
||||
export const generateReadableRandomString = (() => {
|
||||
const CONSONANTS = 'bdfgklmnprtvz'.split('')
|
||||
const VOWELS = 'aeiou'.split('')
|
||||
return (length = 8) => {
|
||||
const result = new Array(length)
|
||||
for (let i = 0; i < length; ++i) {
|
||||
result[i] = sample((i & 1) === 0 ? VOWELS : CONSONANTS)
|
||||
}
|
||||
return result.join('')
|
||||
}
|
||||
})()
|
||||
|
||||
export const cowSet = (object, path, value, depth = 0) => {
|
||||
if (depth >= path.length) {
|
||||
return value
|
||||
}
|
||||
|
||||
object = object != null ? clone(object) : {}
|
||||
const prop = path[depth]
|
||||
object[prop] = cowSet(object[prop], path, value, depth + 1)
|
||||
return object
|
||||
}
|
||||
|
||||
// Generates a function that returns a value between 0 and 1
|
||||
// This function returns an estimated progress value between 0 and 1
|
||||
// based on the elapsed time since the createFakeProgress call and
|
||||
// the given estimated duration d
|
||||
export const createFakeProgress = (() => {
|
||||
const S = 0.95 // Progress value after d seconds
|
||||
return d => {
|
||||
const startTime = Date.now() / 1e3
|
||||
return () => {
|
||||
const x = Date.now() / 1e3 - startTime
|
||||
return -Math.exp((x * Math.log(1 - S)) / d) + 1
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
63
src/common/xo-defined.js
Normal file
63
src/common/xo-defined.js
Normal file
@@ -0,0 +1,63 @@
|
||||
// Usage:
|
||||
//
|
||||
// ```js
|
||||
// const httpProxy = defined(
|
||||
// process.env.HTTP_PROXY,
|
||||
// process.env.http_proxy
|
||||
// )
|
||||
//
|
||||
// const httpProxy = defined([
|
||||
// process.env.HTTP_PROXY,
|
||||
// process.env.http_proxy
|
||||
// ])
|
||||
// ```
|
||||
export default function defined () {
|
||||
let args = arguments
|
||||
let n = args.length
|
||||
if (n === 1) {
|
||||
args = arguments[0]
|
||||
n = args.length
|
||||
}
|
||||
|
||||
for (let i = 0; i < n; ++i) {
|
||||
let arg = arguments[i]
|
||||
if (typeof arg === 'function') {
|
||||
arg = get(arg)
|
||||
}
|
||||
if (arg !== undefined) {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
//
|
||||
// ```js
|
||||
// const friendName = get(() => props.user.friends[0].name)
|
||||
//
|
||||
// // this form can be used to avoid recreating functions:
|
||||
// const getFriendName = _ => _.friends[0].name
|
||||
// const friendName = get(getFriendName, props.user)
|
||||
// ```
|
||||
export const get = (accessor, arg) => {
|
||||
try {
|
||||
return accessor(arg)
|
||||
} catch (error) {
|
||||
if (!(error instanceof TypeError)) { // avoid hiding other errors
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
//
|
||||
// ```js
|
||||
// const httpAgent = ifDef(
|
||||
// process.env.HTTP_PROXY,
|
||||
// _ => new ProxyAgent(_)
|
||||
// )
|
||||
// ```
|
||||
export const ifDef = (value, thenFn) =>
|
||||
value !== undefined
|
||||
? thenFn(value)
|
||||
: value
|
||||
139
src/common/xo/choose-sr-for-each-vdis-modal/index.js
Normal file
139
src/common/xo/choose-sr-for-each-vdis-modal/index.js
Normal 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>
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import sortBy from 'lodash/sortBy'
|
||||
import throttle from 'lodash/throttle'
|
||||
import Xo from 'xo-lib'
|
||||
import { createBackoff } from 'jsonrpc-websocket-client'
|
||||
import { lastly, reflect } from 'promise-toolbox'
|
||||
import { lastly, reflect, tap } from 'promise-toolbox'
|
||||
import { forbiddenOperation, noHostsAvailable } from 'xo-common/api-errors'
|
||||
import { resolve } from 'url'
|
||||
|
||||
@@ -26,7 +26,7 @@ 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,
|
||||
@@ -48,7 +48,7 @@ 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'
|
||||
|
||||
// ===================================================================
|
||||
@@ -92,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,
|
||||
@@ -163,6 +163,11 @@ const createSubscription = cb => {
|
||||
|
||||
let running = false
|
||||
|
||||
const uninstall = () => {
|
||||
clearTimeout(timeout)
|
||||
cache = undefined
|
||||
}
|
||||
|
||||
const loop = () => {
|
||||
if (running) {
|
||||
return
|
||||
@@ -171,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)) {
|
||||
@@ -187,6 +197,11 @@ const createSubscription = cb => {
|
||||
}
|
||||
}, error => {
|
||||
running = false
|
||||
|
||||
if (n === 0) {
|
||||
return uninstall()
|
||||
}
|
||||
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
@@ -195,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()
|
||||
}
|
||||
@@ -204,9 +221,8 @@ const createSubscription = cb => {
|
||||
return once(() => {
|
||||
delete subscribers[id]
|
||||
|
||||
if (!--n) {
|
||||
clearTimeout(timeout)
|
||||
cache = undefined
|
||||
if (--n === 0) {
|
||||
uninstall()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -275,15 +291,15 @@ export const subscribeIpPools = createSubscription(() => _call('ipPool.getAll'))
|
||||
|
||||
export const subscribeResourceCatalog = createSubscription(() => _call('cloud.getResourceCatalog'))
|
||||
|
||||
const xosanSubscriptions = {}
|
||||
export const subscribeIsInstallingXosan = (pool, cb) => {
|
||||
const checkSrCurrentStateSubscriptions = {}
|
||||
export const subscribeCheckSrCurrentState = (pool, cb) => {
|
||||
const poolId = resolveId(pool)
|
||||
|
||||
if (!xosanSubscriptions[poolId]) {
|
||||
xosanSubscriptions[poolId] = createSubscription(() => _call('xosan.checkSrIsBusy', { poolId }))
|
||||
if (!checkSrCurrentStateSubscriptions[poolId]) {
|
||||
checkSrCurrentStateSubscriptions[poolId] = createSubscription(() => _call('xosan.checkSrCurrentState', { poolId }))
|
||||
}
|
||||
|
||||
return xosanSubscriptions[poolId](cb)
|
||||
return checkSrCurrentStateSubscriptions[poolId](cb)
|
||||
}
|
||||
|
||||
const missingPatchesByHost = {}
|
||||
@@ -308,6 +324,46 @@ subscribeHostMissingPatches.forceRefresh = host => {
|
||||
}
|
||||
}
|
||||
|
||||
const volumeInfoBySr = {}
|
||||
export const subscribeVolumeInfo = ({ sr, infoType }, cb) => {
|
||||
sr = resolveId(sr)
|
||||
|
||||
if (volumeInfoBySr[sr] == null) {
|
||||
volumeInfoBySr[sr] = {}
|
||||
}
|
||||
|
||||
if (volumeInfoBySr[sr][infoType] == null) {
|
||||
volumeInfoBySr[sr][infoType] = createSubscription(() => _call('xosan.getVolumeInfo', { sr, infoType }))
|
||||
}
|
||||
|
||||
return volumeInfoBySr[sr][infoType](cb)
|
||||
}
|
||||
subscribeVolumeInfo.forceRefresh = (() => {
|
||||
const refreshSrVolumeInfo = volumeInfo => {
|
||||
forEach(volumeInfo, subscription => subscription.forceRefresh())
|
||||
}
|
||||
|
||||
return sr => {
|
||||
if (sr === undefined) {
|
||||
forEach(volumeInfoBySr, refreshSrVolumeInfo)
|
||||
} else {
|
||||
refreshSrVolumeInfo(volumeInfoBySr[sr])
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
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')
|
||||
@@ -336,8 +392,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) => (
|
||||
@@ -414,10 +471,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) => (
|
||||
@@ -761,14 +838,13 @@ export const cloneVm = ({ id, name_label: nameLabel }, fullCopy = false) => (
|
||||
)
|
||||
|
||||
import CopyVmModalBody from './copy-vm-modal' // eslint-disable-line import/first
|
||||
export const copyVm = (vm, sr, name, compress) => {
|
||||
if (sr) {
|
||||
return confirm({
|
||||
export const copyVm = (vm, sr, name, compress) =>
|
||||
sr !== undefined
|
||||
? confirm({
|
||||
title: _('copyVm'),
|
||||
body: _('copyVmConfirm', { SR: sr.name_label })
|
||||
}).then(() => _call('vm.copy', { vm: vm.id, sr: sr.id, name: name || vm.name_label + '_COPY', compress }))
|
||||
} else {
|
||||
return confirm({
|
||||
: confirm({
|
||||
title: _('copyVm'),
|
||||
body: <CopyVmModalBody vm={vm} />
|
||||
}).then(
|
||||
@@ -777,12 +853,10 @@ export const copyVm = (vm, sr, name, compress) => {
|
||||
error('copyVmsNoTargetSr', 'copyVmsNoTargetSrMessage')
|
||||
return
|
||||
}
|
||||
_call('vm.copy', { vm: vm.id, ...params })
|
||||
return _call('vm.copy', { vm: vm.id, ...params })
|
||||
},
|
||||
noop
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
import CopyVmsModalBody from './copy-vms-modal' // eslint-disable-line import/first
|
||||
export const copyVms = vms => {
|
||||
@@ -791,19 +865,17 @@ export const copyVms = vms => {
|
||||
title: _('copyVm'),
|
||||
body: <CopyVmsModalBody vms={_vms} />
|
||||
}).then(
|
||||
params => {
|
||||
if (!params.sr) {
|
||||
error(_('copyVmsNoTargetSr'), _('copyVmsNoTargetSrMessage'))
|
||||
return
|
||||
({
|
||||
compress,
|
||||
names,
|
||||
sr
|
||||
}) => {
|
||||
if (sr !== undefined) {
|
||||
return Promise.all(map(_vms, (vm, index) =>
|
||||
_call('vm.copy', { vm, sr, compress, name: names[index] })
|
||||
))
|
||||
}
|
||||
const {
|
||||
compress,
|
||||
names,
|
||||
sr
|
||||
} = params
|
||||
Promise.all(map(_vms, (vm, index) =>
|
||||
_call('vm.copy', { vm, sr, compress, name: names[index] })
|
||||
))
|
||||
error(_('copyVmsNoTargetSr'), _('copyVmsNoTargetSrMessage'))
|
||||
},
|
||||
noop
|
||||
)
|
||||
@@ -951,8 +1023,13 @@ 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' // eslint-disable-line import/first
|
||||
@@ -1036,11 +1113,15 @@ export const attachDiskToVm = (vdi, vm, { bootable, mode, position }) => (
|
||||
|
||||
// DISK ---------------------------------------------------------------
|
||||
|
||||
export const createDisk = (name, size, sr) => (
|
||||
export const createDisk = (name, size, sr, { vm, bootable, mode, position }) => (
|
||||
_call('disk.create', {
|
||||
bootable,
|
||||
mode,
|
||||
name,
|
||||
position,
|
||||
size,
|
||||
sr: resolveId(sr)
|
||||
sr: resolveId(sr),
|
||||
vm: resolveId(vm)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -1060,6 +1141,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'),
|
||||
@@ -1436,16 +1527,14 @@ export const getSchedule = id => (
|
||||
|
||||
export const loadPlugin = async id => (
|
||||
_call('plugin.load', { id })::tap(
|
||||
subscribePlugins.forceRefresh
|
||||
)::rethrow(
|
||||
subscribePlugins.forceRefresh,
|
||||
err => error(_('pluginError'), (err && err.message) || _('unknownPluginError'))
|
||||
)
|
||||
)
|
||||
|
||||
export const unloadPlugin = id => (
|
||||
_call('plugin.unload', { id })::tap(
|
||||
subscribePlugins.forceRefresh
|
||||
)::rethrow(
|
||||
subscribePlugins.forceRefresh,
|
||||
err => error(_('pluginError'), (err && err.message) || _('unknownPluginError'))
|
||||
)
|
||||
)
|
||||
@@ -1467,8 +1556,7 @@ export const configurePlugin = (id, configuration) =>
|
||||
() => {
|
||||
info(_('pluginConfigurationSuccess'), _('pluginConfigurationChanges'))
|
||||
subscribePlugins.forceRefresh()
|
||||
}
|
||||
)::rethrow(
|
||||
},
|
||||
err => error(_('pluginError'), JSON.stringify(err.data) || _('unknownPluginError'))
|
||||
)
|
||||
|
||||
@@ -1516,7 +1604,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))
|
||||
)
|
||||
)
|
||||
@@ -1553,20 +1642,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))
|
||||
)
|
||||
)
|
||||
@@ -1673,16 +1763,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))
|
||||
)
|
||||
)
|
||||
@@ -1697,14 +1785,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))
|
||||
)
|
||||
)
|
||||
@@ -1715,35 +1804,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))
|
||||
)
|
||||
)
|
||||
@@ -1753,9 +1842,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))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1927,18 +2017,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, ipRange }) => _call('xosan.createSR', {
|
||||
template,
|
||||
pif: pif.id,
|
||||
vlan: String(vlan),
|
||||
srs: resolveIds(srs),
|
||||
glusterType,
|
||||
redundancy: Number.parseInt(redundancy)
|
||||
redundancy: Number.parseInt(redundancy),
|
||||
brickSize,
|
||||
memorySize,
|
||||
ipRange
|
||||
})
|
||||
|
||||
export const computeXosanPossibleOptions = lvmSrs => _call('xosan.computeXosanPossibleOptions', { lvmSrs })
|
||||
export const addXosanBricks = (xosansr, lvmsrs, brickSize) => _call('xosan.addBricks', {xosansr, lvmsrs, brickSize})
|
||||
|
||||
export const replaceXosanBrick = (xosansr, previousBrick, newLvmSr, brickSize, onSameVM = false) =>
|
||||
_call('xosan.replaceBrick', resolveIds({ 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 =>
|
||||
@@ -1951,3 +2051,5 @@ export const downloadAndInstallXosanPack = pool =>
|
||||
)
|
||||
|
||||
export const registerXosan = namespace => _call('cloud.registerResource', { namespace: 'xosan' })
|
||||
|
||||
export const fixHostNotInXosanNetwork = (xosanSr, host) => _call('xosan.fixHostNotInNetwork', {xosanSr, host})
|
||||
|
||||
@@ -93,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>
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -452,6 +460,10 @@
|
||||
@extend .fa-server;
|
||||
@extend .text-warning;
|
||||
}
|
||||
&-forget {
|
||||
@extend .fa;
|
||||
@extend .fa-ban;
|
||||
}
|
||||
&-working {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@@ -951,4 +963,15 @@
|
||||
@extend .fa;
|
||||
@extend .fa-star;
|
||||
}
|
||||
|
||||
// XOSAN related
|
||||
|
||||
&-health {
|
||||
@extend .fa;
|
||||
@extend .fa-heartbeat;
|
||||
}
|
||||
&-fix {
|
||||
@extend .fa;
|
||||
@extend .fa-wrench;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,13 @@ const keymap = {
|
||||
NAV_UP: 'k',
|
||||
SELECT: 'x',
|
||||
JUMP_INTO: 'enter'
|
||||
},
|
||||
SortedTable: {
|
||||
SEARCH: '/',
|
||||
NAV_DOWN: 'j',
|
||||
NAV_UP: 'k',
|
||||
SELECT: 'x',
|
||||
ROW_ACTION: 'enter'
|
||||
}
|
||||
}
|
||||
export { keymap as default }
|
||||
@@ -25,6 +32,6 @@ export const help = mapValues(keymap, (shortcuts, contextLabel) => ({
|
||||
name: _(`shortcut_${contextLabel}`),
|
||||
shortcuts: mapValues(shortcuts, (shortcut, label) => ({
|
||||
keys: shortcuts[label],
|
||||
message: _(`shortcut_${label}`)
|
||||
message: _(`shortcut_${contextLabel}_${label}`)
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -212,6 +212,7 @@ const CONTINUOUS_REPLICATION_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...COMMON_SCHEMA.properties,
|
||||
retention: DEPTH_PROPERTY,
|
||||
sr: {
|
||||
type: 'string',
|
||||
'xo:type': 'sr',
|
||||
@@ -563,131 +564,133 @@ export default class New extends Component {
|
||||
|
||||
return (
|
||||
<Upgrade place='newBackup' required={2}>
|
||||
<Wizard><form id='form-new-vm-backup'>
|
||||
<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) =>
|
||||
_(info.label, message => <option key={key} 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>}
|
||||
required
|
||||
schema={backupInfo.schema}
|
||||
uiSchema={backupInfo.uiSchema}
|
||||
onChange={this.linkState('mainParams')}
|
||||
value={this._getMainParams()}
|
||||
/>
|
||||
<form id='form-new-vm-backup'>
|
||||
<Wizard>
|
||||
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<fieldset className='form-group'>
|
||||
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
|
||||
<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='smartMode'
|
||||
onChange={this._handleSmartBackupMode}
|
||||
id='selectBackup'
|
||||
onChange={this.linkState('job.method')}
|
||||
required
|
||||
value={smartBackupMode ? 'smart' : 'normal'}
|
||||
value={method}
|
||||
>
|
||||
{_('normalBackup', message => <option value='normal'>{message}</option>)}
|
||||
{_('smartBackup', message => <option value='smart'>{message}</option>)}
|
||||
{_('noSelectedValue', message => <option value=''>{message}</option>)}
|
||||
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
|
||||
_({ key }, info.label, message => <option value={key}>{message}</option>)
|
||||
)}
|
||||
</select>
|
||||
</fieldset>
|
||||
{smartBackupMode
|
||||
? <Upgrade place='newBackup' required={3}>
|
||||
<GenericInput
|
||||
{(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>}
|
||||
required
|
||||
schema={backupInfo.schema}
|
||||
uiSchema={backupInfo.uiSchema}
|
||||
onChange={this.linkState('mainParams')}
|
||||
value={this._getMainParams()}
|
||||
/>
|
||||
<fieldset className='form-group'>
|
||||
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
|
||||
<select
|
||||
className='form-control'
|
||||
id='smartMode'
|
||||
onChange={this._handleSmartBackupMode}
|
||||
required
|
||||
value={smartBackupMode ? 'smart' : 'normal'}
|
||||
>
|
||||
{_('normalBackup', message => <option value='normal'>{message}</option>)}
|
||||
{_('smartBackup', message => <option value='smart'>{message}</option>)}
|
||||
</select>
|
||||
</fieldset>
|
||||
{smartBackupMode
|
||||
? <Upgrade place='newBackup' required={3}>
|
||||
<GenericInput
|
||||
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
|
||||
onChange={this.linkState('vmsParam')}
|
||||
required
|
||||
schema={SMART_SCHEMA}
|
||||
uiSchema={SMART_UI_SCHEMA}
|
||||
value={vms}
|
||||
/>
|
||||
</Upgrade>
|
||||
: <GenericInput
|
||||
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
|
||||
onChange={this.linkState('vmsParam')}
|
||||
required
|
||||
schema={SMART_SCHEMA}
|
||||
uiSchema={SMART_UI_SCHEMA}
|
||||
schema={NO_SMART_SCHEMA}
|
||||
uiSchema={NO_SMART_UI_SCHEMA}
|
||||
value={vms}
|
||||
/>
|
||||
</Upgrade>
|
||||
: <GenericInput
|
||||
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
|
||||
onChange={this.linkState('vmsParam')}
|
||||
required
|
||||
schema={NO_SMART_SCHEMA}
|
||||
uiSchema={NO_SMART_UI_SCHEMA}
|
||||
value={vms}
|
||||
/>
|
||||
}
|
||||
</div>}
|
||||
</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>
|
||||
</form></Wizard>
|
||||
}
|
||||
</div>}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,15 +2,10 @@ 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'
|
||||
@@ -22,6 +17,14 @@ import {
|
||||
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)} />
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -3,17 +3,18 @@ import ActionRowButton from 'action-row-button'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
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 { 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,
|
||||
@@ -23,18 +24,18 @@ import {
|
||||
isSrWritable
|
||||
} from 'xo'
|
||||
import {
|
||||
flatten,
|
||||
get,
|
||||
isEmpty,
|
||||
map,
|
||||
mapValues
|
||||
} from 'lodash'
|
||||
import {
|
||||
areObjectsFetched,
|
||||
createCollectionWrapper,
|
||||
createGetObject,
|
||||
createGetObjectsOfType,
|
||||
createSelector
|
||||
} from 'selectors'
|
||||
import {
|
||||
flatten,
|
||||
get,
|
||||
map,
|
||||
mapValues
|
||||
} from 'lodash'
|
||||
import {
|
||||
connectStore,
|
||||
formatSize,
|
||||
@@ -304,7 +305,7 @@ 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(
|
||||
@@ -352,6 +353,7 @@ const ALARM_COLUMNS = [
|
||||
.filter([ message => message.name === 'ALARM' ])
|
||||
|
||||
return {
|
||||
areObjectsFetched,
|
||||
alertMessages: getAlertMessages,
|
||||
controlDomainVdis: getControlDomainVdis,
|
||||
userSrs: getUserSrs,
|
||||
@@ -428,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>
|
||||
@@ -437,18 +441,21 @@ 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}
|
||||
shortcutsTarget='body'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
</NoObjects>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -460,9 +467,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
|
||||
@@ -479,7 +488,7 @@ export default class Health extends Component {
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
}
|
||||
</NoObjects>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -491,25 +500,29 @@ export default class Health extends Component {
|
||||
<Icon icon='disk' /> {_('vdisOnControlDomain')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{isEmpty(this.props.controlDomainVdis)
|
||||
? <p className='text-xs-center'>{_('noControlDomainVdis')}</p>
|
||||
: <SortedTable collection={this.props.controlDomainVdis} columns={CONTROL_DOMAIN_VDI_COLUMNS} />
|
||||
}
|
||||
<NoObjects
|
||||
collection={props.areObjectsFetched ? props.controlDomainVdis : null}
|
||||
emptyMessage={_('noControlDomainVdis')}
|
||||
>
|
||||
<SortedTable collection={props.controlDomainVdis} columns={CONTROL_DOMAIN_VDI_COLUMNS} />
|
||||
</NoObjects>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Row className='orphaned-vms'>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<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} shortcutsTarget='.orphaned-vms' />
|
||||
</NoObjects>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -521,9 +534,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
|
||||
@@ -540,7 +555,7 @@ export default class Health extends Component {
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
}
|
||||
</NoObjects>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
@@ -5,10 +5,12 @@ import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import CenterPanel from 'center-panel'
|
||||
import Component from 'base-component'
|
||||
import defined, { get } from 'xo-defined'
|
||||
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'
|
||||
@@ -20,7 +22,6 @@ import {
|
||||
filter,
|
||||
find,
|
||||
forEach,
|
||||
get,
|
||||
identity,
|
||||
includes,
|
||||
isEmpty,
|
||||
@@ -51,18 +52,19 @@ import {
|
||||
startVms,
|
||||
stopHosts,
|
||||
stopVms,
|
||||
subscribeResourceSets,
|
||||
subscribeServers
|
||||
} from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
SelectHost,
|
||||
SelectPool,
|
||||
SelectResourceSet,
|
||||
SelectTag
|
||||
} from 'select-objects'
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
firstDefined,
|
||||
noop
|
||||
} from 'utils'
|
||||
import {
|
||||
@@ -73,7 +75,8 @@ import {
|
||||
createPager,
|
||||
createSelector,
|
||||
createSort,
|
||||
getUser
|
||||
getUser,
|
||||
isAdmin
|
||||
} from 'selectors'
|
||||
import {
|
||||
DropdownButton,
|
||||
@@ -143,6 +146,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 +209,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
|
||||
}
|
||||
@@ -285,12 +387,12 @@ export default class Home extends Component {
|
||||
|
||||
_getDefaultFilter (props = this.props) {
|
||||
const { type } = props
|
||||
const preferences = get(props, 'user.preferences')
|
||||
const defaultFilterName = get(preferences, [ 'defaultHomeFilters', type ])
|
||||
return firstDefined(
|
||||
defaultFilterName && firstDefined(
|
||||
get(homeFilters, [ type, defaultFilterName ]),
|
||||
get(preferences, [ 'filters', type, defaultFilterName ])
|
||||
const preferences = get(() => props.user.preferences)
|
||||
const defaultFilterName = get(() => preferences.defaultHomeFilters[type])
|
||||
return defined(
|
||||
defaultFilterName && defined(
|
||||
() => homeFilters[type][defaultFilterName],
|
||||
() => preferences.filters[type][defaultFilterName]
|
||||
),
|
||||
OPTIONS[type].defaultFilter
|
||||
)
|
||||
@@ -300,8 +402,8 @@ export default class Home extends Component {
|
||||
const sortOption = find(OPTIONS[props.type].sortOptions, 'default')
|
||||
|
||||
return {
|
||||
sortBy: firstDefined(sortOption && sortOption.sortBy, 'name_label'),
|
||||
sortOrder: firstDefined(sortOption && sortOption.sortOrder, 'asc')
|
||||
sortBy: defined(() => sortOption.sortBy, 'name_label'),
|
||||
sortOrder: defined(() => sortOption.sortOrder, 'asc')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,6 +434,7 @@ export default class Home extends Component {
|
||||
selectedHosts: properties.$container,
|
||||
selectedPools: properties.$pool,
|
||||
selectedTags: properties.tags,
|
||||
selectedResourceSets: properties.resourceSet,
|
||||
...sort
|
||||
})
|
||||
|
||||
@@ -369,6 +472,8 @@ export default class Home extends Component {
|
||||
pathname,
|
||||
query: { ...query, s: filter }
|
||||
})
|
||||
|
||||
this.page = 1
|
||||
}
|
||||
|
||||
_clearFilter = () => this._setFilter('')
|
||||
@@ -446,6 +551,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(),
|
||||
@@ -500,10 +618,10 @@ export default class Home extends Component {
|
||||
this.refs.filterInput.focus()
|
||||
break
|
||||
case 'NAV_DOWN':
|
||||
this.setState({ highlighted: (this.state.highlighted + items.length + 1) % items.length || 0 })
|
||||
this.setState({ highlighted: (this.state.highlighted + 1) % items.length || 0 })
|
||||
break
|
||||
case 'NAV_UP':
|
||||
this.setState({ highlighted: (this.state.highlighted + items.length - 1) % items.length || 0 })
|
||||
this.setState({ highlighted: (this.state.highlighted - 1) % items.length || 0 })
|
||||
break
|
||||
case 'SELECT':
|
||||
const itemId = items[this.state.highlighted].id
|
||||
@@ -528,7 +646,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()
|
||||
|
||||
@@ -541,25 +663,27 @@ export default class Home extends Component {
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='input-group'>
|
||||
{!isEmpty(filters) && (
|
||||
<div className='input-group-btn'>
|
||||
<DropdownButton id='filter' bsStyle='info' title={_('homeFilters')}>
|
||||
{!isEmpty(customFilters) && [
|
||||
map(customFilters, (filter, name) =>
|
||||
<MenuItem key={`custom-${name}`} onClick={() => this._setFilter(filter)}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
),
|
||||
<MenuItem divider />
|
||||
]}
|
||||
{map(filters, (filter, label) =>
|
||||
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
|
||||
{_(label)}
|
||||
<span className='input-group-btn'>
|
||||
<DropdownButton id='filter' bsStyle='info' title={_('homeFilters')}>
|
||||
<MenuItem onClick={this._addCustomFilter}>
|
||||
{_('filterSaveAs')}
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
{!isEmpty(customFilters) && [
|
||||
map(customFilters, (filter, name) =>
|
||||
<MenuItem key={`custom-${name}`} onClick={() => this._setFilter(filter)}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
)}
|
||||
</DropdownButton>
|
||||
</div>
|
||||
)}
|
||||
),
|
||||
<MenuItem key='divider' divider />
|
||||
]}
|
||||
{map(filters, (filter, label) =>
|
||||
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
|
||||
{_(label)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</DropdownButton>
|
||||
</span>
|
||||
<input
|
||||
className='form-control'
|
||||
defaultValue={this._getFilter()}
|
||||
@@ -567,27 +691,30 @@ export default class Home extends Component {
|
||||
ref='filterInput'
|
||||
type='text'
|
||||
/>
|
||||
<div className='input-group-btn'>
|
||||
<Tooltip content={_('filterSyntaxLinkTooltip')}>
|
||||
<a
|
||||
className='input-group-addon'
|
||||
href='https://xen-orchestra.com/docs/search.html#filter-syntax'
|
||||
target='_blank'
|
||||
>
|
||||
<Icon icon='info' />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<span className='input-group-btn'>
|
||||
<Button onClick={this._clearFilter}>
|
||||
<Icon icon='clear-search' />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='input-group-btn'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
handler={this._addCustomFilter}
|
||||
icon='save'
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
</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>
|
||||
}
|
||||
@@ -596,89 +723,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()
|
||||
@@ -691,6 +746,7 @@ export default class Home extends Component {
|
||||
selectedHosts,
|
||||
selectedItems,
|
||||
selectedPools,
|
||||
selectedResourceSets,
|
||||
selectedTags,
|
||||
sortBy
|
||||
} = this.state
|
||||
@@ -705,7 +761,8 @@ export default class Home extends Component {
|
||||
mainActions,
|
||||
otherActions,
|
||||
showHostsSelector,
|
||||
showPoolsSelector
|
||||
showPoolsSelector,
|
||||
showResourceSetsSelector
|
||||
} = options
|
||||
|
||||
// Necessary because indeterminate cannot be used as an attribute
|
||||
@@ -785,7 +842,6 @@ export default class Home extends Component {
|
||||
<Button btnStyle='link'><Icon icon='pool' /> {_('homeAllPools')}</Button>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
{' '}
|
||||
{showHostsSelector && (
|
||||
<OverlayTrigger
|
||||
trigger='click'
|
||||
@@ -805,7 +861,6 @@ export default class Home extends Component {
|
||||
<Button btnStyle='link'><Icon icon='host' /> {_('homeAllHosts')}</Button>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
{' '}
|
||||
<OverlayTrigger
|
||||
autoFocus
|
||||
trigger='click'
|
||||
@@ -825,7 +880,23 @@ export default class Home extends Component {
|
||||
>
|
||||
<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 })}>
|
||||
@@ -859,7 +930,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>
|
||||
))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import defined from 'xo-defined'
|
||||
import Ellipsis, { EllipsisContainer } from 'ellipsis'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
} from 'xo'
|
||||
import {
|
||||
connectStore,
|
||||
firstDefined,
|
||||
osFamily
|
||||
} from 'utils'
|
||||
import {
|
||||
@@ -74,7 +74,7 @@ export default class TemplateItem extends Component {
|
||||
<Col mediumSize={4} className={styles.itemExpanded}>
|
||||
<span>
|
||||
<Number value={vm.CPUs.number} onChange={this._setCpus} />x <Icon icon='cpu' className='mr-1' />
|
||||
<Size value={firstDefined(vm.memory.size, null)} onChange={this._setMemory} /> <Icon icon='memory' />
|
||||
<Size value={defined(vm.memory.size, null)} onChange={this._setMemory} /> <Icon icon='memory' />
|
||||
</span>
|
||||
</Col>
|
||||
<Col largeSize={4} className={styles.itemExpanded}>
|
||||
|
||||
@@ -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 {
|
||||
{' '} {' '}
|
||||
{formatSize(vm.memory.size)} <Icon icon='memory' />
|
||||
{' '} {' '}
|
||||
{formatSize(this.props.totalDiskSize)} <Icon icon='disk' />
|
||||
{' '} {' '}
|
||||
{isEmpty(vm.snapshots)
|
||||
? null
|
||||
: <span>{vm.snapshots.length}x <Icon icon='vm-snapshot' /></span>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -255,7 +255,6 @@ export default class Host extends Component {
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
<Col>
|
||||
<NavTabs>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -92,7 +92,6 @@ export default class extends Component {
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
@@ -238,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>
|
||||
|
||||
@@ -381,7 +381,7 @@ export default class Jobs extends Component {
|
||||
/>
|
||||
<input type='text' ref='name' className='form-control mb-1 mt-1' placeholder={formatMessage(messages.jobNamePlaceholder)} pattern='[^_]+' required />
|
||||
<SelectPlainObject ref='method' options={actions} optionKey='method' onChange={this._handleSelectMethod} placeholder={_('jobActionPlaceHolder')} />
|
||||
<input type='number' onChange={this.linkState('timeout')} value={state.timeout} className='form-control mb-1 mt-1' placeholder='Job timeout (seconds)' />
|
||||
<input type='number' onChange={this.linkState('timeout')} value={state.timeout || ''} className='form-control mb-1 mt-1' placeholder={formatMessage(messages.jobTimeoutPlaceHolder)} />
|
||||
{action && <fieldset>
|
||||
<GenericInput ref='params' schema={action.info} uiSchema={action.uiSchema} label={action.method} required />
|
||||
{job && <p className='text-warning'>{_('jobEditMessage', { name: job.name, id: job.id.slice(4, 8) })}</p>}
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
import _, { FormattedDuration } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import BaseComponent from 'base-component'
|
||||
import ButtonGroup from 'button-group'
|
||||
import classnames from 'classnames'
|
||||
import forEach from 'lodash/forEach'
|
||||
import get from 'lodash/get'
|
||||
import Icon from 'icon'
|
||||
import includes from 'lodash/includes'
|
||||
import map from 'lodash/map'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import NoObjects from 'no-objects'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React, { Component } from 'react'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
import SortedTable from 'sorted-table'
|
||||
import Tooltip from 'tooltip'
|
||||
import { alert, confirm } from 'modal'
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObject } from 'selectors'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
|
||||
import {
|
||||
connectStore,
|
||||
formatSize,
|
||||
formatSpeed
|
||||
} from 'utils'
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBlock
|
||||
} from 'card'
|
||||
|
||||
import {
|
||||
forEach,
|
||||
get,
|
||||
includes,
|
||||
isEmpty,
|
||||
map,
|
||||
orderBy
|
||||
} from 'lodash'
|
||||
import {
|
||||
deleteJobsLog,
|
||||
subscribeJobsLogs
|
||||
@@ -52,9 +59,9 @@ class JobParam extends Component {
|
||||
id
|
||||
} = this.props
|
||||
|
||||
return object
|
||||
? <span><strong>{object.type || paramKey}</strong>: {renderXoItem(object)} </span>
|
||||
: <span><strong>{paramKey}:</strong> {String(id)} </span>
|
||||
return object != null
|
||||
? _.keyValue(object.type || paramKey, renderXoItem(object))
|
||||
: _.keyValue(paramKey, String(id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,33 +77,98 @@ class JobReturn extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const Log = props => <ul className='list-group'>
|
||||
{map(props.log.calls, call => {
|
||||
const { returnedValue } = call
|
||||
let id
|
||||
if (returnedValue != null) {
|
||||
id = returnedValue.id
|
||||
if (id === undefined) {
|
||||
id = returnedValue
|
||||
}
|
||||
}
|
||||
const JobCallStateInfos = ({ end, error }) => {
|
||||
const [ icon, tooltip ] = error !== undefined
|
||||
? ['halted', 'failedJobCall']
|
||||
: end !== undefined
|
||||
? ['running', 'successfulJobCall']
|
||||
: ['busy', 'jobCallInProgess']
|
||||
|
||||
return <li key={call.callKey} className='list-group-item'>
|
||||
<strong className='text-info'>{call.method}: </strong><br />
|
||||
{map(call.params, (value, key) => [ <JobParam id={value} paramKey={key} key={key} />, <br /> ])}
|
||||
{id !== undefined && <span>{' '}<JobReturn id={id} /></span>}
|
||||
{call.error &&
|
||||
<span className='text-danger'>
|
||||
<Icon icon='error' />
|
||||
{' '}
|
||||
{call.error.message
|
||||
? <strong>{call.error.message}</strong>
|
||||
: JSON.stringify(call.error)
|
||||
return <Tooltip content={_(tooltip)}>
|
||||
<Icon icon={icon} />
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
const JobTransferredDataInfos = ({ start, end, size }) => <div>
|
||||
<span><strong>{_('jobTransferredDataSize')}</strong> {formatSize(size)}</span>
|
||||
<br />
|
||||
<span><strong>{_('jobTransferredDataSpeed')}</strong> {formatSpeed(size, end - start)}</span>
|
||||
</div>
|
||||
|
||||
const CALL_FILTER_OPTIONS = [
|
||||
{label: 'successfulJobCall', value: 'success'},
|
||||
{label: 'failedJobCall', value: 'error'},
|
||||
{label: 'jobCallInProgess', value: 'running'},
|
||||
{label: 'allJobCalls', value: 'all'}
|
||||
]
|
||||
|
||||
const PREDICATES = {
|
||||
all: () => true,
|
||||
error: call => call.error !== undefined,
|
||||
running: call => call.end === undefined && call.error === undefined,
|
||||
success: call => call.end !== undefined && call.error === undefined
|
||||
}
|
||||
|
||||
class Log extends BaseComponent {
|
||||
state = {
|
||||
filter: 'all'
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const predicate = PREDICATES[state.filter]
|
||||
|
||||
return <div>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this.linkState('filter')}
|
||||
value={state.filter}
|
||||
>
|
||||
{map(CALL_FILTER_OPTIONS, ({ label, value }) => _(
|
||||
{ key: value },
|
||||
label,
|
||||
message => <option value={value}>{message}</option>
|
||||
))}
|
||||
</select>
|
||||
<br />
|
||||
<ul className='list-group'>
|
||||
{map(props.log.calls, call => {
|
||||
const {
|
||||
end,
|
||||
error,
|
||||
returnedValue,
|
||||
start
|
||||
} = call
|
||||
|
||||
let id
|
||||
if (returnedValue != null) {
|
||||
id = returnedValue.id
|
||||
if (id === undefined && typeof returnedValue === 'string') {
|
||||
id = returnedValue
|
||||
}
|
||||
}
|
||||
</span>}
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
|
||||
return predicate(call) && <li key={call.callKey} className='list-group-item'>
|
||||
<strong className='text-info'>{call.method}: </strong><JobCallStateInfos end={end} error={error} /><br />
|
||||
{map(call.params, (value, key) => [ <JobParam id={value} paramKey={key} key={key} />, <br /> ])}
|
||||
{end !== undefined && _.keyValue(_('jobDuration'), <FormattedDuration duration={end - start} />)}
|
||||
{returnedValue != null && returnedValue.size !== undefined && <JobTransferredDataInfos start={start} end={end} size={returnedValue.size} />}
|
||||
{id !== undefined && <span>{' '}<JobReturn id={id} /></span>}
|
||||
{call.error &&
|
||||
<span className='text-danger'>
|
||||
<Icon icon='error' />
|
||||
{' '}
|
||||
{call.error.message
|
||||
? <strong>{call.error.message}</strong>
|
||||
: JSON.stringify(call.error)
|
||||
}
|
||||
</span>}
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const showCalls = log => alert(_('jobModalTitle', { job: log.jobId }), <Log log={log} />)
|
||||
|
||||
@@ -165,7 +237,6 @@ export default class LogList extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
logs: [],
|
||||
logsToClear: []
|
||||
}
|
||||
this.filters = {
|
||||
@@ -208,6 +279,7 @@ export default class LogList extends Component {
|
||||
callKey: logKey,
|
||||
params: data.params,
|
||||
method: data.method,
|
||||
start: time,
|
||||
time
|
||||
}
|
||||
} else if (data.event === 'jobCall.end') {
|
||||
@@ -219,6 +291,7 @@ export default class LogList extends Component {
|
||||
entry.meta = 'error'
|
||||
} else {
|
||||
call.returnedValue = data.returnedValue
|
||||
call.end = time
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,12 +326,12 @@ export default class LogList extends Component {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='log' /> Logs<span className='pull-right'><ActionButton disabled={!logs.length} btnStyle='danger' handler={this._deleteAllLogs} icon='delete' /></span>
|
||||
<Icon icon='log' /> Logs<span className='pull-right'><ActionButton disabled={isEmpty(logs)} btnStyle='danger' handler={this._deleteAllLogs} icon='delete' /></span>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{logs.length
|
||||
? <SortedTable collection={logs} columns={LOG_COLUMNS} filters={this.filters} />
|
||||
: <p>{_('noLogs')}</p>}
|
||||
<NoObjects collection={logs} emptyMessage={_('noLogs')}>
|
||||
<SortedTable collection={logs} columns={LOG_COLUMNS} filters={this.filters} />
|
||||
</NoObjects>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -166,7 +166,7 @@ export default class Menu extends Component {
|
||||
{ to: '/tasks', icon: 'task', label: 'taskMenu', pill: nTasks },
|
||||
isAdmin && { to: '/xosan', icon: 'menu-xosan', label: 'xosan' },
|
||||
!(noOperatablePools && noResourceSets) && { to: '/vms/new', icon: 'menu-new', label: 'newMenu', subMenu: [
|
||||
{ to: '/vms/new', icon: 'menu-new-vm', label: 'newVmPage' },
|
||||
(isAdmin || !noResourceSets) && { to: '/vms/new', icon: 'menu-new-vm', label: 'newVmPage' },
|
||||
isAdmin && { to: '/new/sr', icon: 'menu-new-sr', label: 'newSrPage' },
|
||||
isAdmin && { to: '/settings/servers', icon: 'menu-settings-servers', label: 'newServerPage' },
|
||||
!noOperatablePools && { to: '/vms/import', icon: 'menu-new-import', label: 'newImport' }
|
||||
|
||||
@@ -4,6 +4,7 @@ import BaseComponent from 'base-component'
|
||||
import Button from 'button'
|
||||
import classNames from 'classnames'
|
||||
import DebounceInput from 'react-debounce-input'
|
||||
import defined from 'xo-defined'
|
||||
import Icon from 'icon'
|
||||
import isIp from 'is-ip'
|
||||
import Page from '../page'
|
||||
@@ -66,14 +67,13 @@ import {
|
||||
addSubscriptions,
|
||||
buildTemplate,
|
||||
connectStore,
|
||||
firstDefined,
|
||||
formatSize,
|
||||
getCoresPerSocketPossibilities,
|
||||
generateReadableRandomString,
|
||||
noop,
|
||||
resolveResourceSet
|
||||
} from 'utils'
|
||||
import {
|
||||
createFilter,
|
||||
createSelector,
|
||||
createGetObject,
|
||||
createGetObjectsOfType,
|
||||
@@ -90,8 +90,6 @@ const NB_VMS_MAX = 100
|
||||
|
||||
const getObject = createGetObject((_, id) => id)
|
||||
|
||||
const returnTrue = () => true
|
||||
|
||||
// Sub-components
|
||||
|
||||
const SectionContent = ({ column, children }) => (
|
||||
@@ -240,9 +238,6 @@ export default class NewVm extends BaseComponent {
|
||||
|
||||
// Utils -----------------------------------------------------------------------
|
||||
|
||||
getUniqueId () {
|
||||
return this._uniqueId++
|
||||
}
|
||||
get _isDiskTemplate () {
|
||||
const { template } = this.state.state
|
||||
return template &&
|
||||
@@ -403,7 +398,7 @@ export default class NewVm extends BaseComponent {
|
||||
}
|
||||
const vdi = getObject(storeState, vbd.VDI, resourceSet)
|
||||
if (vdi) {
|
||||
existingDisks[this.getUniqueId()] = {
|
||||
existingDisks[vbd.position] = {
|
||||
name_label: vdi.name_label,
|
||||
name_description: vdi.name_description,
|
||||
size: vdi.size,
|
||||
@@ -418,7 +413,6 @@ export default class NewVm extends BaseComponent {
|
||||
forEach(template.VIFs, vifId => {
|
||||
const vif = getObject(storeState, vifId, resourceSet)
|
||||
VIFs.push({
|
||||
id: this.getUniqueId(),
|
||||
network: pool || isInResourceSet(vif.$network)
|
||||
? vif.$network
|
||||
: resourceSet.objectsByType['network'][0].id
|
||||
@@ -427,7 +421,6 @@ export default class NewVm extends BaseComponent {
|
||||
if (VIFs.length === 0) {
|
||||
const networkId = this._getDefaultNetworkId()
|
||||
VIFs.push({
|
||||
id: this.getUniqueId(),
|
||||
network: networkId
|
||||
})
|
||||
}
|
||||
@@ -454,12 +447,10 @@ export default class NewVm extends BaseComponent {
|
||||
// disks
|
||||
existingDisks,
|
||||
VDIs: map(template.template_info.disks, disk => {
|
||||
const device = String(this.getUniqueId())
|
||||
return {
|
||||
...disk,
|
||||
device,
|
||||
name_description: disk.name_description || 'Created by XO',
|
||||
name_label: (name_label || 'disk') + '_' + device,
|
||||
name_label: (name_label || 'disk') + '_' + generateReadableRandomString(5),
|
||||
SR: pool
|
||||
? pool.default_SR
|
||||
: resourceSet.objectsByType['SR'][0].id
|
||||
@@ -493,13 +484,6 @@ export default class NewVm extends BaseComponent {
|
||||
objectsIds => id => includes(objectsIds, id)
|
||||
)
|
||||
|
||||
_getCanOperate = createSelector(
|
||||
() => this.props.isAdmin,
|
||||
() => this.props.permissions,
|
||||
(isAdmin, permissions) => isAdmin
|
||||
? returnTrue
|
||||
: ({ id }) => permissions && permissions[id] && permissions[id].operate
|
||||
)
|
||||
_getVmPredicate = createSelector(
|
||||
this._getIsInPool,
|
||||
this._getIsInResourceSet,
|
||||
@@ -548,11 +532,7 @@ export default class NewVm extends BaseComponent {
|
||||
},
|
||||
(networks, poolId) => filter(networks, network => network.$pool === poolId)
|
||||
)
|
||||
_getOperatablePools = createFilter(
|
||||
() => this.props.pools,
|
||||
this._getCanOperate,
|
||||
[ (pool, canOperate) => canOperate(pool) ]
|
||||
)
|
||||
|
||||
_getAffinityHostPredicate = createSelector(
|
||||
() => this.props.pool,
|
||||
() => this.state.state.existingDisks,
|
||||
@@ -655,12 +635,10 @@ export default class NewVm extends BaseComponent {
|
||||
_addVdi = () => {
|
||||
const { state } = this.state
|
||||
const { pool } = this.props
|
||||
const device = String(this.getUniqueId())
|
||||
|
||||
this._setState({ VDIs: [ ...state.VDIs, {
|
||||
device,
|
||||
name_description: 'Created by XO',
|
||||
name_label: (state.name_label || 'disk') + '_' + device,
|
||||
name_label: (state.name_label || 'disk') + '_' + generateReadableRandomString(5),
|
||||
SR: pool && pool.default_SR,
|
||||
type: 'system'
|
||||
}] })
|
||||
@@ -674,7 +652,6 @@ export default class NewVm extends BaseComponent {
|
||||
const networkId = this._getDefaultNetworkId()
|
||||
|
||||
this._setState({ VIFs: [ ...this.state.state.VIFs, {
|
||||
id: this.getUniqueId(),
|
||||
network: networkId
|
||||
}] })
|
||||
}
|
||||
@@ -709,13 +686,10 @@ export default class NewVm extends BaseComponent {
|
||||
// MAIN ------------------------------------------------------------------------
|
||||
|
||||
_renderHeader = () => {
|
||||
const { pool } = this.props
|
||||
const showSelectPool = !isEmpty(this._getOperatablePools())
|
||||
const showSelectResourceSet = !this.props.isAdmin && !isEmpty(this.props.resourceSets)
|
||||
const {isAdmin, pool, resourceSets} = this.props
|
||||
const selectPool = <span className={styles.inlineSelect}>
|
||||
<SelectPool
|
||||
onChange={this._selectPool}
|
||||
predicate={this._getCanOperate()}
|
||||
value={pool}
|
||||
/>
|
||||
</span>
|
||||
@@ -729,14 +703,9 @@ export default class NewVm extends BaseComponent {
|
||||
<Row>
|
||||
<Col mediumSize={12}>
|
||||
<h2>
|
||||
{showSelectPool && showSelectResourceSet
|
||||
? _('newVmCreateNewVmOn2', {
|
||||
select1: selectPool,
|
||||
select2: selectResourceSet
|
||||
})
|
||||
: showSelectPool || showSelectResourceSet
|
||||
{isAdmin || !isEmpty(resourceSets)
|
||||
? _('newVmCreateNewVmOn', {
|
||||
select: showSelectPool ? selectPool : selectResourceSet
|
||||
select: isAdmin ? selectPool : selectResourceSet
|
||||
})
|
||||
: _('newVmCreateNewVmNoPermission')
|
||||
}
|
||||
@@ -859,7 +828,7 @@ export default class NewVm extends BaseComponent {
|
||||
<SizeInput
|
||||
className={styles.sizeInput}
|
||||
onChange={this._linkState('memoryDynamicMax')}
|
||||
value={firstDefined(memoryDynamicMax, null)}
|
||||
value={defined(memoryDynamicMax, null)}
|
||||
/>
|
||||
</Item>
|
||||
<Item label={_('vmCpuTopology')}>
|
||||
@@ -1182,7 +1151,7 @@ export default class NewVm extends BaseComponent {
|
||||
className={styles.sizeInput}
|
||||
onChange={this._linkState(`existingDisks.${index}.size`)}
|
||||
readOnly={!configDrive}
|
||||
value={firstDefined(disk.size, null)}
|
||||
value={defined(disk.size, null)}
|
||||
/>
|
||||
</Item>
|
||||
</LineItem>
|
||||
@@ -1190,7 +1159,7 @@ export default class NewVm extends BaseComponent {
|
||||
</div>)}
|
||||
|
||||
{/* VDIs */}
|
||||
{map(VDIs, (vdi, index) => <div key={vdi.device}>
|
||||
{map(VDIs, (vdi, index) => <div key={index}>
|
||||
<LineItem>
|
||||
<Item label={_('newVmSrLabel')}>
|
||||
<span className={styles.inlineSelect}>
|
||||
@@ -1227,7 +1196,7 @@ export default class NewVm extends BaseComponent {
|
||||
<SizeInput
|
||||
className={styles.sizeInput}
|
||||
onChange={this._linkState(`VDIs.${index}.size`)}
|
||||
value={firstDefined(vdi.size, null)}
|
||||
value={defined(vdi.size, null)}
|
||||
/>
|
||||
</Item>
|
||||
<Item>
|
||||
@@ -1276,6 +1245,7 @@ export default class NewVm extends BaseComponent {
|
||||
showAdvanced,
|
||||
tags
|
||||
} = this.state.state
|
||||
const { isAdmin } = this.props
|
||||
const { formatMessage } = this.props.intl
|
||||
return <Section icon='new-vm-advanced' title='newVmAdvancedPanel' done={this._isAdvancedDone()}>
|
||||
<SectionContent column>
|
||||
@@ -1346,13 +1316,13 @@ export default class NewVm extends BaseComponent {
|
||||
</SectionContent>,
|
||||
<SectionContent>
|
||||
<Item label={_('newVmDynamicMinLabel')}>
|
||||
<SizeInput value={firstDefined(memoryDynamicMin, null)} onChange={this._linkState('memoryDynamicMin')} className={styles.sizeInput} />
|
||||
<SizeInput value={defined(memoryDynamicMin, null)} onChange={this._linkState('memoryDynamicMin')} className={styles.sizeInput} />
|
||||
</Item>
|
||||
<Item label={_('newVmDynamicMaxLabel')}>
|
||||
<SizeInput value={firstDefined(memoryDynamicMax, null)} onChange={this._linkState('memoryDynamicMax')} className={styles.sizeInput} />
|
||||
<SizeInput value={defined(memoryDynamicMax, null)} onChange={this._linkState('memoryDynamicMax')} className={styles.sizeInput} />
|
||||
</Item>
|
||||
<Item label={_('newVmStaticMaxLabel')}>
|
||||
<SizeInput value={firstDefined(memoryStaticMax, null)} onChange={this._linkState('memoryStaticMax')} className={styles.sizeInput} />
|
||||
<SizeInput value={defined(memoryStaticMax, null)} onChange={this._linkState('memoryStaticMax')} className={styles.sizeInput} />
|
||||
</Item>
|
||||
</SectionContent>,
|
||||
<SectionContent>
|
||||
@@ -1411,7 +1381,7 @@ export default class NewVm extends BaseComponent {
|
||||
)}
|
||||
</LineItem>}
|
||||
</SectionContent>,
|
||||
<SectionContent>
|
||||
isAdmin && <SectionContent>
|
||||
<Item label={_('newVmAffinityHost')}>
|
||||
<SelectHost
|
||||
onChange={this._linkState('affinityHost')}
|
||||
|
||||
@@ -46,45 +46,46 @@ import {
|
||||
options: propTypes.array.isRequired
|
||||
})
|
||||
class SelectIqn extends Component {
|
||||
_computeOptions (props = this.props) {
|
||||
this.setState({
|
||||
options: map(props.options, (iqn, id) => ({
|
||||
value: `${iqn.ip}$${iqn.iqn}`,
|
||||
label: `${iqn.iqn} (${iqn.ip})`
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
_handleChange = value => {
|
||||
const { onChange } = this.props
|
||||
|
||||
value = value.value
|
||||
const index = value.indexOf('$')
|
||||
|
||||
this.setState({
|
||||
value
|
||||
}, () => onChange({
|
||||
ip: value.slice(0, index),
|
||||
iqn: value.slice(index + 1)
|
||||
_getOptions = createSelector(
|
||||
() => this.props.options,
|
||||
options => map(options, ({ ip, iqn }, index) => ({
|
||||
label: `${iqn} (${ip})`,
|
||||
value: index
|
||||
}))
|
||||
)
|
||||
|
||||
_handleChange = ({ value }) => {
|
||||
const { props } = this
|
||||
|
||||
this.setState(
|
||||
{ value },
|
||||
() => props.onChange(props.options[value])
|
||||
)
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this._computeOptions()
|
||||
componentDidMount () {
|
||||
return this.componentDidUpdate()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
this._computeOptions(props)
|
||||
componentDidUpdate () {
|
||||
let options
|
||||
if (
|
||||
this.state.value === null &&
|
||||
(options = this._getOptions()).length === 1
|
||||
) {
|
||||
this._handleChange(options[0])
|
||||
}
|
||||
}
|
||||
|
||||
state = { value: null }
|
||||
|
||||
render () {
|
||||
const { state } = this
|
||||
return (
|
||||
<Select
|
||||
clearable={false}
|
||||
onChange={this._handleChange}
|
||||
options={state.options}
|
||||
value={state.value}
|
||||
options={this._getOptions()}
|
||||
value={this.state.value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -95,37 +96,45 @@ class SelectIqn extends Component {
|
||||
options: propTypes.array.isRequired
|
||||
})
|
||||
class SelectLun extends Component {
|
||||
_computeOptions (props = this.props) {
|
||||
this.setState({
|
||||
options: map(props.options, lun => ({
|
||||
value: lun.id,
|
||||
label: `LUN ${lun.id}: ${lun.serial} - ${formatSize(+lun.size)} - (${lun.vendor})`
|
||||
}))
|
||||
})
|
||||
_getOptions = createSelector(
|
||||
() => this.props.options,
|
||||
options => map(options, (lun, index) => ({
|
||||
label: `LUN ${lun.id}: ${lun.serial} - ${formatSize(+lun.size)} - (${lun.vendor})`,
|
||||
value: index
|
||||
}))
|
||||
)
|
||||
|
||||
_handleChange = ({ value }) => {
|
||||
const { props } = this
|
||||
this.setState(
|
||||
{ value },
|
||||
() => props.onChange(props.options[value])
|
||||
)
|
||||
}
|
||||
|
||||
_handleChange = value => {
|
||||
const { onChange, options } = this.props
|
||||
value = value.value
|
||||
this.setState({ value }, () => onChange(options[value]))
|
||||
componentDidMount () {
|
||||
return this.componentDidUpdate()
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this._computeOptions()
|
||||
componentDidUpdate () {
|
||||
let options
|
||||
if (
|
||||
this.state.value === null &&
|
||||
(options = this._getOptions()).length === 1
|
||||
) {
|
||||
this._handleChange(options[0])
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
this._computeOptions(props)
|
||||
}
|
||||
state = { value: null }
|
||||
|
||||
render () {
|
||||
const { state } = this
|
||||
return (
|
||||
<Select
|
||||
clearable={false}
|
||||
onChange={this._handleChange}
|
||||
options={state.options}
|
||||
value={state.value}
|
||||
options={this._getOptions()}
|
||||
value={this.state.value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -172,6 +181,7 @@ export default class New extends Component {
|
||||
host: hostId && getObject(store.getState(), hostId),
|
||||
iqn: undefined,
|
||||
iqns: undefined,
|
||||
loading: 0,
|
||||
lockCreation: undefined,
|
||||
lun: undefined,
|
||||
luns: undefined,
|
||||
@@ -279,7 +289,7 @@ export default class New extends Component {
|
||||
} = this.state
|
||||
|
||||
try {
|
||||
this.setState({loading: true})
|
||||
this.setState(({ loading }) => ({ loading: loading + 1 }))
|
||||
const luns = await probeSrIscsiLuns(host.id, iqn.ip, iqn.iqn, username && username.value, password && password.value)
|
||||
this.setState({
|
||||
iqn,
|
||||
@@ -288,7 +298,7 @@ export default class New extends Component {
|
||||
} catch (err) {
|
||||
error('LUNs Detection', err.message || String(err))
|
||||
} finally {
|
||||
this.setState({loading: undefined})
|
||||
this.setState(({ loading }) => ({ loading: loading - 1 }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +314,7 @@ export default class New extends Component {
|
||||
} = this.state
|
||||
|
||||
try {
|
||||
this.setState({loading: true})
|
||||
this.setState(({ loading }) => ({ loading: loading + 1 }))
|
||||
const list = await probeSrIscsiExists(host.id, iqn.ip, iqn.iqn, lun.scsiId, +port.value, username && username.value, password && password.value)
|
||||
const srIds = map(this.getHostSrs(), sr => sr.id)
|
||||
const used = filter(list, item => includes(srIds, item.id))
|
||||
@@ -319,7 +329,7 @@ export default class New extends Component {
|
||||
} catch (err) {
|
||||
error('iSCSI Error', err.message || String(err))
|
||||
} finally {
|
||||
this.setState({loading: undefined})
|
||||
this.setState(({ loading }) => ({ loading: loading - 1 }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +385,7 @@ export default class New extends Component {
|
||||
} = this.state
|
||||
|
||||
try {
|
||||
this.setState({loading: true})
|
||||
this.setState(({ loading }) => ({ loading: loading + 1 }))
|
||||
const list = await probeSrNfsExists(host.id, server.value, path)
|
||||
const srIds = map(this.getHostSrs(), sr => sr.id)
|
||||
const used = filter(list, item => includes(srIds, item.id))
|
||||
@@ -390,7 +400,7 @@ export default class New extends Component {
|
||||
} catch (err) {
|
||||
error('NFS Error', err.message || String(err))
|
||||
} finally {
|
||||
this.setState({loading: undefined})
|
||||
this.setState(({ loading }) => ({ loading: loading - 1 }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,7 +501,7 @@ export default class New extends Component {
|
||||
>
|
||||
<option value={null}>{formatMessage(messages.noSelectedValue)}</option>
|
||||
{map(typeGroups, (types, group) =>
|
||||
<optgroup label={SR_GROUP_TO_LABEL[group]}>
|
||||
<optgroup key={group} label={SR_GROUP_TO_LABEL[group]}>
|
||||
{map(types, type =>
|
||||
<option key={type} value={type}>{SR_TYPE_TO_LABEL[type]}</option>
|
||||
)}
|
||||
@@ -657,7 +667,7 @@ export default class New extends Component {
|
||||
}
|
||||
</fieldset>
|
||||
}
|
||||
{loading &&
|
||||
{loading !== 0 &&
|
||||
<Icon icon='loading' />
|
||||
}
|
||||
</Section>
|
||||
|
||||
@@ -1,39 +1,78 @@
|
||||
import ActionBar from 'action-bar'
|
||||
import _ from 'intl'
|
||||
import ActionBar, { Action } from 'action-bar'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import {
|
||||
addHostToPool
|
||||
find
|
||||
} from 'lodash'
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
noop
|
||||
} from 'utils'
|
||||
import {
|
||||
addHostToPool,
|
||||
disconnectServer,
|
||||
subscribeServers
|
||||
} from 'xo'
|
||||
|
||||
const NOT_IMPLEMENTED = () => {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
@connectStore({
|
||||
hosts: createGetObjectsOfType('host')
|
||||
})
|
||||
@addSubscriptions({
|
||||
servers: subscribeServers
|
||||
})
|
||||
export default class PoolActionBar extends Component {
|
||||
_getMasterAddress = createSelector(
|
||||
() => this.props.pool && this.props.pool.master,
|
||||
() => this.props.hosts,
|
||||
(poolMaster, hosts) => {
|
||||
const master = find(hosts, { id: poolMaster })
|
||||
|
||||
const PoolActionBar = ({ pool }) => (
|
||||
<ActionBar
|
||||
actions={[
|
||||
{
|
||||
icon: 'add-sr',
|
||||
label: 'addSrLabel',
|
||||
redirectOnSuccess: `new/sr?host=${pool.master}`
|
||||
},
|
||||
{
|
||||
icon: 'add-vm',
|
||||
label: 'addVmLabel',
|
||||
redirectOnSuccess: `vms/new?pool=${pool.id}`
|
||||
},
|
||||
{
|
||||
icon: 'add-host',
|
||||
label: 'addHostLabel',
|
||||
handler: addHostToPool
|
||||
},
|
||||
{
|
||||
icon: 'disconnect',
|
||||
label: 'disconnectServer',
|
||||
handler: NOT_IMPLEMENTED // TODO disconnect server
|
||||
}
|
||||
]}
|
||||
display='icon'
|
||||
param={pool}
|
||||
/>
|
||||
)
|
||||
export default PoolActionBar
|
||||
return master && master.address
|
||||
}
|
||||
)
|
||||
|
||||
_getServer = createSelector(
|
||||
this._getMasterAddress,
|
||||
() => this.props.servers,
|
||||
(masterAddress, servers) => find(servers, { host: masterAddress })
|
||||
)
|
||||
|
||||
_disconnectServer = () =>
|
||||
disconnectServer(this._getServer())
|
||||
|
||||
render () {
|
||||
const { pool } = this.props
|
||||
|
||||
return <ActionBar
|
||||
display='icon'
|
||||
handlerParam={pool}
|
||||
>
|
||||
<Action
|
||||
handler={noop}
|
||||
icon='add-sr'
|
||||
label={_('addSrLabel')}
|
||||
redirectOnSuccess={`new/sr?host=${pool.master}`}
|
||||
/>
|
||||
<Action
|
||||
handler={noop}
|
||||
icon='add-vm'
|
||||
label={_('addVmLabel')}
|
||||
redirectOnSuccess={`vms/new?pool=${pool.id}`}
|
||||
/>
|
||||
<Action
|
||||
handler={addHostToPool}
|
||||
icon='add-host'
|
||||
label={_('addHostLabel')}
|
||||
/>
|
||||
<Action
|
||||
handler={this._disconnectServer}
|
||||
icon='disconnect'
|
||||
label={_('disconnectServer')}
|
||||
redirectOnSuccess='/home'
|
||||
/>
|
||||
</ActionBar>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ export default class Pool extends Component {
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
<Col>
|
||||
<NavTabs>
|
||||
|
||||
@@ -1,10 +1,43 @@
|
||||
import _ from 'intl'
|
||||
import Copiable from 'copiable'
|
||||
import React from 'react'
|
||||
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Copiable from 'copiable'
|
||||
import SelectFiles from 'select-files'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { installSupplementalPackOnAllHosts } from 'xo'
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { XoSelect } from 'editable'
|
||||
import { installSupplementalPackOnAllHosts, setPoolMaster } from 'xo'
|
||||
import {
|
||||
Container,
|
||||
Row,
|
||||
Col
|
||||
} from 'grid'
|
||||
|
||||
@connectStore(() => ({
|
||||
master: createGetObjectsOfType('host').find(
|
||||
(_, { pool }) => ({ id: pool.master })
|
||||
)
|
||||
}))
|
||||
class PoolMaster extends Component {
|
||||
_getPoolMasterPredicate = host => host.$pool === this.props.pool.id
|
||||
|
||||
_onChange = host => setPoolMaster(host)
|
||||
|
||||
render () {
|
||||
const { pool, master } = this.props
|
||||
|
||||
return <XoSelect
|
||||
onChange={this._onChange}
|
||||
predicate={this._getPoolMasterPredicate}
|
||||
value={pool.master}
|
||||
xoType='host'
|
||||
>
|
||||
{master.name_label}
|
||||
</XoSelect>
|
||||
}
|
||||
}
|
||||
|
||||
export default ({
|
||||
pool
|
||||
@@ -32,6 +65,14 @@ export default ({
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col size={3}>
|
||||
<strong>{_('setpoolMaster')}</strong>
|
||||
</Col>
|
||||
<Col size={9}>
|
||||
<PoolMaster pool={pool} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<h3 className='mt-1 mb-1'>{_('supplementalPackPoolNew')}</h3>
|
||||
<Upgrade place='poolSupplementalPacks' required={2}>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import _ from 'intl'
|
||||
import ActionRow from 'action-row-button'
|
||||
import Button from 'button'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import React, { Component } from 'react'
|
||||
import TabButton from 'tab-button'
|
||||
import { deleteMessage } from 'xo'
|
||||
import { createPager } from 'selectors'
|
||||
import { createPager, createSelector } from 'selectors'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
ceil,
|
||||
isEmpty,
|
||||
map
|
||||
} from 'lodash'
|
||||
|
||||
const LOGS_PER_PAGE = 10
|
||||
|
||||
export default class TabLogs extends Component {
|
||||
constructor () {
|
||||
@@ -17,7 +21,12 @@ export default class TabLogs extends Component {
|
||||
this.getLogs = createPager(
|
||||
() => this.props.logs,
|
||||
() => this.state.page,
|
||||
10
|
||||
LOGS_PER_PAGE
|
||||
)
|
||||
|
||||
this.getNPages = createSelector(
|
||||
() => this.props.logs ? this.props.logs.length : 0,
|
||||
nLogs => ceil(nLogs / LOGS_PER_PAGE)
|
||||
)
|
||||
|
||||
this.state = {
|
||||
@@ -26,11 +35,12 @@ export default class TabLogs extends Component {
|
||||
}
|
||||
|
||||
_deleteAllLogs = () => map(this.props.logs, deleteMessage)
|
||||
_nextPage = () => this.setState({ page: this.state.page + 1 })
|
||||
_previousPage = () => this.setState({ page: this.state.page - 1 })
|
||||
_nextPage = () => this.setState({ page: Math.min(this.state.page + 1, this.getNPages()) })
|
||||
_previousPage = () => this.setState({ page: Math.max(this.state.page - 1, 1) })
|
||||
|
||||
render () {
|
||||
const logs = this.getLogs()
|
||||
const { page } = this.state
|
||||
|
||||
return <Container>
|
||||
{isEmpty(logs)
|
||||
@@ -43,15 +53,21 @@ export default class TabLogs extends Component {
|
||||
: <div>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
<Button size='large' onClick={this._previousPage}>
|
||||
<
|
||||
</Button>
|
||||
<Button size='large' onClick={this._nextPage}>
|
||||
>
|
||||
</Button>
|
||||
<TabButton
|
||||
btnStyle='secondary'
|
||||
disabled={page === 1}
|
||||
handler={this._previousPage}
|
||||
icon='previous'
|
||||
/>
|
||||
<TabButton
|
||||
btnStyle='secondary'
|
||||
disabled={page === this.getNPages()}
|
||||
handler={this._nextPage}
|
||||
icon='next'
|
||||
/>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={this._removeAllLogs}
|
||||
handler={this._removeAllLogs} // FIXME: define this method
|
||||
icon='delete'
|
||||
labelId='logRemoveAll'
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import ActionButton from 'action-button'
|
||||
import ChartistGraph from 'react-chartist'
|
||||
import Collapse from 'collapse'
|
||||
import Component from 'base-component'
|
||||
import defined from 'xo-defined'
|
||||
import differenceBy from 'lodash/differenceBy'
|
||||
import filter from 'lodash/filter'
|
||||
import forEach from 'lodash/forEach'
|
||||
@@ -36,7 +37,6 @@ import {
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
firstDefined,
|
||||
formatSize,
|
||||
resolveIds,
|
||||
resolveResourceSets
|
||||
@@ -312,15 +312,15 @@ export class Edit extends Component {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
_addIpPool = () => {
|
||||
const { ipPools, newIpPool, newIpPoolQuantity } = this.state
|
||||
_onChangeIpPool = newIpPool => {
|
||||
const { ipPools, newIpPoolQuantity } = this.state
|
||||
|
||||
this.setState({
|
||||
ipPools: [ ...ipPools, { id: newIpPool.id, quantity: newIpPoolQuantity } ],
|
||||
newIpPool: undefined,
|
||||
newIpPoolQuantity: ''
|
||||
})
|
||||
}
|
||||
|
||||
_removeIpPool = index => {
|
||||
const ipPools = [ ...this.state.ipPools ]
|
||||
remove(ipPools, (_, i) => index === i)
|
||||
@@ -444,33 +444,30 @@ export class Edit extends Component {
|
||||
<Row>
|
||||
<Col mediumSize={4}>
|
||||
<Row>
|
||||
<Col mediumSize={7}>
|
||||
<strong>{_('ipPool')}</strong>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<strong>{_('quantity')}</strong>
|
||||
</Col>
|
||||
<Col mediumSize={7}>
|
||||
<strong>{_('ipPool')}</strong>
|
||||
</Col>
|
||||
</Row>
|
||||
{map(state.ipPools, (ipPool, index) => <Row className='mb-1' key={index}>
|
||||
<Col mediumSize={3}>
|
||||
<input className='form-control' type='number' min={0} onChange={this.linkState(`ipPools.${index}.quantity`)} value={defined(ipPool.quantity, '')} placeholder='∞' />
|
||||
</Col>
|
||||
<Col mediumSize={7}>
|
||||
<SelectIpPool onChange={this.linkState(`ipPools.${index}.id`, 'id')} value={ipPool.id} />
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<input className='form-control' type='number' min={0} onChange={this.linkState(`ipPools.${index}.quantity`)} value={firstDefined(ipPool.quantity, '')} placeholder='∞' />
|
||||
</Col>
|
||||
<Col mediumSize={2}>
|
||||
<ActionButton icon='delete' handler={this._removeIpPool} handlerParam={index} />
|
||||
</Col>
|
||||
</Row>)}
|
||||
<Row>
|
||||
<Col mediumSize={7}>
|
||||
<SelectIpPool onChange={this.linkState('newIpPool')} value={state.newIpPool} predicate={this._getIpPoolPredicate()} />
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<input className='form-control' type='number' min={0} onChange={this.linkState('newIpPoolQuantity')} value={state.newIpPoolQuantity || ''} placeholder='∞' />
|
||||
</Col>
|
||||
<Col mediumSize={2}>
|
||||
<ActionButton icon='add' handler={this._addIpPool} />
|
||||
<Col mediumSize={7}>
|
||||
<SelectIpPool onChange={this._onChangeIpPool} value='' predicate={this._getIpPoolPredicate()} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
@@ -510,7 +507,7 @@ class ResourceSet extends Component {
|
||||
} = resourceSet
|
||||
|
||||
return [
|
||||
<li className='list-group-item'>
|
||||
<li key='subjects' className='list-group-item'>
|
||||
<Subjects subjects={subjects} />
|
||||
</li>,
|
||||
...map(objectsByType, (objectsSet, type) => (
|
||||
@@ -518,7 +515,7 @@ class ResourceSet extends Component {
|
||||
{map(objectsSet, object => renderXoItem(object, { className: 'mr-1' }))}
|
||||
</li>
|
||||
)),
|
||||
!isEmpty(ipPools) && <li className='list-group-item'>
|
||||
!isEmpty(ipPools) && <li key='ipPools' className='list-group-item'>
|
||||
{map(ipPools, pool => {
|
||||
const resolvedIpPool = resolvedIpPools[pool]
|
||||
const limits = get(resourceSet, `limits[ipPool:${pool}]`)
|
||||
@@ -534,7 +531,7 @@ class ResourceSet extends Component {
|
||||
}
|
||||
)}
|
||||
</li>,
|
||||
<li className='list-group-item'>
|
||||
<li key='graphs' className='list-group-item'>
|
||||
<Row>
|
||||
<Col mediumSize={4}>
|
||||
<Card>
|
||||
@@ -616,7 +613,7 @@ class ResourceSet extends Component {
|
||||
</Col>
|
||||
</Row>
|
||||
</li>,
|
||||
<li className='list-group-item text-xs-center'>
|
||||
<li key='actions' className='list-group-item text-xs-center'>
|
||||
<div className='btn-toolbar'>
|
||||
<ActionButton btnStyle='primary' icon='edit' handler={this.toggleState('editionMode')}>{_('editResourceSet')}</ActionButton>
|
||||
<ActionButton btnStyle='danger' icon='delete' handler={deleteResourceSet} handlerParam={resourceSet}>{_('deleteResourceSet')}</ActionButton>
|
||||
|
||||
@@ -86,7 +86,7 @@ const GROUP_COLUMNS = [
|
||||
},
|
||||
{
|
||||
name: _('addUserToGroupColumn'),
|
||||
itemRenderer: group => <SelectSubject predicate={getPredicate(group.users)} onChange={user => user && addUserToGroup(user, group)} defaultValue={null} />
|
||||
itemRenderer: group => <SelectSubject predicate={getPredicate(group.users)} onChange={user => user && addUserToGroup(user, group)} value={null} />
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
|
||||
@@ -85,9 +85,9 @@ class IpsCell extends BaseComponent {
|
||||
<Row>
|
||||
<Col mediumSize={6} offset={5}><strong>{_('ipsVifs')}</strong></Col>
|
||||
</Row>
|
||||
{ipPool.addresses && map(formatIps(keys(ipPool.addresses)), ip => {
|
||||
{ipPool.addresses && map(formatIps(keys(ipPool.addresses)), (ip, key) => {
|
||||
if (isObject(ip)) { // Range of IPs
|
||||
return <Row>
|
||||
return <Row key={key}>
|
||||
<Col mediumSize={5}>
|
||||
<strong>{ip.first} <Icon icon='arrow-right' /> {ip.last}</strong>
|
||||
</Col>
|
||||
@@ -109,7 +109,7 @@ class IpsCell extends BaseComponent {
|
||||
? map(addressVifs, (vifId, index) => {
|
||||
const vif = vifs[vifId] && vifs[vifId][0]
|
||||
const network = vif && networks[vif.$network] && networks[vif.$network][0]
|
||||
return <span className='mr-1'>
|
||||
return <span key={index} className='mr-1'>
|
||||
{network && vif
|
||||
? `${network.name_label} #${vif.device}`
|
||||
: <em>{_('ipPoolUnknownVif')}</em>
|
||||
@@ -188,7 +188,7 @@ class NetworksCell extends BaseComponent {
|
||||
const { newNetworks, showNewNetworkForm } = this.state
|
||||
|
||||
return <Container>
|
||||
{map(ipPool.networks, networkId => <Row>
|
||||
{map(ipPool.networks, networkId => <Row key={networkId}>
|
||||
<Col mediumSize={11}>
|
||||
{renderXoItemFromId(networkId)}
|
||||
</Col>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import React from 'react'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import { find, map } from 'lodash'
|
||||
|
||||
import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import BaseComponent from 'base-component'
|
||||
import ButtonGroup from 'button-group'
|
||||
import Copiable from 'copiable'
|
||||
import find from 'lodash/find'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import NoObjects from 'no-objects'
|
||||
import SortedTable from 'sorted-table'
|
||||
import styles from './index.css'
|
||||
import TabButton from 'tab-button'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import { alert, confirm } from 'modal'
|
||||
import { createSelector } from 'selectors'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import { subscribeApiLogs, subscribeUsers, deleteApiLog } from 'xo'
|
||||
|
||||
const CAN_REPORT_BUG = process.env.XOA_PLAN > 1
|
||||
@@ -99,7 +99,7 @@ export default class Logs extends BaseComponent {
|
||||
|
||||
_getLogs = createSelector(
|
||||
() => this.props.logs,
|
||||
logs => map(logs, (log, id) => ({ ...log, id }))
|
||||
logs => logs && map(logs, (log, id) => ({ ...log, id }))
|
||||
)
|
||||
|
||||
_showError = log => alert(
|
||||
@@ -115,29 +115,28 @@ export default class Logs extends BaseComponent {
|
||||
(users, showError) => ({ users, showError })
|
||||
)
|
||||
|
||||
_getPredicate = logs => logs != null
|
||||
|
||||
render () {
|
||||
const logs = this._getLogs()
|
||||
|
||||
return <div>
|
||||
{isEmpty(logs)
|
||||
? <p>{_('noLogs')}</p>
|
||||
: <div>
|
||||
<span className='pull-right'>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={this._deleteAllLogs}
|
||||
icon='delete'
|
||||
labelId='logDeleteAll'
|
||||
/>
|
||||
</span>
|
||||
{' '}
|
||||
<SortedTable
|
||||
collection={logs}
|
||||
columns={COLUMNS}
|
||||
userData={this._getData()}
|
||||
return <NoObjects collection={logs} message={_('noLogs')} predicate={this._getPredicate}>
|
||||
<div>
|
||||
<span className='pull-right'>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={this._deleteAllLogs}
|
||||
icon='delete'
|
||||
labelId='logDeleteAll'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</span>
|
||||
{' '}
|
||||
<SortedTable
|
||||
collection={logs}
|
||||
columns={COLUMNS}
|
||||
userData={this._getData()}
|
||||
/>
|
||||
</div>
|
||||
</NoObjects>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ class Plugin extends Component {
|
||||
const { editedConfig, expanded } = state
|
||||
const {
|
||||
configurationPresets,
|
||||
configurationSchema,
|
||||
loaded
|
||||
} = props
|
||||
|
||||
@@ -148,15 +149,13 @@ class Plugin extends Component {
|
||||
</div>
|
||||
</h5>
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<div className='form-group pull-right small'>
|
||||
<Button btnStyle='primary' onClick={this._updateExpanded}>
|
||||
<Icon icon={expanded ? 'minus' : 'plus'} />
|
||||
</Button>
|
||||
</div>
|
||||
</Col>
|
||||
{configurationSchema !== undefined && <Col className='text-xs-right' mediumSize={4}>
|
||||
<Button btnStyle='primary' onClick={this._updateExpanded}>
|
||||
<Icon icon={expanded ? 'minus' : 'plus'} />
|
||||
</Button>
|
||||
</Col>}
|
||||
</Row>
|
||||
{expanded && props.configurationSchema &&
|
||||
{expanded &&
|
||||
<form id={this.configFormId} onReset={this._stopEditing}>
|
||||
{size(configurationPresets) > 0 && (
|
||||
<div>
|
||||
@@ -188,25 +187,39 @@ class Plugin extends Component {
|
||||
<GenericInput
|
||||
label='Configuration'
|
||||
required
|
||||
schema={props.configurationSchema}
|
||||
schema={configurationSchema}
|
||||
uiSchema={this._getUiSchema()}
|
||||
onChange={this.linkState('editedConfig')}
|
||||
value={editedConfig || this.props.configuration}
|
||||
value={editedConfig || props.configuration}
|
||||
/>
|
||||
<div className='form-group pull-right'>
|
||||
<div className='btn-toolbar'>
|
||||
<div className='btn-group'>
|
||||
<ActionButton btnStyle='danger' disabled={!props.configuration} icon='delete' handler={this._deleteConfiguration}>
|
||||
<ActionButton
|
||||
btnStyle='danger'
|
||||
disabled={!props.configuration}
|
||||
handler={this._deleteConfiguration}
|
||||
icon='delete'
|
||||
>
|
||||
{_('deletePluginConfiguration')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className='btn-group'>
|
||||
<Button disabled={!editedConfig} type='reset'>
|
||||
<Button
|
||||
disabled={!editedConfig}
|
||||
type='reset'
|
||||
>
|
||||
{_('cancelPluginEdition')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className='btn-group'>
|
||||
<ActionButton disabled={!editedConfig} form={this.configFormId} icon='save' className='btn-primary' handler={this._saveConfiguration}>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={!editedConfig}
|
||||
form={this.configFormId}
|
||||
handler={this._saveConfiguration}
|
||||
icon='save'
|
||||
>
|
||||
{_('savePluginConfiguration')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
@@ -347,6 +347,7 @@ export default class Remotes extends Component {
|
||||
>
|
||||
{map(remoteTypes, (label, key) => _({key}, label, message => <option value={key}>{message}</option>))}
|
||||
</select>
|
||||
{type === 'smb' && <em className='text-warning'>{_('remoteSmbWarningMessage')}</em>}
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<input type='text' ref='name' className='form-control' placeholder={this.props.intl.formatMessage(messages.remoteMyNamePlaceHolder)} required />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import StateButton from 'state-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addSubscriptions } from 'utils'
|
||||
@@ -12,7 +12,7 @@ import { Container } from 'grid'
|
||||
import { Password as EditablePassword, Text } from 'editable'
|
||||
import { Password, Toggle } from 'form'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { map, noop } from 'lodash'
|
||||
import { noop } from 'lodash'
|
||||
import {
|
||||
addServer,
|
||||
editServer,
|
||||
@@ -22,10 +22,152 @@ import {
|
||||
subscribeServers
|
||||
} from 'xo'
|
||||
|
||||
const showInfo = () => alert(
|
||||
_('serverAllowUnauthorizedCertificates'),
|
||||
_('serverUnauthorizedCertificatesInfo')
|
||||
)
|
||||
const showServerError = server => {
|
||||
const { code, message } = server.error
|
||||
|
||||
if (code === 'DEPTH_ZERO_SELF_SIGNED_CERT') {
|
||||
return confirm({
|
||||
title: _('serverSelfSignedCertError'),
|
||||
body: _('serverSelfSignedCertQuestion')
|
||||
}).then(
|
||||
() => editServer(server, { allowUnauthorized: true }).then(
|
||||
() => connectServer(server)
|
||||
),
|
||||
noop
|
||||
)
|
||||
}
|
||||
|
||||
if (code === 'SESSION_AUTHENTICATION_FAILED') {
|
||||
return alert(_('serverAuthFailed'), message)
|
||||
}
|
||||
|
||||
return alert(code || _('serverUnknownError'), message)
|
||||
}
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
itemRenderer: (server, formatMessage) =>
|
||||
<Text
|
||||
value={server.label || ''}
|
||||
onChange={label => editServer(server, { label })}
|
||||
placeholder={formatMessage(messages.serverPlaceHolderLabel)}
|
||||
/>,
|
||||
default: true,
|
||||
name: _('serverLabel'),
|
||||
sortCriteria: _ => _.name_label
|
||||
},
|
||||
{
|
||||
itemRenderer: (server, formatMessage) =>
|
||||
<Text
|
||||
value={server.host}
|
||||
onChange={host => editServer(server, { host })}
|
||||
placeholder={formatMessage(messages.serverPlaceHolderAddress)}
|
||||
/>,
|
||||
name: _('serverHost'),
|
||||
sortCriteria: _ => _.host
|
||||
},
|
||||
{
|
||||
itemRenderer: (server, formatMessage) =>
|
||||
<Text
|
||||
value={server.username}
|
||||
onChange={username => editServer(server, { username })}
|
||||
placeholder={formatMessage(messages.serverPlaceHolderUser)}
|
||||
/>,
|
||||
name: _('serverUsername'),
|
||||
sortCriteria: _ => _.username
|
||||
},
|
||||
{
|
||||
itemRenderer: (server, formatMessage) =>
|
||||
<EditablePassword
|
||||
value=''
|
||||
onChange={password => editServer(server, { password })}
|
||||
placeholder={formatMessage(messages.serverPlaceHolderPassword)}
|
||||
/>,
|
||||
name: _('serverPassword')
|
||||
},
|
||||
{
|
||||
itemRenderer: server =>
|
||||
<div>
|
||||
<StateButton
|
||||
disabledLabel={_('serverDisconnected')}
|
||||
disabledHandler={connectServer}
|
||||
disabledTooltip={_('serverConnect')}
|
||||
|
||||
enabledLabel={_('serverConnected')}
|
||||
enabledHandler={disconnectServer}
|
||||
enabledTooltip={_('serverDisconnect')}
|
||||
|
||||
handlerParam={server}
|
||||
pending={server.status === 'connecting'}
|
||||
state={server.status === 'connected'}
|
||||
/>
|
||||
{' '}
|
||||
{server.error &&
|
||||
<Tooltip content={_('serverConnectionFailed')}>
|
||||
<a
|
||||
className='text-danger btn btn-link btn-sm'
|
||||
onClick={() => showServerError(server)}
|
||||
>
|
||||
<Icon
|
||||
icon='alarm'
|
||||
size='lg'
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>,
|
||||
name: _('serverStatus'),
|
||||
sortCriteria: _ => _.status
|
||||
},
|
||||
{
|
||||
itemRenderer: server =>
|
||||
<Toggle
|
||||
onChange={readOnly => editServer(server, { readOnly })}
|
||||
value={!!server.readOnly}
|
||||
/>,
|
||||
name: _('serverReadOnly'),
|
||||
sortCriteria: _ => !!_.readOnly
|
||||
},
|
||||
{
|
||||
itemRenderer: server =>
|
||||
<Toggle
|
||||
value={server.allowUnauthorized}
|
||||
onChange={allowUnauthorized => editServer(server, { allowUnauthorized })}
|
||||
/>,
|
||||
name: <span>
|
||||
{_('serverUnauthorizedCertificates')}
|
||||
{' '}
|
||||
<Tooltip content={_('serverAllowUnauthorizedCertificates')}>
|
||||
<a
|
||||
className='text-info'
|
||||
onClick={showInfo}
|
||||
>
|
||||
<Icon
|
||||
icon='info'
|
||||
size='lg'
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>,
|
||||
sortCriteria: _ => !!_.allowUnauthorized
|
||||
}
|
||||
]
|
||||
const INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
handler: removeServer,
|
||||
icon: 'delete',
|
||||
label: _('remove'),
|
||||
level: 'danger'
|
||||
}
|
||||
]
|
||||
|
||||
@addSubscriptions({
|
||||
servers: subscribeServers
|
||||
})
|
||||
|
||||
@injectIntl
|
||||
export default class Servers extends Component {
|
||||
_addServer = async () => {
|
||||
@@ -36,33 +178,6 @@ export default class Servers extends Component {
|
||||
this.setState({ label: '', host: '', password: '', username: '' })
|
||||
}
|
||||
|
||||
_showError = server => {
|
||||
const { code, message } = server.error
|
||||
|
||||
if (code === 'DEPTH_ZERO_SELF_SIGNED_CERT') {
|
||||
return confirm({
|
||||
title: _('serverSelfSignedCertError'),
|
||||
body: _('serverSelfSignedCertQuestion')
|
||||
}).then(
|
||||
() => editServer(server, { allowUnauthorized: true }).then(
|
||||
() => connectServer(server)
|
||||
),
|
||||
noop
|
||||
)
|
||||
}
|
||||
|
||||
if (code === 'SESSION_AUTHENTICATION_FAILED') {
|
||||
return alert(_('serverAuthFailed'), message)
|
||||
}
|
||||
|
||||
return alert(code || _('serverUnknownError'), message)
|
||||
}
|
||||
|
||||
_showInfo = () => alert(
|
||||
_('serverAllowUnauthorizedCertificates'),
|
||||
_('serverUnauthorizedCertificatesInfo')
|
||||
)
|
||||
|
||||
render () {
|
||||
const {
|
||||
props: {
|
||||
@@ -73,112 +188,12 @@ export default class Servers extends Component {
|
||||
} = this
|
||||
|
||||
return <Container>
|
||||
<table className='table table-striped'>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{_('serverLabel')}</td>
|
||||
<td>{_('serverHost')}</td>
|
||||
<td>{_('serverUsername')}</td>
|
||||
<td>{_('serverPassword')}</td>
|
||||
<td>{_('serverStatus')}</td>
|
||||
<td>{_('serverReadOnly')}</td>
|
||||
<td>
|
||||
{_('serverUnauthorizedCertificates')}
|
||||
{' '}
|
||||
<Tooltip content={_('serverAllowUnauthorizedCertificates')}>
|
||||
<a
|
||||
className='text-info'
|
||||
onClick={this._showInfo}
|
||||
>
|
||||
<Icon
|
||||
icon='info'
|
||||
size='lg'
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td className='text-xs-right'>{_('serverAction')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(servers, server => (
|
||||
<tr key={server.id}>
|
||||
<td>
|
||||
<Text
|
||||
value={server.label || ''}
|
||||
onChange={label => editServer(server, { label })}
|
||||
placeholder={formatMessage(messages.serverPlaceHolderLabel)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Text
|
||||
value={server.host}
|
||||
onChange={host => editServer(server, { host })}
|
||||
placeholder={formatMessage(messages.serverPlaceHolderAddress)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Text
|
||||
value={server.username}
|
||||
onChange={username => editServer(server, { username })}
|
||||
placeholder={formatMessage(messages.serverPlaceHolderUser)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<EditablePassword
|
||||
value=''
|
||||
onChange={password => editServer(server, { password })}
|
||||
placeholder={formatMessage(messages.serverPlaceHolderPassword)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<StateButton
|
||||
disabledLabel={_('serverDisconnected')}
|
||||
disabledHandler={connectServer}
|
||||
disabledTooltip={_('serverConnect')}
|
||||
|
||||
enabledLabel={_('serverConnected')}
|
||||
enabledHandler={disconnectServer}
|
||||
enabledTooltip={_('serverDisconnect')}
|
||||
|
||||
handlerParam={server}
|
||||
pending={server.status === 'connecting'}
|
||||
state={server.status === 'connected'}
|
||||
/>
|
||||
{' '}
|
||||
{server.error &&
|
||||
<Tooltip content={_('serverConnectionFailed')}>
|
||||
<a
|
||||
className='text-danger btn btn-link btn-sm'
|
||||
onClick={() => this._showError(server)}
|
||||
>
|
||||
<Icon
|
||||
icon='alarm'
|
||||
size='lg'
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
}
|
||||
</td>
|
||||
<td><Toggle value={!!server.readOnly} onChange={readOnly => editServer(server, { readOnly })} /></td>
|
||||
<td>
|
||||
<Toggle
|
||||
value={server.allowUnauthorized}
|
||||
onChange={allowUnauthorized => editServer(server, { allowUnauthorized })}
|
||||
/>
|
||||
</td>
|
||||
<td className='text-xs-right'>
|
||||
<ActionRowButton
|
||||
btnStyle='danger'
|
||||
handler={removeServer}
|
||||
handlerParam={server}
|
||||
icon='delete'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<SortedTable
|
||||
collection={servers}
|
||||
columns={COLUMNS}
|
||||
individualActions={INDIVIDUAL_ACTIONS}
|
||||
userData={formatMessage}
|
||||
/>
|
||||
<form
|
||||
className='form-inline'
|
||||
id='form-add-server'
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import ActionBar from 'action-bar'
|
||||
import _ from 'intl'
|
||||
import ActionBar, { Action } from 'action-bar'
|
||||
import React from 'react'
|
||||
import { forgetSr, rescanSr, reconnectAllHostsSr, disconnectAllHostsSr } from 'xo'
|
||||
|
||||
const SrActionBar = ({ sr }) => (
|
||||
<ActionBar
|
||||
actions={[
|
||||
{
|
||||
icon: 'refresh',
|
||||
label: 'srRescan',
|
||||
handler: rescanSr
|
||||
},
|
||||
{
|
||||
icon: 'sr-reconnect-all',
|
||||
label: 'srReconnectAll',
|
||||
handler: reconnectAllHostsSr
|
||||
},
|
||||
{
|
||||
icon: 'sr-disconnect-all',
|
||||
label: 'srDisconnectAll',
|
||||
handler: disconnectAllHostsSr
|
||||
},
|
||||
{
|
||||
icon: 'sr-forget',
|
||||
label: 'srForget',
|
||||
handler: forgetSr
|
||||
}
|
||||
]}
|
||||
display='icon'
|
||||
param={sr}
|
||||
/>
|
||||
handlerParam={sr}
|
||||
>
|
||||
<Action
|
||||
handler={rescanSr}
|
||||
label={_('srRescan')}
|
||||
icon='refresh'
|
||||
/>
|
||||
<Action
|
||||
handler={reconnectAllHostsSr}
|
||||
label={_('srReconnectAll')}
|
||||
icon='sr-reconnect-all'
|
||||
/>
|
||||
<Action
|
||||
handler={disconnectAllHostsSr}
|
||||
label={_('srDisconnectAll')}
|
||||
icon='sr-disconnect-all'
|
||||
/>
|
||||
<Action
|
||||
handler={forgetSr}
|
||||
label={_('srForget')}
|
||||
icon='sr-forget'
|
||||
/>
|
||||
</ActionBar>
|
||||
)
|
||||
export default SrActionBar
|
||||
|
||||
55
src/xo-app/sr/add-subvolume-modal.js
Normal file
55
src/xo-app/sr/add-subvolume-modal.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import { SelectSr } from 'select-objects'
|
||||
import { SizeInput } from 'form'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createSelector } from 'selectors'
|
||||
import {
|
||||
map,
|
||||
min
|
||||
} from 'lodash'
|
||||
|
||||
export default class AddSubvolumeModalBody extends Component {
|
||||
get value () {
|
||||
return this.state
|
||||
}
|
||||
|
||||
_getSrPredicate = createSelector(
|
||||
() => this.props.sr.$pool,
|
||||
poolId => sr => sr.SR_type === 'lvm' && sr.$pool === poolId
|
||||
)
|
||||
|
||||
_selectSrs = srs => {
|
||||
this.setState({
|
||||
srs,
|
||||
brickSize: min(map(srs, sr => sr.size - sr.physical_usage))
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Container>
|
||||
<Row className='mb-1'>
|
||||
<Col size={6}>{_('xosanSelectNSrs', { nSrs: this.props.subvolumeSize })}</Col>
|
||||
<Col size={6}>
|
||||
<SelectSr
|
||||
multi
|
||||
onChange={this._selectSrs}
|
||||
predicate={this._getSrPredicate()}
|
||||
value={this.state.srs}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='mb-1'>
|
||||
<Col size={6}>{_('xosanSize')}</Col>
|
||||
<Col size={6}>
|
||||
<SizeInput
|
||||
onChange={this.linkState('brickSize')}
|
||||
required
|
||||
value={this.state.brickSize}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,19 @@
|
||||
import _ from 'intl'
|
||||
import assign from 'lodash/assign'
|
||||
import Component from 'base-component'
|
||||
import find from 'lodash/find'
|
||||
import flatten from 'lodash/flatten'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import Page from '../page'
|
||||
import pick from 'lodash/pick'
|
||||
import React, { cloneElement } from 'react'
|
||||
import SrActionBar from './action-bar'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { editSr } from 'xo'
|
||||
import { NavLink, NavTabs } from 'nav'
|
||||
import { Text } from 'editable'
|
||||
import {
|
||||
assign,
|
||||
map,
|
||||
pick
|
||||
} from 'lodash'
|
||||
import {
|
||||
connectStore,
|
||||
routes
|
||||
@@ -62,73 +60,25 @@ import TabXosan from './tab-xosan'
|
||||
)
|
||||
)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const getVdis = createGetObjectsOfType('VDI').pick(
|
||||
createSelector(getSr, sr => sr.VDIs)
|
||||
).sort()
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
const getLogs = createGetObjectMessages(getSr)
|
||||
|
||||
const getVbdsByVdi = createGetObjectsOfType('VBD').pick(
|
||||
createSelector(
|
||||
getVdis,
|
||||
vdis => flatten(map(vdis, vdi => vdi.$VBDs))
|
||||
)
|
||||
).groupBy('VDI')
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
const getVdiIds = (state, props) => getSr(state, props).VDIs
|
||||
|
||||
const getVdisUnmanaged = createGetObjectsOfType('VDI-unmanaged').pick(
|
||||
createSelector(getSr, sr => sr.VDIs)
|
||||
const getVdis = createGetObjectsOfType('VDI').pick(
|
||||
getVdiIds
|
||||
).sort()
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const getVdiSnapshots = createGetObjectsOfType('VDI-snapshot').pick(
|
||||
getVdiIds
|
||||
).sort()
|
||||
const getUnmanagedVdis = createGetObjectsOfType('VDI-unmanaged').pick(
|
||||
createSelector(getSr, sr => sr.VDIs)
|
||||
).sort()
|
||||
|
||||
const getVdiSnapshotToVdi = createSelector(
|
||||
getVdis,
|
||||
getVdiSnapshots,
|
||||
(vdis, vdiSnapshots) => {
|
||||
const vdiSnapshotToVdi = {}
|
||||
forEach(vdiSnapshots, vdiSnapshot => {
|
||||
vdiSnapshotToVdi[vdiSnapshot.id] = vdiSnapshot.$snapshot_of && find(vdis, vdi => vdi.id === vdiSnapshot.$snapshot_of)
|
||||
})
|
||||
|
||||
return vdiSnapshotToVdi
|
||||
}
|
||||
)
|
||||
|
||||
const getVbdsByVdiSnapshot = createSelector(
|
||||
getVbdsByVdi,
|
||||
getVdiSnapshots,
|
||||
getVdiSnapshotToVdi,
|
||||
(vbdsByVdi, vdiSnapshots, vdiSnapshotToVdi) => {
|
||||
const vbdsByVdiSnapshot = {}
|
||||
forEach(vdiSnapshots, vdiSnapshot => {
|
||||
const vdi = vdiSnapshotToVdi[vdiSnapshot.id]
|
||||
vbdsByVdiSnapshot[vdiSnapshot.id] = vdi && vbdsByVdi[vdi.id]
|
||||
})
|
||||
|
||||
return vbdsByVdiSnapshot
|
||||
}
|
||||
)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const getVdisToVmIds = createSelector(
|
||||
getVbdsByVdi,
|
||||
getVbdsByVdiSnapshot,
|
||||
(vbdsByVdi, vbdsByVdiSnapshot) => mapValues({ ...vbdsByVdi, ...vbdsByVdiSnapshot }, vbds => {
|
||||
const vbd = find(vbds, 'VM')
|
||||
if (vbd) {
|
||||
return vbd.VM
|
||||
}
|
||||
})
|
||||
)
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
return (state, props) => {
|
||||
const sr = getSr(state, props)
|
||||
@@ -142,9 +92,8 @@ import TabXosan from './tab-xosan'
|
||||
pbds: getPbds(state, props),
|
||||
logs: getLogs(state, props),
|
||||
vdis: getVdis(state, props),
|
||||
vdisUnmanaged: getVdisUnmanaged(state, props),
|
||||
unmanagedVdis: getUnmanagedVdis(state, props),
|
||||
vdiSnapshots: getVdiSnapshots(state, props),
|
||||
vdisToVmIds: getVdisToVmIds(state, props),
|
||||
sr
|
||||
}
|
||||
}
|
||||
@@ -194,7 +143,6 @@ export default class Sr extends Component {
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
<Col>
|
||||
<NavTabs>
|
||||
@@ -223,9 +171,8 @@ export default class Sr extends Component {
|
||||
'pbds',
|
||||
'sr',
|
||||
'vdis',
|
||||
'vdisUnmanaged',
|
||||
'vdiSnapshots',
|
||||
'vdisToVmIds'
|
||||
'unmanagedVdis',
|
||||
'vdiSnapshots'
|
||||
]))
|
||||
return <Page header={this.header()} title={`${sr.name_label}${container ? ` (${container.name_label})` : ''}`}>
|
||||
{cloneElement(this.props.children, childProps)}
|
||||
|
||||
63
src/xo-app/sr/replace-brick-modal.js
Normal file
63
src/xo-app/sr/replace-brick-modal.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import { SelectSr } from 'select-objects'
|
||||
import { Toggle, SizeInput } from 'form'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createSelector } from 'selectors'
|
||||
|
||||
export default class ReplaceBrickModalBody extends Component {
|
||||
get value () {
|
||||
return this.state
|
||||
}
|
||||
|
||||
_getSrPredicate = createSelector(
|
||||
() => this.props.vm,
|
||||
() => this.state.onSameVm,
|
||||
(vm, onSameVm) => onSameVm
|
||||
? sr => sr.$container === vm.$container && sr.SR_type === 'lvm'
|
||||
: sr => sr.$pool === vm.$pool && sr.SR_type === 'lvm'
|
||||
)
|
||||
|
||||
_toggleOnSameVm = () => this.setState({
|
||||
onSameVm: !this.state.onSameVm,
|
||||
sr: undefined
|
||||
})
|
||||
|
||||
_selectSr = sr => {
|
||||
this.setState({
|
||||
sr,
|
||||
brickSize: sr.size - sr.physical_usage
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Container>
|
||||
<Row className='mb-1'>
|
||||
<Col size={6}><strong>{_('xosanOnSameVm')}</strong></Col>
|
||||
<Col size={6}>
|
||||
<Toggle onChange={this._toggleOnSameVm} value={this.state.onSameVm} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='mb-1'>
|
||||
<Col size={6}><strong>{_('xosanUnderlyingStorage')}</strong></Col>
|
||||
<Col size={6}>
|
||||
<SelectSr
|
||||
onChange={this._selectSr}
|
||||
predicate={this._getSrPredicate()}
|
||||
value={this.state.sr}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='mb-1'>
|
||||
<Col size={6}><strong>{_('xosanBrickSize')}</strong></Col>
|
||||
<Col size={6}>
|
||||
<SizeInput
|
||||
onChange={this.linkState('brickSize')}
|
||||
value={this.state.brickSize}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,58 @@
|
||||
import _ from 'intl'
|
||||
import Copiable from 'copiable'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import { deleteSr } from 'xo'
|
||||
import { addSubscriptions, connectStore, formatSize } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { createSelector } from 'reselect'
|
||||
import { createSrUnhealthyVdiChainsLengthSubscription, deleteSr } from 'xo'
|
||||
import { flowRight, isEmpty, keys, sum, values } from 'lodash'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: _('srUnhealthyVdiNameLabel'),
|
||||
itemRenderer: vdi => <span>{vdi.name_label}</span>,
|
||||
sortCriteria: vdi => vdi.name_label
|
||||
},
|
||||
{
|
||||
name: _('srUnhealthyVdiSize'),
|
||||
itemRenderer: vdi => formatSize(vdi.size),
|
||||
sortCriteria: vdi => vdi.size
|
||||
},
|
||||
{
|
||||
name: _('srUnhealthyVdiDepth'),
|
||||
itemRenderer: (vdi, chains) => chains[vdi.uuid],
|
||||
sortCriteria: (vdi, chains) => chains[vdi.uuid]
|
||||
}
|
||||
]
|
||||
|
||||
const UnhealthyVdiChains = flowRight(
|
||||
addSubscriptions(props => ({
|
||||
chains: createSrUnhealthyVdiChainsLengthSubscription(props.sr)
|
||||
})),
|
||||
connectStore(() => ({
|
||||
vdis: createGetObjectsOfType('VDI').pick(
|
||||
createSelector(
|
||||
(_, props) => props.chains,
|
||||
keys
|
||||
)
|
||||
)
|
||||
}))
|
||||
)(({ chains, vdis }) => isEmpty(vdis)
|
||||
? null
|
||||
: <div>
|
||||
<h3>{_('srUnhealthyVdiTitle', { total: sum(values(chains)) })}</h3>
|
||||
<SortedTable
|
||||
collection={vdis}
|
||||
columns={COLUMNS}
|
||||
userData={chains}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default ({
|
||||
sr
|
||||
@@ -34,4 +83,9 @@ export default ({
|
||||
</table>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<UnhealthyVdiChains sr={sr} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import _ from 'intl'
|
||||
import ActionRow from 'action-row-button'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import Link from 'link'
|
||||
import React from 'react'
|
||||
import renderXoItem, { renderXoUnknownItem } from 'render-xo-item'
|
||||
import SortedTable from 'sorted-table'
|
||||
import { formatSize } from 'utils'
|
||||
import { concat, isEmpty } from 'lodash'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { deleteVdi, editVdi } from 'xo'
|
||||
import { renderXoItemFromId } from 'render-xo-item'
|
||||
import { createGetObject, createSelector } from 'selectors'
|
||||
import { deleteVdi, deleteVdis, editVdi } from 'xo'
|
||||
import { Text } from 'editable'
|
||||
|
||||
// ===================================================================
|
||||
@@ -37,20 +38,46 @@ const COLUMNS = [
|
||||
},
|
||||
{
|
||||
name: _('vdiVm'),
|
||||
itemRenderer: (vdi, vdisToVmIds) => {
|
||||
const id = vdisToVmIds[vdi.id]
|
||||
const Item = renderXoItemFromId(id)
|
||||
component: connectStore(() => {
|
||||
const getObject = createGetObject((_, id) => id)
|
||||
|
||||
if (id) {
|
||||
return (
|
||||
<Link to={`/vms/${id}${vdi.type === 'VDI-snapshot' ? '/snapshots' : ''}`}>
|
||||
{Item}
|
||||
</Link>
|
||||
)
|
||||
return {
|
||||
vm: (state, { item: { $VBDs: [ vbdId ] } }) => {
|
||||
if (vbdId === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
const vbd = getObject(state, vbdId)
|
||||
if (vbd != null) {
|
||||
return getObject(state, vbd.VM)
|
||||
}
|
||||
}
|
||||
}
|
||||
})(({ vm }) => {
|
||||
if (vm === null) {
|
||||
return null // no attached VM
|
||||
}
|
||||
|
||||
return Item
|
||||
}
|
||||
if (vm === undefined) {
|
||||
return renderXoUnknownItem()
|
||||
}
|
||||
|
||||
let link
|
||||
const { type } = vm
|
||||
if (type === 'VM') {
|
||||
link = `/vms/${vm.id}`
|
||||
} else if (type === 'VM-snapshot') {
|
||||
const id = vm.$snapshot_of
|
||||
link = id !== undefined
|
||||
? `/vms/${id}/snapshots`
|
||||
: '/dashboard/health'
|
||||
}
|
||||
|
||||
const item = renderXoItem(vm)
|
||||
return link === undefined
|
||||
? item
|
||||
: <Link to={link}>{item}</Link>
|
||||
})
|
||||
},
|
||||
{
|
||||
name: _('vdiTags'),
|
||||
@@ -60,38 +87,64 @@ const COLUMNS = [
|
||||
name: _('vdiSize'),
|
||||
itemRenderer: vdi => formatSize(vdi.size),
|
||||
sortCriteria: vdi => vdi.size
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
const GROUPED_ACTIONS = [
|
||||
{
|
||||
name: _('vdiAction'),
|
||||
itemRenderer: vdi => (
|
||||
<ActionRow
|
||||
btnStyle='danger'
|
||||
handler={deleteVdi}
|
||||
handlerParam={vdi}
|
||||
icon='delete'
|
||||
/>
|
||||
)
|
||||
handler: deleteVdis,
|
||||
icon: 'delete',
|
||||
label: _('deleteSelectedVdis')
|
||||
}
|
||||
]
|
||||
|
||||
const INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
handler: deleteVdi,
|
||||
icon: 'delete',
|
||||
label: _('deleteSelectedVdi'),
|
||||
level: 'danger'
|
||||
}
|
||||
]
|
||||
|
||||
const FILTERS = {
|
||||
filterNoSnapshots: 'type:!VDI-snapshot',
|
||||
filterOnlyBaseCopy: 'type:VDI-unmanaged',
|
||||
filterOnlyRegularDisks: 'type:!VDI-unmanaged type:!VDI-snapshot',
|
||||
filterOnlySnapshots: 'type:VDI-snapshot'
|
||||
filterOnlyManaged: 'type:!VDI-unmanaged',
|
||||
filterOnlyRegular: '!type:|(VDI-snapshot VDI-unmanaged)',
|
||||
filterOnlySnapshots: 'type:VDI-snapshot',
|
||||
filterOnlyOrphaned: 'type:!VDI-unmanaged $VBDs:!""',
|
||||
filterOnlyUnmanaged: 'type:VDI-unmanaged'
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default ({ vdis, vdisUnmanaged, vdiSnapshots, vdisToVmIds }) => (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
{!isEmpty(vdis)
|
||||
? <SortedTable collection={vdis.concat(vdiSnapshots, vdisUnmanaged)} userData={vdisToVmIds} columns={COLUMNS} filters={FILTERS} />
|
||||
: <h4 className='text-xs-center'>{_('srNoVdis')}</h4>
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
export default class SrDisks extends Component {
|
||||
_getAllVdis = createSelector(
|
||||
() => this.props.vdis,
|
||||
() => this.props.vdiSnapshots,
|
||||
() => this.props.unmanagedVdis,
|
||||
concat
|
||||
)
|
||||
|
||||
render () {
|
||||
const vdis = this._getAllVdis()
|
||||
return <Container>
|
||||
<Row>
|
||||
<Col>
|
||||
{!isEmpty(vdis)
|
||||
? <SortedTable
|
||||
collection={vdis}
|
||||
columns={COLUMNS}
|
||||
defaultFilter='filterOnlyManaged'
|
||||
filters={FILTERS}
|
||||
filterUrlParam='s'
|
||||
groupedActions={GROUPED_ACTIONS}
|
||||
individualActions={INDIVIDUAL_ACTIONS}
|
||||
shortcutsTarget='body'
|
||||
/>
|
||||
: <h4 className='text-xs-center'>{_('srNoVdis')}</h4>
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import _ from 'intl'
|
||||
import HomeTags from 'home-tags'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import HomeTags from 'home-tags'
|
||||
import { addTag, removeTag } from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { formatSize } from 'utils'
|
||||
import { renderXoItemFromId } from 'render-xo-item'
|
||||
import Usage, { UsageElement } from 'usage'
|
||||
import { addTag, removeTag } from 'xo'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetObject } from 'selectors'
|
||||
import { renderXoItemFromId } from 'render-xo-item'
|
||||
|
||||
const UsageTooltip = connectStore(() => ({
|
||||
vbd: createGetObject((_, { vdi }) => vdi.$VBDs[0])
|
||||
}))(({ vbd, vdi }) =>
|
||||
<span>
|
||||
{vdi.name_label} − {formatSize(vdi.usage)}
|
||||
{vbd != null && <br />}
|
||||
{vbd != null && renderXoItemFromId(vbd.VM)}
|
||||
</span>
|
||||
)
|
||||
|
||||
export default ({
|
||||
sr,
|
||||
vdis,
|
||||
vdisUnmanaged,
|
||||
vdisToVmIds
|
||||
vdiSnapshots,
|
||||
unmanagedVdis
|
||||
}) => <Container>
|
||||
<Row className='text-xs-center'>
|
||||
<Col mediumSize={4}>
|
||||
@@ -35,23 +46,21 @@ export default ({
|
||||
<Row>
|
||||
<Col smallOffset={1} mediumSize={10}>
|
||||
<Usage total={sr.size}>
|
||||
{map(vdisUnmanaged, vdi => <UsageElement
|
||||
{map(unmanagedVdis, vdi => <UsageElement
|
||||
highlight
|
||||
key={vdi.id}
|
||||
tooltip={<span>
|
||||
{vdi.name_label}
|
||||
<br />
|
||||
{vdisToVmIds[vdi.id] && renderXoItemFromId(vdisToVmIds[vdi.id])}
|
||||
</span>}
|
||||
tooltip={<UsageTooltip vdi={vdi} />}
|
||||
value={vdi.usage}
|
||||
/>)}
|
||||
{map(vdis, vdi => <UsageElement
|
||||
key={vdi.id}
|
||||
tooltip={<span>
|
||||
{vdi.name_label}
|
||||
<br />
|
||||
{vdisToVmIds[vdi.id] && renderXoItemFromId(vdisToVmIds[vdi.id])}
|
||||
</span>}
|
||||
tooltip={<UsageTooltip vdi={vdi} />}
|
||||
value={vdi.usage}
|
||||
/>)}
|
||||
{map(vdiSnapshots, vdi => <UsageElement
|
||||
highlight
|
||||
key={vdi.id}
|
||||
tooltip={<UsageTooltip vdi={vdi} />}
|
||||
value={vdi.usage}
|
||||
/>)}
|
||||
</Usage>
|
||||
|
||||
@@ -1,30 +1,585 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Collapse from 'collapse'
|
||||
import Copiable from 'copiable'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import React from 'react'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import Tooltip from 'tooltip'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import { confirm } from 'modal'
|
||||
import { error } from 'notification'
|
||||
import { Toggle } from 'form'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import {
|
||||
keys,
|
||||
map
|
||||
forEach,
|
||||
isEmpty,
|
||||
map,
|
||||
reduce,
|
||||
sum
|
||||
} from 'lodash'
|
||||
import {
|
||||
getVolumeInfo
|
||||
createGetObjectsOfType,
|
||||
createSelector
|
||||
} from 'selectors'
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
formatSize
|
||||
} from 'utils'
|
||||
import {
|
||||
addXosanBricks,
|
||||
fixHostNotInXosanNetwork,
|
||||
// TODO: uncomment when implementing subvolume deletion
|
||||
// removeXosanBricks,
|
||||
replaceXosanBrick,
|
||||
startVm,
|
||||
subscribeVolumeInfo
|
||||
} from 'xo'
|
||||
|
||||
export default class TabXosan extends Component {
|
||||
componentDidMount () {
|
||||
getVolumeInfo(this.props.sr.id).then(info => {
|
||||
this.setState({ volumeInfo: info })
|
||||
import { INFO_TYPES } from '../xosan'
|
||||
|
||||
import ReplaceBrickModalBody from './replace-brick-modal'
|
||||
import AddSubvolumeModalBody from './add-subvolume-modal'
|
||||
|
||||
const ISSUE_CODE_TO_MESSAGE = {
|
||||
VMS_DOWN: 'xosanVmsNotRunning',
|
||||
VMS_NOT_FOUND: 'xosanVmsNotFound',
|
||||
FILES_NEED_HEALING: 'xosanFilesNeedHealing',
|
||||
HOST_NOT_IN_NETWORK: 'xosanHostNotInNetwork'
|
||||
}
|
||||
|
||||
const BORDERS = {
|
||||
border: 'solid 2px #ccc',
|
||||
borderRadius: '5px',
|
||||
borderTop: 'none'
|
||||
}
|
||||
|
||||
const Issues = ({ issues }) => <Container>
|
||||
{map(issues, issue => <Row key={issue.key || issue.code} className='alert alert-danger mb-1' role='alert'>
|
||||
<Col>
|
||||
<Icon icon='error' /> <strong>{_(ISSUE_CODE_TO_MESSAGE[issue.code], issue.params)}</strong>
|
||||
{issue.fix && <Tooltip content={issue.fix.title}>
|
||||
<ActionButton
|
||||
btnStyle='danger'
|
||||
className='ml-1'
|
||||
handler={issue.fix.action}
|
||||
icon='fix'
|
||||
size='small'
|
||||
>
|
||||
{_('xosanFixIssue')}
|
||||
</ActionButton>
|
||||
</Tooltip>}
|
||||
</Col>
|
||||
</Row>)}
|
||||
</Container>
|
||||
|
||||
const Field = ({ title, children }) => <SingleLineRow>
|
||||
<Col size={3}><strong>{title}</strong></Col>
|
||||
<Col size={9}>{children}</Col>
|
||||
</SingleLineRow>
|
||||
|
||||
@connectStore({
|
||||
srs: createGetObjectsOfType('SR'),
|
||||
vms: createGetObjectsOfType('VM')
|
||||
})
|
||||
class Node extends Component {
|
||||
_replaceBrick = async ({ brick, vm }) => {
|
||||
const { sr, brickSize, onSameVm = false } = await confirm({
|
||||
icon: 'refresh',
|
||||
title: _('xosanReplace'),
|
||||
body: <ReplaceBrickModalBody vm={vm} />
|
||||
})
|
||||
|
||||
if (sr == null || brickSize == null) {
|
||||
return error(_('xosanReplaceBrickErrorTitle'), _('xosanReplaceBrickErrorMessage'))
|
||||
}
|
||||
|
||||
await replaceXosanBrick(this.props.sr, brick, sr, brickSize, onSameVm)
|
||||
}
|
||||
|
||||
_getSizeUsage = createSelector(
|
||||
() => this.props.node.statusDetail,
|
||||
statusDetail => ({
|
||||
used: String(Math.round(100 - (+statusDetail.sizeFree / +statusDetail.sizeTotal) * 100)),
|
||||
free: formatSize(+statusDetail.sizeFree)
|
||||
})
|
||||
)
|
||||
|
||||
_getInodesUsage = createSelector(
|
||||
() => this.props.node.statusDetail,
|
||||
statusDetail => ({
|
||||
used: String(Math.round(100 - (+statusDetail.inodesFree / +statusDetail.inodesTotal) * 100)),
|
||||
free: formatSize(+statusDetail.inodesFree)
|
||||
})
|
||||
)
|
||||
|
||||
render () {
|
||||
return <Container>
|
||||
{this.state.volumeInfo && map(keys(this.state.volumeInfo).sort(), key => key !== 'Bricks'
|
||||
? <Row key={key}>
|
||||
<Col size={3}><strong>{key}</strong></Col>
|
||||
<Col size={4}>{this.state.volumeInfo[key]}</Col>
|
||||
</Row>
|
||||
: null
|
||||
)}
|
||||
</Container >
|
||||
const { srs } = this.props
|
||||
const { showAdvanced } = this.state
|
||||
|
||||
const {
|
||||
config,
|
||||
heal,
|
||||
size,
|
||||
status,
|
||||
statusDetail,
|
||||
uuid,
|
||||
vm
|
||||
} = this.props.node
|
||||
|
||||
return <Collapse
|
||||
buttonText={<span>
|
||||
<Icon
|
||||
color={heal
|
||||
? heal.status === 'Connected'
|
||||
? 'text-success'
|
||||
: 'text-warning'
|
||||
: 'text-danger'
|
||||
}
|
||||
icon='disk'
|
||||
/> {srs[config.underlyingSr].name_label}
|
||||
</span>}
|
||||
className='mb-1'
|
||||
>
|
||||
<div style={BORDERS}>
|
||||
<Container className='p-1'>
|
||||
<Field title={_('xosanVm')}>
|
||||
{vm !== undefined
|
||||
? <span>
|
||||
<Tooltip content={_(`powerState${vm.power_state}`)}>
|
||||
<Icon icon={vm.power_state.toLowerCase()} />
|
||||
</Tooltip> <Link to={`/vms/${config.vm.id}`}>{vm.name_label}</Link>
|
||||
{(vm.power_state !== 'Running') &&
|
||||
<Tooltip content={_('xosanRun')}>
|
||||
<ActionButton
|
||||
handler={startVm}
|
||||
handlerParam={vm}
|
||||
icon='vm-start'
|
||||
size='small'
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
</span>
|
||||
: <span style={{color: 'red'}}>
|
||||
<Icon icon='alarm' /> {_('xosanCouldNotFindVm')}
|
||||
</span>
|
||||
}
|
||||
</Field>
|
||||
<Field title={_('xosanUnderlyingStorage')}>
|
||||
<Link to={`/srs/${config.underlyingSr}`}>{srs[config.underlyingSr].name_label}</Link>
|
||||
{' - '}
|
||||
{size != null && _('xosanUnderlyingStorageUsage', { usage: formatSize(size) })}
|
||||
</Field>
|
||||
<Field title={_('xosanStatus')}>
|
||||
{heal ? heal.status : 'unknown'}
|
||||
</Field>
|
||||
{statusDetail && <Field title={_('xosanUsedSpace')}>
|
||||
<span style={{ display: 'inline-block', width: '20em', height: '1em' }}>
|
||||
<Tooltip content={_('spaceLeftTooltip', this._getSizeUsage())}>
|
||||
<progress
|
||||
className='progress'
|
||||
max='100'
|
||||
value={100 - (+statusDetail.sizeFree / +statusDetail.sizeTotal) * 100}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</Field>}
|
||||
{config.arbiter === 'True' && <Field title={_('xosanArbiter')} />}
|
||||
<Row className='mt-1'>
|
||||
<Col>
|
||||
<ActionButton
|
||||
btnStyle='success'
|
||||
icon='refresh'
|
||||
handler={this._replaceBrick}
|
||||
handlerParam={{ brick: config.brickName, vm }}
|
||||
>
|
||||
{_('xosanReplace')}
|
||||
</ActionButton>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='mt-1'>
|
||||
<Col><h3><Toggle iconSize={1} onChange={this.toggleState('showAdvanced')} value={showAdvanced} /> {_('xosanAdvanced')}</h3></Col>
|
||||
</Row>
|
||||
{showAdvanced && [
|
||||
<Field title={_('xosanBrickName')}>
|
||||
<Copiable tagName='div'>{config.brickName}</Copiable>
|
||||
</Field>,
|
||||
<Field title={_('xosanBrickUuid')}>
|
||||
<Copiable tagName='div'>{uuid}</Copiable>
|
||||
</Field>,
|
||||
<div>
|
||||
{statusDetail && [
|
||||
<Field key='usedInodes' title={_('xosanUsedInodes')}>
|
||||
<span style={{ display: 'inline-block', width: '20em', height: '1em' }}>
|
||||
<Tooltip content={_('spaceLeftTooltip', this._getInodesUsage())}>
|
||||
<progress className='progress' max='100' value={100 - (+statusDetail.inodesFree / +statusDetail.inodesTotal) * 100}
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</Field>,
|
||||
<Field key='blockSize' title={_('xosanBlockSize')}>{statusDetail.blockSize}</Field>,
|
||||
<Field key='device' title={_('xosanDevice')}>{statusDetail.device}</Field>,
|
||||
<Field key='fsName' title={_('xosanFsName')}>{statusDetail.fsName}</Field>,
|
||||
<Field key='mountOptions' title={_('xosanMountOptions')}>{statusDetail.mntOptions}</Field>,
|
||||
<Field key='path' title={_('xosanPath')}>{statusDetail.path}</Field>
|
||||
]}
|
||||
</div>,
|
||||
<div>
|
||||
{status && status.length !== 0 && <Row className='mt-1'>
|
||||
<Col>
|
||||
<table className='table' style={{ maxWidth: '50em' }}>
|
||||
<thead>
|
||||
<th>{_('xosanJob')}</th>
|
||||
<th>{_('xosanPath')}</th>
|
||||
<th>{_('xosanStatus')}</th>
|
||||
<th>{_('xosanPid')}</th>
|
||||
<th>{_('xosanPort')}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(status, job => <tr key={job.pid}>
|
||||
<td>{job.hostname}</td>
|
||||
<td>{job.path}</td>
|
||||
<td>{job.status}</td>
|
||||
<td>{job.pid}</td>
|
||||
<td>{job.port}</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</Col>
|
||||
</Row>}
|
||||
</div>,
|
||||
<div>
|
||||
{heal && heal.file && heal.file.length !== 0 && <div>
|
||||
<h4>{_('xosanFilesNeedingHealing')}</h4>
|
||||
{map(heal.file, file => <Row key={file.gfid}>
|
||||
<Col size={5}>{file._}</Col >
|
||||
<Col size={4}>{file.gfid}</Col>
|
||||
</Row>)}
|
||||
</div>}
|
||||
</div>
|
||||
]}
|
||||
</Container>
|
||||
</div>
|
||||
</Collapse>
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@connectStore(() => ({
|
||||
vms: createGetObjectsOfType('VM'),
|
||||
hosts: createGetObjectsOfType('host'),
|
||||
vbds: createGetObjectsOfType('VBD'),
|
||||
vdis: createGetObjectsOfType('VDI')
|
||||
}))
|
||||
@addSubscriptions(({ sr }) => {
|
||||
const subscriptions = {}
|
||||
forEach(INFO_TYPES, infoType => {
|
||||
subscriptions[`${infoType}_`] = cb => subscribeVolumeInfo({ sr, infoType }, cb)
|
||||
})
|
||||
|
||||
return subscriptions
|
||||
})
|
||||
export default class TabXosan extends Component {
|
||||
_addSubvolume = async () => {
|
||||
const { srs, brickSize } = await confirm({
|
||||
icon: 'add',
|
||||
title: _('xosanAddSubvolume'),
|
||||
body: <AddSubvolumeModalBody sr={this.props.sr} subvolumeSize={this._getSubvolumeSize()} />
|
||||
})
|
||||
|
||||
if (brickSize == null || (srs && srs.length) !== this._getSubvolumeSize()) {
|
||||
return error(_('xosanAddSubvolumeErrorTitle'), _('xosanAddSubvolumeErrorMessage', { nSrs: this._getSubvolumeSize() }))
|
||||
}
|
||||
|
||||
return this._addBricks({ srs, brickSize })
|
||||
}
|
||||
|
||||
// TODO: uncomment when implementing subvolume deletion
|
||||
// async _removeSubVolume (bricks) {
|
||||
// await removeXosanBricks(this.props.sr.id, bricks)
|
||||
// }
|
||||
|
||||
async _addBricks ({srs, brickSize}) {
|
||||
await addXosanBricks(this.props.sr.id, srs.map(sr => sr.id), brickSize)
|
||||
}
|
||||
|
||||
_getStrippedVolumeInfo = createSelector(
|
||||
() => this.props.info_,
|
||||
info => info && info.commandStatus ? info.result : null
|
||||
)
|
||||
|
||||
_getSubvolumeSize = createSelector(
|
||||
this._getStrippedVolumeInfo,
|
||||
strippedVolumeInfo => strippedVolumeInfo
|
||||
? +strippedVolumeInfo.disperseCount || +strippedVolumeInfo.replicaCount
|
||||
: null
|
||||
)
|
||||
|
||||
// TODO: uncomment when implementing subvolume deletion
|
||||
// _getSubvolumes = createSelector(
|
||||
// this._getStrippedVolumeInfo,
|
||||
// this._getSubvolumeSize,
|
||||
// (strippedVolumeInfo, subvolumeSize) => {
|
||||
// const subVolumes = []
|
||||
// if (strippedVolumeInfo) {
|
||||
// for (let i = 0; i < strippedVolumeInfo.bricks.length; i += subvolumeSize) {
|
||||
// subVolumes.push(strippedVolumeInfo.bricks.slice(i, i + subvolumeSize))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return subVolumes
|
||||
// }
|
||||
// )
|
||||
|
||||
_getConfig = createSelector(
|
||||
() => this.props.sr && this.props.sr.other_config['xo:xosan_config'],
|
||||
otherConfig => otherConfig ? JSON.parse(otherConfig) : null
|
||||
)
|
||||
|
||||
_getBrickByName = createSelector(
|
||||
this._getConfig,
|
||||
() => this.props.vms,
|
||||
() => this.props.vdis,
|
||||
() => this.props.vbds,
|
||||
() => this.props.heal_,
|
||||
() => this.props.status_,
|
||||
() => this.props.statusDetail_,
|
||||
this._getStrippedVolumeInfo,
|
||||
(xosanConfig, vms, vdis, vbds, heal, status, statusDetail, strippedVolumeInfo) => {
|
||||
const nodes = xosanConfig && xosanConfig.nodes
|
||||
|
||||
const brickByName = {}
|
||||
forEach(nodes, node => {
|
||||
const vm = vms[node.vm.id]
|
||||
|
||||
brickByName[node.brickName] = {
|
||||
config: node,
|
||||
uuid: '-',
|
||||
size: isEmpty(vm && vm.$VBDs)
|
||||
? null
|
||||
: sum(map(vm.$VBDs, vbdId => {
|
||||
const vdi = vdis[vbds[vbdId].VDI]
|
||||
return vdi === undefined ? 0 : vdi.size
|
||||
})),
|
||||
vm
|
||||
}
|
||||
})
|
||||
|
||||
const brickByUuid = {}
|
||||
if (strippedVolumeInfo) {
|
||||
forEach(strippedVolumeInfo.bricks, brick => {
|
||||
brickByName[brick.name] = brickByName[brick.name] || {}
|
||||
brickByName[brick.name].info = brick
|
||||
brickByName[brick.name].uuid = brick.hostUuid
|
||||
brickByUuid[brick.hostUuid] = brickByUuid[brick.hostUuid] || brickByName[brick.name]
|
||||
})
|
||||
}
|
||||
|
||||
if (heal && heal.commandStatus) {
|
||||
forEach(heal.result.bricks, brick => {
|
||||
brickByName[brick.name] = brickByName[brick.name] || {}
|
||||
brickByName[brick.name].heal = brick
|
||||
brickByName[brick.name].uuid = brick.hostUuid
|
||||
brickByUuid[brick.hostUuid] = brickByUuid[brick.hostUuid] || brickByName[brick.name]
|
||||
})
|
||||
}
|
||||
|
||||
if (status && status.commandStatus) {
|
||||
forEach(brickByUuid, (brick, uuid) => {
|
||||
brick.status = status.result.nodes[uuid]
|
||||
})
|
||||
}
|
||||
|
||||
if (statusDetail && statusDetail.commandStatus) {
|
||||
forEach(brickByUuid, (brick, uuid) => {
|
||||
if (uuid in statusDetail.result.nodes) {
|
||||
brick.statusDetail = statusDetail.result.nodes[uuid][0]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return brickByName
|
||||
}
|
||||
)
|
||||
|
||||
_getOrderedBrickList = createSelector(
|
||||
this._getConfig,
|
||||
this._getBrickByName,
|
||||
(xosanConfig, brickByName) => {
|
||||
if (!xosanConfig || !xosanConfig.nodes) {
|
||||
return
|
||||
}
|
||||
|
||||
return map(xosanConfig.nodes, node => brickByName[node.brickName])
|
||||
}
|
||||
)
|
||||
|
||||
_getIssues = createSelector(
|
||||
this._getOrderedBrickList,
|
||||
() => this.props.hosts_,
|
||||
() => this.props.hosts,
|
||||
() => this.props.sr,
|
||||
(orderedBrickList, hosts_, hosts, sr) => {
|
||||
if (orderedBrickList == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const issues = []
|
||||
if (reduce(orderedBrickList,
|
||||
(hasStopped, node) => hasStopped || (node.vm && node.vm.power_state !== 'Running'),
|
||||
false
|
||||
)) { issues.push({ code: 'VMS_DOWN' }) }
|
||||
|
||||
if (reduce(orderedBrickList,
|
||||
(hasNotFound, node) => hasNotFound || node.vm === undefined,
|
||||
false
|
||||
)) { issues.push({ code: 'VMS_NOT_FOUND' }) }
|
||||
|
||||
if (reduce(orderedBrickList,
|
||||
(hasFileToHeal, node) => hasFileToHeal || (node.heal && node.heal.file && node.heal.file.length !== 0),
|
||||
false
|
||||
)) { issues.push({ code: 'FILES_NEED_HEALING' }) }
|
||||
|
||||
forEach(hosts_, ({ host }) => {
|
||||
issues.push({
|
||||
code: 'HOST_NOT_IN_NETWORK',
|
||||
key: 'HOST_NOT_IN_NETWORK' + host,
|
||||
params: { hostName: hosts[host].name_label },
|
||||
fix: {
|
||||
action: () => fixHostNotInXosanNetwork(sr.id, host),
|
||||
title: _('xosanIssueHostNotInNetwork')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return issues
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
const { showAdvanced } = this.state
|
||||
const {
|
||||
heal_,
|
||||
info_,
|
||||
sr,
|
||||
status_,
|
||||
statusDetail_,
|
||||
vbds,
|
||||
vdis
|
||||
} = this.props
|
||||
|
||||
const xosanConfig = this._getConfig()
|
||||
|
||||
if (!xosanConfig) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!xosanConfig.version) {
|
||||
return <div>
|
||||
{_('xosanWarning')}
|
||||
</div>
|
||||
}
|
||||
|
||||
const strippedVolumeInfo = this._getStrippedVolumeInfo()
|
||||
// const subVolumes = this._getSubvolumes() // TODO: uncomment when implementing subvolume deletion
|
||||
const orderedBrickList = this._getOrderedBrickList()
|
||||
|
||||
return <Container>
|
||||
<Row className='text-xs-center mb-1 mt-1'>
|
||||
<Col size={3}>
|
||||
<h2><Icon icon='sr' size='lg' color={status_ ? (status_.commandStatus ? 'text-success' : status_.error) : 'text-info'} /></h2>
|
||||
</Col>
|
||||
<Col size={3}>
|
||||
<h2><Icon icon='health' size='lg' color={heal_ ? (heal_.commandStatus ? 'text-success' : heal_.error) : 'text-info'} /></h2>
|
||||
</Col>
|
||||
<Col size={3}>
|
||||
<h2><Icon icon='settings' size='lg' color={statusDetail_ ? (statusDetail_.commandStatus ? 'text-success' : statusDetail_.error) : 'text-info'} /></h2>
|
||||
</Col>
|
||||
<Col size={3}>
|
||||
<h2><Icon icon='info' size='lg' color={info_ ? (info_.commandStatus ? 'text-success' : info_.error) : 'text-info'} /></h2>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='mb-1'>
|
||||
<Col><Issues issues={this._getIssues()} /></Col>
|
||||
</Row>
|
||||
{map(orderedBrickList, node => <Row key={node.config.brickName}>
|
||||
<Col>
|
||||
<Node
|
||||
heal_={heal_}
|
||||
info_={info_}
|
||||
node={node}
|
||||
sr={sr}
|
||||
status_={status_}
|
||||
statusDetail_={statusDetail_}
|
||||
vbds={vbds}
|
||||
vdis={vdis}
|
||||
/>
|
||||
</Col>
|
||||
</Row>)}
|
||||
<Row>
|
||||
<Col>
|
||||
<ActionButton
|
||||
btnStyle='success'
|
||||
handler={this._addSubvolume}
|
||||
icon='add'
|
||||
>
|
||||
{_('xosanAddSubvolume')}
|
||||
</ActionButton>
|
||||
<hr />
|
||||
</Col>
|
||||
</Row>
|
||||
{/* We will implement this later */}
|
||||
{/* <Row>
|
||||
<Col>
|
||||
<h2>{_('xosanRemoveSubvolumes')}</h2>
|
||||
<table className='table'>
|
||||
{map(subVolumes, (subvolume, i) => <tr key={i}>
|
||||
<td>
|
||||
<ul>{map(subvolume, (brick, j) => <li key={j}>{brick.name}</li>)}</ul>
|
||||
</td>
|
||||
<td>
|
||||
<ActionButton
|
||||
btnStyle='danger'
|
||||
icon='remove'
|
||||
handler={::this._removeSubVolume}
|
||||
handlerParam={map(subvolume, brick => brick.name)}
|
||||
>
|
||||
{_('xosanRemove')}
|
||||
</ActionButton>
|
||||
</td>
|
||||
</tr>)}
|
||||
</table>
|
||||
<hr />
|
||||
</Col>
|
||||
</Row> */}
|
||||
<Row>
|
||||
<Col>
|
||||
<h2><Toggle iconSize={1} onChange={this.toggleState('showAdvanced')} value={showAdvanced} /> {_('xosanAdvanced')}</h2>
|
||||
{strippedVolumeInfo && showAdvanced && <div>
|
||||
<h3>{_('xosanVolume')}</h3>
|
||||
<Container>
|
||||
<Field title={'Name'}>{strippedVolumeInfo.name}</Field>
|
||||
<Field title={'Status'}>{strippedVolumeInfo.statusStr}</Field>
|
||||
<Field title={'Type'}>{strippedVolumeInfo.typeStr}</Field>
|
||||
<Field title={'Brick Count'}>{strippedVolumeInfo.brickCount}</Field>
|
||||
<Field title={'Stripe Count'}>{strippedVolumeInfo.stripeCount}</Field>
|
||||
<Field title={'Replica Count'}>{strippedVolumeInfo.replicaCount}</Field>
|
||||
<Field title={'Arbiter Count'}>{strippedVolumeInfo.arbiterCount}</Field>
|
||||
<Field title={'Disperse Count'}>{strippedVolumeInfo.disperseCount}</Field>
|
||||
<Field title={'Redundancy Count'}>{strippedVolumeInfo.redundancyCount}</Field>
|
||||
</Container>
|
||||
<h3 className='mt-1'>{_('xosanVolumeOptions')}</h3>
|
||||
<Container>
|
||||
{map(strippedVolumeInfo.options, option =>
|
||||
<Field key={option.name} title={option.name}>{option.value}</Field>
|
||||
)}
|
||||
</Container>
|
||||
</div>}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,34 @@ import _, { messages } from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import ButtonGroup from 'button-group'
|
||||
import CenterPanel from 'center-panel'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import keys from 'lodash/keys'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { connectStore } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { SelectPool } from 'select-objects'
|
||||
import {
|
||||
Card,
|
||||
CardBlock,
|
||||
CardHeader
|
||||
} from 'card'
|
||||
import {
|
||||
connectStore,
|
||||
resolveId,
|
||||
resolveIds
|
||||
} from 'utils'
|
||||
import {
|
||||
Col,
|
||||
Container,
|
||||
Row
|
||||
} from 'grid'
|
||||
import {
|
||||
includes,
|
||||
isEmpty,
|
||||
keys,
|
||||
map
|
||||
} from 'lodash'
|
||||
import {
|
||||
createGetObject,
|
||||
createGetObjectsOfType,
|
||||
@@ -67,31 +84,37 @@ export const TaskItem = connectStore(() => ({
|
||||
</Col>
|
||||
</SingleLineRow>)
|
||||
|
||||
export default injectIntl(
|
||||
connectStore(() => {
|
||||
const getTasks = createGetObjectsOfType('task')
|
||||
@connectStore(() => {
|
||||
const getPendingTasks = createGetObjectsOfType('task').filter(
|
||||
[ task => task.status === 'pending' ]
|
||||
)
|
||||
|
||||
const getNPendingTasks = getTasks.count(
|
||||
[ task => task.status === 'pending' ]
|
||||
)
|
||||
const getNPendingTasks = getPendingTasks.count()
|
||||
|
||||
const getPendingTasksByPool = getTasks.filter(
|
||||
[ task => task.status === 'pending' ]
|
||||
).sort().groupBy('$pool')
|
||||
const getPendingTasksByPool = getPendingTasks.sort().groupBy('$pool')
|
||||
|
||||
const getPools = createGetObjectsOfType('pool').pick(
|
||||
createSelector(
|
||||
getPendingTasksByPool,
|
||||
pendingTasksByPool => keys(pendingTasksByPool)
|
||||
)
|
||||
).sort()
|
||||
const getPools = createGetObjectsOfType('pool').pick(
|
||||
createSelector(getPendingTasksByPool, keys)
|
||||
).sort()
|
||||
|
||||
return {
|
||||
nTasks: getNPendingTasks,
|
||||
pendingTasksByPool: getPendingTasksByPool,
|
||||
pools: getPools
|
||||
}
|
||||
})
|
||||
@injectIntl
|
||||
export default class Tasks extends Component {
|
||||
_showPoolTasks = pool => isEmpty(this.state.pools) || includes(resolveIds(this.state.pools), resolveId(pool))
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const {
|
||||
intl,
|
||||
nTasks,
|
||||
pendingTasksByPool
|
||||
} = props
|
||||
|
||||
return (state, props) => ({
|
||||
nTasks: getNPendingTasks(state, props),
|
||||
pendingTasksByPool: getPendingTasksByPool(state, props),
|
||||
pools: getPools(state, props)
|
||||
})
|
||||
})(({ intl, nTasks, pendingTasksByPool, pools }) => {
|
||||
if (isEmpty(pendingTasksByPool)) {
|
||||
return <Page header={HEADER} title='taskPage' formatTitle>
|
||||
<CenterPanel>
|
||||
@@ -112,19 +135,24 @@ export default injectIntl(
|
||||
const { formatMessage } = intl
|
||||
return <Page header={HEADER} title={`(${nTasks}) ${formatMessage(messages.taskPage)}`}>
|
||||
<Container>
|
||||
{map(pools, pool =>
|
||||
<Row>
|
||||
<Card>
|
||||
<CardHeader key={pool.id}><Link to={`/pools/${pool.id}`}>{pool.name_label}</Link></CardHeader>
|
||||
<CardBlock>
|
||||
{map(pendingTasksByPool[pool.id], task =>
|
||||
<TaskItem key={task.id} task={task} />
|
||||
)}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Row>
|
||||
)}
|
||||
<Row className='mb-1'>
|
||||
<SelectPool
|
||||
multi
|
||||
value={state.pools}
|
||||
onChange={this.linkState('pools')}
|
||||
/>
|
||||
</Row>
|
||||
{map(props.pools, pool => this._showPoolTasks(pool) && <Row>
|
||||
<Card>
|
||||
<CardHeader key={pool.id}><Link to={`/pools/${pool.id}`}>{pool.name_label}</Link></CardHeader>
|
||||
<CardBlock>
|
||||
{map(pendingTasksByPool[pool.id], task =>
|
||||
<TaskItem key={task.id} task={task} />
|
||||
)}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Row>)}
|
||||
</Container>
|
||||
</Page>
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@ export default class User extends Component {
|
||||
<Col smallSize={10}>
|
||||
<form className='form-inline' id='changePassword'>
|
||||
<input
|
||||
autocomplete='off'
|
||||
autoComplete='off'
|
||||
className='form-control'
|
||||
onChange={this._handleOldPasswordChange}
|
||||
placeholder={formatMessage(messages.oldPasswordPlaceholder)}
|
||||
@@ -339,7 +339,7 @@ export default class User extends Component {
|
||||
/>
|
||||
{' '}
|
||||
<input type='password'
|
||||
autocomplete='off'
|
||||
autoComplete='off'
|
||||
className='form-control'
|
||||
onChange={this._handleNewPasswordChange}
|
||||
placeholder={formatMessage(messages.newPasswordPlaceholder)}
|
||||
@@ -348,7 +348,7 @@ export default class User extends Component {
|
||||
/>
|
||||
{' '}
|
||||
<input
|
||||
autocomplete='off'
|
||||
autoComplete='off'
|
||||
className='form-control'
|
||||
onChange={this._handleConfirmPasswordChange}
|
||||
placeholder={formatMessage(messages.confirmPasswordPlaceholder)}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import ActionBar from 'action-bar'
|
||||
import _ from 'intl'
|
||||
import ActionBar, { Action } from 'action-bar'
|
||||
import React from 'react'
|
||||
import { connectStore } from 'utils'
|
||||
import { includes } from 'lodash'
|
||||
import { isAdmin } from 'selectors'
|
||||
import { addSubscriptions, connectStore } from 'utils'
|
||||
import { find, includes } from 'lodash'
|
||||
import { createSelector, getCheckPermissions, getUser } from 'selectors'
|
||||
import {
|
||||
cloneVm,
|
||||
copyVm,
|
||||
@@ -12,142 +13,163 @@ import {
|
||||
resumeVm,
|
||||
snapshotVm,
|
||||
startVm,
|
||||
stopVm
|
||||
stopVm,
|
||||
subscribeResourceSets
|
||||
} from 'xo'
|
||||
|
||||
const vmActionBarByState = {
|
||||
Running: ({ isAdmin, vm }) => (
|
||||
Running: ({ vm, isSelfUser, canAdministrate }) => (
|
||||
<ActionBar
|
||||
actions={[
|
||||
{
|
||||
icon: 'vm-stop',
|
||||
label: 'stopVmLabel',
|
||||
handler: stopVm,
|
||||
pending: includes(vm.current_operations, 'clean_shutdown')
|
||||
},
|
||||
{
|
||||
icon: 'vm-reboot',
|
||||
label: 'rebootVmLabel',
|
||||
handler: restartVm,
|
||||
pending: includes(vm.current_operations, 'clean_reboot')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-migrate',
|
||||
label: 'migrateVmLabel',
|
||||
handler: migrateVm,
|
||||
pending:
|
||||
includes(vm.current_operations, 'migrate_send') ||
|
||||
includes(vm.current_operations, 'pool_migrate')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-snapshot',
|
||||
label: 'snapshotVmLabel',
|
||||
handler: snapshotVm,
|
||||
pending: includes(vm.current_operations, 'snapshot')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'export',
|
||||
label: 'exportVmLabel',
|
||||
handler: exportVm,
|
||||
pending: includes(vm.current_operations, 'export')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-copy',
|
||||
label: 'copyVmLabel',
|
||||
handler: copyVm,
|
||||
pending: includes(vm.current_operations, 'copy')
|
||||
}
|
||||
]}
|
||||
display='icon'
|
||||
param={vm}
|
||||
/>
|
||||
handlerParam={vm}
|
||||
>
|
||||
<Action
|
||||
handler={stopVm}
|
||||
icon='vm-stop'
|
||||
label={_('stopVmLabel')}
|
||||
pending={includes(vm.current_operations, 'clean_shutdown')}
|
||||
/>
|
||||
<Action
|
||||
handler={restartVm}
|
||||
icon='vm-reboot'
|
||||
label={_('rebootVmLabel')}
|
||||
pending={includes(vm.current_operations, 'clean_reboot')}
|
||||
/>
|
||||
{!isSelfUser && <Action
|
||||
handler={migrateVm}
|
||||
icon='vm-migrate'
|
||||
label={_('migrateVmLabel')}
|
||||
pending={
|
||||
includes(vm.current_operations, 'migrate_send') ||
|
||||
includes(vm.current_operations, 'pool_migrate')
|
||||
}
|
||||
/>}
|
||||
{!isSelfUser && <Action
|
||||
handler={snapshotVm}
|
||||
icon='vm-snapshot'
|
||||
label={_('snapshotVmLabel')}
|
||||
pending={includes(vm.current_operations, 'snapshot')}
|
||||
/>}
|
||||
{!isSelfUser && canAdministrate && <Action
|
||||
handler={exportVm}
|
||||
icon='export'
|
||||
label={_('exportVmLabel')}
|
||||
pending={includes(vm.current_operations, 'export')}
|
||||
/>}
|
||||
{!isSelfUser && canAdministrate && <Action
|
||||
handler={copyVm}
|
||||
icon='vm-copy'
|
||||
label={_('copyVmLabel')}
|
||||
pending={includes(vm.current_operations, 'copy')}
|
||||
/>}
|
||||
</ActionBar>
|
||||
),
|
||||
Halted: ({ isAdmin, vm }) => (
|
||||
Halted: ({ vm, isSelfUser, canAdministrate }) => (
|
||||
<ActionBar
|
||||
actions={[
|
||||
{
|
||||
icon: 'vm-start',
|
||||
label: 'startVmLabel',
|
||||
handler: startVm,
|
||||
pending: includes(vm.current_operations, 'start')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-fast-clone',
|
||||
label: 'fastCloneVmLabel',
|
||||
handler: cloneVm,
|
||||
pending: includes(vm.current_operations, 'clone')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-migrate',
|
||||
label: 'migrateVmLabel',
|
||||
handler: migrateVm,
|
||||
pending: includes(vm.current_operations, 'pool_migrate')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-snapshot',
|
||||
label: 'snapshotVmLabel',
|
||||
handler: snapshotVm,
|
||||
pending: includes(vm.current_operations, 'snapshot')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'export',
|
||||
label: 'exportVmLabel',
|
||||
handler: exportVm,
|
||||
pending: includes(vm.current_operations, 'export')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-copy',
|
||||
label: 'copyVmLabel',
|
||||
handler: copyVm,
|
||||
pending: includes(vm.current_operations, 'copy')
|
||||
}
|
||||
]}
|
||||
display='icon'
|
||||
param={vm}
|
||||
/>
|
||||
handlerParam={vm}
|
||||
>
|
||||
<Action
|
||||
handler={startVm}
|
||||
icon='vm-start'
|
||||
label={_('startVmLabel')}
|
||||
pending={includes(vm.current_operations, 'start')}
|
||||
/>
|
||||
{!isSelfUser && canAdministrate && <Action
|
||||
handler={cloneVm}
|
||||
icon='vm-fast-clone'
|
||||
label={_('fastCloneVmLabel')}
|
||||
pending={includes(vm.current_operations, 'clone')}
|
||||
/>}
|
||||
{!isSelfUser && <Action
|
||||
handler={migrateVm}
|
||||
icon='vm-migrate'
|
||||
label={_('migrateVmLabel')}
|
||||
pending={
|
||||
includes(vm.current_operations, 'migrate_send') ||
|
||||
includes(vm.current_operations, 'pool_migrate')
|
||||
}
|
||||
/>}
|
||||
{!isSelfUser && <Action
|
||||
handler={snapshotVm}
|
||||
icon='vm-snapshot'
|
||||
label={_('snapshotVmLabel')}
|
||||
pending={includes(vm.current_operations, 'snapshot')}
|
||||
/>}
|
||||
{!isSelfUser && canAdministrate && <Action
|
||||
handler={exportVm}
|
||||
icon='export'
|
||||
label={_('exportVmLabel')}
|
||||
pending={includes(vm.current_operations, 'export')}
|
||||
/>}
|
||||
{!isSelfUser && canAdministrate && <Action
|
||||
handler={copyVm}
|
||||
icon='vm-copy'
|
||||
label={_('copyVmLabel')}
|
||||
pending={includes(vm.current_operations, 'copy')}
|
||||
/>}
|
||||
</ActionBar>
|
||||
),
|
||||
Suspended: ({ isAdmin, vm }) => (
|
||||
Suspended: ({ vm, isSelfUser, canAdministrate }) => (
|
||||
<ActionBar
|
||||
actions={[
|
||||
{
|
||||
icon: 'vm-start',
|
||||
label: 'resumeVmLabel',
|
||||
handler: resumeVm,
|
||||
pending: includes(vm.current_operations, 'start')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-snapshot',
|
||||
label: 'snapshotVmLabel',
|
||||
handler: snapshotVm,
|
||||
pending: includes(vm.current_operations, 'snapshot')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'export',
|
||||
label: 'exportVmLabel',
|
||||
handler: exportVm,
|
||||
pending: includes(vm.current_operations, 'export')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-copy',
|
||||
label: 'copyVmLabel',
|
||||
handler: copyVm,
|
||||
pending: includes(vm.current_operations, 'copy')
|
||||
}
|
||||
]}
|
||||
display='icon'
|
||||
param={vm}
|
||||
/>
|
||||
handlerParam={vm}
|
||||
>
|
||||
<Action
|
||||
handler={resumeVm}
|
||||
icon='vm-start'
|
||||
label={_('resumeVmLabel')}
|
||||
pending={includes(vm.current_operations, 'start')}
|
||||
/>
|
||||
{!isSelfUser && <Action
|
||||
handler={snapshotVm}
|
||||
icon='vm-snapshot'
|
||||
label={_('snapshotVmLabel')}
|
||||
pending={includes(vm.current_operations, 'snapshot')}
|
||||
/>}
|
||||
{!isSelfUser && canAdministrate && <Action
|
||||
handler={exportVm}
|
||||
icon='export'
|
||||
label={_('exportVmLabel')}
|
||||
pending={includes(vm.current_operations, 'export')}
|
||||
/>}
|
||||
{!isSelfUser && canAdministrate && <Action
|
||||
handler={copyVm}
|
||||
icon='vm-copy'
|
||||
label={_('copyVmLabel')}
|
||||
pending={includes(vm.current_operations, 'copy')}
|
||||
/>}
|
||||
</ActionBar>
|
||||
)
|
||||
}
|
||||
|
||||
const VmActionBar = connectStore({
|
||||
isAdmin
|
||||
})(({ isAdmin, vm }) => {
|
||||
const VmActionBar = addSubscriptions(() => ({
|
||||
resourceSets: subscribeResourceSets
|
||||
}))(connectStore(() => ({
|
||||
checkPermissions: getCheckPermissions,
|
||||
userId: createSelector(getUser, user => user.id)
|
||||
}))(({ checkPermissions, vm, userId, resourceSets }) => {
|
||||
// Is the user in the same resource set as the VM
|
||||
const _getIsSelfUser = createSelector(
|
||||
() => resourceSets,
|
||||
resourceSets => {
|
||||
const vmResourceSet = vm.resourceSet && find(resourceSets, { id: vm.resourceSet })
|
||||
|
||||
return vmResourceSet && includes(vmResourceSet.subjects, userId)
|
||||
}
|
||||
)
|
||||
|
||||
const _getCanAdministrate = createSelector(
|
||||
() => checkPermissions,
|
||||
() => vm.id,
|
||||
(check, vmId) => check(vmId, 'administrate')
|
||||
)
|
||||
|
||||
const ActionBar = vmActionBarByState[vm.power_state]
|
||||
if (!ActionBar) {
|
||||
return <p>No action bar for state {vm.power_state}</p>
|
||||
}
|
||||
|
||||
return <ActionBar isAdmin={isAdmin} vm={vm} />
|
||||
})
|
||||
return <ActionBar vm={vm} isSelfUser={_getIsSelfUser()} canAdministrate={_getCanAdministrate()} />
|
||||
}))
|
||||
export default VmActionBar
|
||||
|
||||
@@ -9,7 +9,6 @@ import VmActionBar from './action-bar'
|
||||
import { Select, Text } from 'editable'
|
||||
import {
|
||||
assign,
|
||||
forEach,
|
||||
isEmpty,
|
||||
map,
|
||||
pick
|
||||
@@ -23,13 +22,14 @@ import {
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
connectStore,
|
||||
mapPlus,
|
||||
routes
|
||||
} from 'utils'
|
||||
import {
|
||||
createGetObject,
|
||||
createGetObjectsOfType,
|
||||
createGetVmDisks,
|
||||
createSelector,
|
||||
createSumBy,
|
||||
getCheckPermissions,
|
||||
isAdmin
|
||||
} from 'selectors'
|
||||
@@ -71,16 +71,7 @@ import TabAdvanced from './tab-advanced'
|
||||
const getVbds = createGetObjectsOfType('VBD').pick(
|
||||
(state, props) => getVm(state, props).$VBDs
|
||||
).sort()
|
||||
const getVdis = createGetObjectsOfType('VDI').pick(
|
||||
createSelector(
|
||||
getVbds,
|
||||
vbds => mapPlus(vbds, (vbd, push) => {
|
||||
if (!vbd.is_cd_drive && vbd.VDI) {
|
||||
push(vbd.VDI)
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
const getVdis = createGetVmDisks(getVm)
|
||||
const getSrs = createGetObjectsOfType('SR').pick(
|
||||
createSelector(
|
||||
getVdis,
|
||||
@@ -88,15 +79,9 @@ import TabAdvanced from './tab-advanced'
|
||||
)
|
||||
)
|
||||
|
||||
const getVmTotalDiskSpace = createSelector(
|
||||
getVdis,
|
||||
vdis => {
|
||||
let vmTotalDiskSpace = 0
|
||||
forEach(vdis, vdi => {
|
||||
vmTotalDiskSpace += vdi.size
|
||||
})
|
||||
return vmTotalDiskSpace
|
||||
}
|
||||
const getVmTotalDiskSpace = createSumBy(
|
||||
createGetVmDisks(getVm),
|
||||
'size'
|
||||
)
|
||||
|
||||
const getHosts = createGetObjectsOfType('host')
|
||||
@@ -245,7 +230,6 @@ export default class Vm extends BaseComponent {
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
<Col>
|
||||
<NavTabs>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Copiable from 'copiable'
|
||||
import defined from 'xo-defined'
|
||||
import getEventValue from 'get-event-value'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
} from 'lodash'
|
||||
import {
|
||||
connectStore,
|
||||
firstDefined,
|
||||
formatSize,
|
||||
getCoresPerSocketPossibilities,
|
||||
normalizeXenToolsStatus,
|
||||
@@ -145,19 +145,21 @@ class CoresPerSocket extends Component {
|
||||
onChange={this._onChange}
|
||||
value={selectedCoresPerSocket || ''}
|
||||
>
|
||||
{_('vmChooseCoresPerSocket', message => <option value=''>{message}</option>)}
|
||||
{_('vmChooseCoresPerSocket', message => <option key='none' value=''>{message}</option>)}
|
||||
{this._selectedValueIsNotInOptions() &&
|
||||
_('vmCoresPerSocketIncorrectValue', message => <option value={selectedCoresPerSocket}> {message}</option>)
|
||||
_('vmCoresPerSocketIncorrectValue', message => <option key='incorrect' value={selectedCoresPerSocket}> {message}</option>)
|
||||
}
|
||||
{map(
|
||||
options,
|
||||
coresPerSocket => _(
|
||||
'vmCoresPerSocket', {
|
||||
coresPerSocket => <option
|
||||
key={coresPerSocket}
|
||||
value={coresPerSocket}
|
||||
>
|
||||
{_('vmCoresPerSocket', {
|
||||
nSockets: vm.CPUs.number / coresPerSocket,
|
||||
nCores: coresPerSocket
|
||||
},
|
||||
message => <option key={coresPerSocket} value={coresPerSocket}>{message}</option>
|
||||
)
|
||||
})}
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
{' '}
|
||||
@@ -367,8 +369,8 @@ export default ({
|
||||
<tr>
|
||||
<th>{_('vmMemoryLimitsLabel')}</th>
|
||||
<td>
|
||||
<p>Static: {formatSize(vm.memory.static[0])}/<Size value={firstDefined(vm.memory.static[1], null)} onChange={memoryStaticMax => editVm(vm, { memoryStaticMax })} /></p>
|
||||
<p>Dynamic: <Size value={firstDefined(vm.memory.dynamic[0], null)} onChange={memoryMin => editVm(vm, { memoryMin })} />/<Size value={firstDefined(vm.memory.dynamic[1], null)} onChange={memoryMax => editVm(vm, { memoryMax })} /></p>
|
||||
<p>Static: {formatSize(vm.memory.static[0])}/<Size value={defined(vm.memory.static[1], null)} onChange={memoryStaticMax => editVm(vm, { memoryStaticMax })} /></p>
|
||||
<p>Dynamic: <Size value={defined(vm.memory.dynamic[0], null)} onChange={memoryMin => editVm(vm, { memoryMin })} />/<Size value={defined(vm.memory.dynamic[1], null)} onChange={memoryMax => editVm(vm, { memoryMax })} /></p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -392,7 +394,7 @@ export default ({
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{_('osKernel')}</th>
|
||||
<td>{vm.os_version ? vm.os_version.uname ? vm.os_version.uname : _('unknownOsKernel') : _('unknownOsKernel')}</td>
|
||||
<td>{(vm.os_version && vm.os_version.uname) || _('unknownOsKernel')}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -150,7 +150,6 @@ export default class TabConsole extends Component {
|
||||
scale={scale}
|
||||
url={resolveUrl(`consoles/${vm.id}`)}
|
||||
/>
|
||||
{!minimalLayout && <p><em><Icon icon='info' /> <a href='https://bugs.xenserver.org/browse/XSO-650' target='_blank'>{_('tipLabel')} {_('tipConsoleLabel')}</a></em></p>}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -2,30 +2,33 @@ import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import Component from 'base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
import HTML5Backend from 'react-dnd-html5-backend'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import IsoDevice from 'iso-device'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import some from 'lodash/some'
|
||||
import StateButton from 'state-button'
|
||||
import TabButton from 'tab-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createSelector } from 'selectors'
|
||||
import { createSelector, createFinder, getCheckPermissions, isAdmin } from 'selectors'
|
||||
import { DragDropContext, DragSource, DropTarget } from 'react-dnd'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { noop } from 'utils'
|
||||
import { SelectSr, SelectVdi } from 'select-objects'
|
||||
import { noop, addSubscriptions, formatSize, connectStore, resolveResourceSet } from 'utils'
|
||||
import { SelectSr, SelectVdi, SelectResourceSetsSr } from 'select-objects'
|
||||
import { SizeInput, Toggle } from 'form'
|
||||
import { XoSelect, Size, Text } from 'editable'
|
||||
import { confirm } from 'modal'
|
||||
import { error } from 'notification'
|
||||
import {
|
||||
forEach,
|
||||
get,
|
||||
isEmpty,
|
||||
map,
|
||||
some
|
||||
} from 'lodash'
|
||||
import {
|
||||
attachDiskToVm,
|
||||
createDisk,
|
||||
@@ -38,7 +41,8 @@ import {
|
||||
isVmRunning,
|
||||
migrateVdi,
|
||||
setBootableVbd,
|
||||
setVmBootOrder
|
||||
setVmBootOrder,
|
||||
subscribeResourceSets
|
||||
} from 'xo'
|
||||
|
||||
const parseBootOrder = bootOrder => {
|
||||
@@ -66,30 +70,23 @@ const parseBootOrder = bootOrder => {
|
||||
onClose: propTypes.func,
|
||||
vm: propTypes.object.isRequired
|
||||
})
|
||||
@addSubscriptions({
|
||||
resourceSets: subscribeResourceSets
|
||||
})
|
||||
@connectStore({
|
||||
isAdmin
|
||||
})
|
||||
class NewDisk extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
sr: undefined
|
||||
}
|
||||
}
|
||||
|
||||
_createDisk = () => {
|
||||
const { vm, onClose = noop } = this.props
|
||||
const {name, size, bootable, readOnly} = this.refs
|
||||
const { sr } = this.state
|
||||
return createDisk(name.value, size.value, sr)
|
||||
.then(diskId => {
|
||||
const mode = readOnly.value ? 'RO' : 'RW'
|
||||
return attachDiskToVm(diskId, vm, {
|
||||
bootable: bootable && bootable.value,
|
||||
mode
|
||||
})
|
||||
.then(onClose)
|
||||
})
|
||||
}
|
||||
const { bootable, name, readOnly, size, sr } = this.state
|
||||
|
||||
_selectSr = sr => this.setState({sr})
|
||||
return createDisk(name, size, sr, {
|
||||
vm,
|
||||
bootable,
|
||||
mode: readOnly ? 'RO' : 'RW'
|
||||
}).then(onClose)
|
||||
}
|
||||
|
||||
// FIXME: duplicate code
|
||||
_getSrPredicate = createSelector(
|
||||
@@ -100,31 +97,66 @@ class NewDisk extends Component {
|
||||
poolId => sr => sr.$pool === poolId && isSrWritable(sr)
|
||||
)
|
||||
|
||||
_getResourceSet = createFinder(
|
||||
() => this.props.resourceSets,
|
||||
createSelector(
|
||||
() => this.props.vm.resourceSet,
|
||||
id => resourceSet => resourceSet.id === id
|
||||
)
|
||||
)
|
||||
|
||||
_getResolvedResourceSet = createSelector(
|
||||
this._getResourceSet,
|
||||
resolveResourceSet
|
||||
)
|
||||
|
||||
_getResourceSetDiskLimit = createSelector(
|
||||
this._getResourceSet,
|
||||
resourceSet => get(resourceSet, 'limits.disk.available')
|
||||
)
|
||||
|
||||
render () {
|
||||
const { vm } = this.props
|
||||
const { vm, isAdmin } = this.props
|
||||
const { formatMessage } = this.props.intl
|
||||
const { size, sr, name, bootable, readOnly } = this.state
|
||||
|
||||
const diskLimit = this._getResourceSetDiskLimit()
|
||||
const resourceSet = this._getResolvedResourceSet()
|
||||
|
||||
const SelectSr_ = isAdmin || resourceSet == null ? SelectSr : SelectResourceSetsSr
|
||||
|
||||
return <form id='newDiskForm'>
|
||||
<div className='form-group'>
|
||||
<SelectSr predicate={this._getSrPredicate()} onChange={this._selectSr} required />
|
||||
<SelectSr_
|
||||
onChange={this.linkState('sr')}
|
||||
predicate={this._getSrPredicate()}
|
||||
required
|
||||
resourceSet={isAdmin ? undefined : resourceSet}
|
||||
value={sr}
|
||||
/>
|
||||
</div>
|
||||
<fieldset className='form-inline'>
|
||||
<div className='form-group'>
|
||||
<input type='text' ref='name' placeholder={formatMessage(messages.vbdNamePlaceHolder)} className='form-control' required />
|
||||
<input type='text' onChange={this.linkState('name')} value={name} placeholder={formatMessage(messages.vbdNamePlaceHolder)} className='form-control' required />
|
||||
</div>
|
||||
{' '}
|
||||
<div className='form-group'>
|
||||
<SizeInput ref='size' placeholder={formatMessage(messages.vbdSizePlaceHolder)} required />
|
||||
<SizeInput onChange={this.linkState('size')} value={size} placeholder={formatMessage(messages.vbdSizePlaceHolder)} required />
|
||||
</div>
|
||||
{' '}
|
||||
<div className='form-group'>
|
||||
{vm.virtualizationMode === 'pv' && <span>{_('vbdBootable')} <Toggle ref='bootable' /> </span>}
|
||||
<span>{_('vbdReadonly')} <Toggle ref='readOnly' /></span>
|
||||
{vm.virtualizationMode === 'pv' && <span>{_('vbdBootable')} <Toggle onChange={this.toggleState('bootable')} value={bootable} /> </span>}
|
||||
<span>{_('vbdReadonly')} <Toggle onChange={this.toggleState('readOnly')} value={readOnly} /></span>
|
||||
</div>
|
||||
<span className='pull-right'>
|
||||
<ActionButton form='newDiskForm' icon='add' btnStyle='primary' handler={this._createDisk}>{_('vbdCreate')}</ActionButton>
|
||||
<ActionButton form='newDiskForm' icon='add' btnStyle='primary' handler={this._createDisk} disabled={diskLimit < size}>{_('vbdCreate')}</ActionButton>
|
||||
</span>
|
||||
</fieldset>
|
||||
{resourceSet != null && diskLimit != null && (
|
||||
diskLimit < size
|
||||
? <em className='text-danger'>{_('notEnoughSpaceInResourceSet', { resourceSet: <strong>{resourceSet.name}</strong>, spaceLeft: formatSize(diskLimit) })}</em>
|
||||
: <em>{_('useQuotaWarning', { resourceSet: <strong>{resourceSet.name}</strong>, spaceLeft: formatSize(diskLimit) })}</em>
|
||||
)}
|
||||
</form>
|
||||
}
|
||||
}
|
||||
@@ -156,18 +188,16 @@ class AttachDisk extends Component {
|
||||
|
||||
_addVdi = () => {
|
||||
const { vm, vbds, onClose = noop } = this.props
|
||||
const { vdi } = this.state
|
||||
const { bootable, readOnly } = this.refs
|
||||
const { bootable, readOnly, vdi } = this.state
|
||||
|
||||
const _isFreeForWriting = vdi => vdi.$VBDs.length === 0 || some(vdi.$VBDs, id => {
|
||||
const vbd = vbds[id]
|
||||
return !vbd || !vbd.attached || vbd.read_only
|
||||
})
|
||||
const mode = readOnly.value || !_isFreeForWriting(vdi) ? 'RO' : 'RW'
|
||||
return attachDiskToVm(vdi, vm, {
|
||||
bootable: bootable && bootable.value,
|
||||
mode
|
||||
})
|
||||
.then(onClose)
|
||||
bootable,
|
||||
mode: readOnly || !_isFreeForWriting(vdi) ? 'RO' : 'RW'
|
||||
}).then(onClose)
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -350,6 +380,10 @@ class MigrateVdiModalBody extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
@connectStore(() => ({
|
||||
checkPermissions: getCheckPermissions,
|
||||
isAdmin
|
||||
}))
|
||||
export default class TabDisks extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
@@ -392,6 +426,19 @@ export default class TabDisks extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
_getIsVmAdmin = createSelector(
|
||||
() => this.props.checkPermissions,
|
||||
() => this.props.vm && this.props.vm.id,
|
||||
(check, vmId) => check(vmId, 'administrate')
|
||||
)
|
||||
|
||||
_getAttachDiskPredicate = createSelector(
|
||||
() => this.props.isAdmin,
|
||||
() => this.props.vm.resourceSet,
|
||||
this._getIsVmAdmin,
|
||||
(isAdmin, resourceSet, isVmAdmin) => isAdmin || (resourceSet == null && isVmAdmin)
|
||||
)
|
||||
|
||||
render () {
|
||||
const {
|
||||
srs,
|
||||
@@ -415,12 +462,12 @@ export default class TabDisks extends Component {
|
||||
icon='add'
|
||||
labelId='vbdCreateDeviceButton'
|
||||
/>
|
||||
<TabButton
|
||||
{this._getAttachDiskPredicate() && <TabButton
|
||||
btnStyle={attachDisk ? 'info' : 'primary'}
|
||||
handler={this._toggleAttachDisk}
|
||||
icon='disk'
|
||||
labelId='vdiAttachDeviceButton'
|
||||
/>
|
||||
/>}
|
||||
{vm.virtualizationMode !== 'pv' && <TabButton
|
||||
btnStyle={bootOrder ? 'info' : 'primary'}
|
||||
handler={this._toggleBootOrder}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _ from 'intl'
|
||||
import Copiable from 'copiable'
|
||||
import defined from 'xo-defined'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
@@ -14,7 +15,6 @@ import { Container, Row, Col } from 'grid'
|
||||
import { Number, Size } from 'editable'
|
||||
import {
|
||||
connectStore,
|
||||
firstDefined,
|
||||
formatSize,
|
||||
osFamily
|
||||
} from 'utils'
|
||||
@@ -43,7 +43,7 @@ export default connectStore(() => {
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<h2 className='form-inline'>
|
||||
<Size value={firstDefined(vm.memory.dynamic[1], null)} onChange={memory => editVm(vm, { memory })} />
|
||||
<Size value={defined(vm.memory.dynamic[1], null)} onChange={memory => editVm(vm, { memory })} />
|
||||
<span><Icon icon='memory' size='lg' /></span>
|
||||
</h2>
|
||||
<BlockLink to={`/vms/${vm.id}/stats`}>{statsOverview && <MemorySparkLines data={statsOverview} />}</BlockLink>
|
||||
@@ -83,12 +83,12 @@ export default connectStore(() => {
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<BlockLink to={`/vms/${vm.id}/network`}>
|
||||
<Copiable tagName='p'>
|
||||
{vm.addresses && vm.addresses['0/ip']
|
||||
? vm.addresses['0/ip']
|
||||
: _('noIpv4Record')
|
||||
}
|
||||
</Copiable>
|
||||
{vm.addresses && vm.addresses['0/ip']
|
||||
? <Copiable tagName='p'>
|
||||
{vm.addresses['0/ip']}
|
||||
</Copiable>
|
||||
: <p>{_('noIpv4Record')}</p>
|
||||
}
|
||||
</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
|
||||
@@ -403,7 +403,7 @@ export default class TabNetwork extends BaseComponent {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(vm.VIFs, vif => <VifItem vifId={vif} isVmRunning={isVmRunning(vm)} resourceSet={vm.resourceSet} />)}
|
||||
{map(vm.VIFs, vif => <VifItem key={vif} vifId={vif} isVmRunning={isVmRunning(vm)} resourceSet={vm.resourceSet} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
{vm.addresses && !isEmpty(vm.addresses)
|
||||
|
||||
132
src/xo-app/xosan/creation-progress.js
Normal file
132
src/xo-app/xosan/creation-progress.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import { Col, Row } from 'grid'
|
||||
import { createSelector } from 'selectors'
|
||||
import {
|
||||
addSubscriptions,
|
||||
createFakeProgress
|
||||
} from 'utils'
|
||||
import {
|
||||
subscribeCheckSrCurrentState
|
||||
} from 'xo'
|
||||
import {
|
||||
map,
|
||||
sum
|
||||
} from 'lodash'
|
||||
|
||||
const ESTIMATED_DURATIONS = [
|
||||
10, // configuringNetwork
|
||||
50, // importingVm
|
||||
30, // copyingVms
|
||||
30, // configuringVms
|
||||
10, // configuringGluster
|
||||
5, // creatingSr
|
||||
5 // scanningSr
|
||||
]
|
||||
|
||||
const TOTAL_ESTIMATED_DURATION = sum(ESTIMATED_DURATIONS)
|
||||
|
||||
@addSubscriptions(props => ({
|
||||
currentState: cb => subscribeCheckSrCurrentState(props.pool, cb)
|
||||
}))
|
||||
export default class CreationProgress extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.state = { intermediateProgress: 0 }
|
||||
|
||||
let sum = 0
|
||||
let _sum = 0
|
||||
this._milestones = map(ESTIMATED_DURATIONS, duration => {
|
||||
_sum = sum
|
||||
sum += duration
|
||||
|
||||
return _sum
|
||||
})
|
||||
}
|
||||
|
||||
_startNewFakeProgress = state => {
|
||||
this._fakeProgress = createFakeProgress(ESTIMATED_DURATIONS[state])
|
||||
this.setState({ intermediateProgress: 0 })
|
||||
this._loopProgress()
|
||||
}
|
||||
|
||||
_loopProgress = () => {
|
||||
this.setState({ intermediateProgress: this._fakeProgress() })
|
||||
this._loopTimeout = setTimeout(this._loopProgress, 50)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { currentState } = this.props
|
||||
|
||||
if (currentState && currentState.operation === 'createSr') {
|
||||
this._startNewFakeProgress(currentState.state)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearTimeout(this._loopTimeout)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
const oldState = this.props.currentState
|
||||
const newState = props.currentState
|
||||
|
||||
if (oldState === newState) {
|
||||
return
|
||||
}
|
||||
|
||||
clearTimeout(this._loopTimeout)
|
||||
|
||||
if (newState && newState.operation === 'createSr') {
|
||||
if (oldState != null) {
|
||||
// Step transition: set the end of the milestone to the current position so the
|
||||
// progress bar doesn't unsmoothly jump to the actual end of the milestone
|
||||
this._milestones[newState.state] = this._getMainProgress()
|
||||
}
|
||||
this._startNewFakeProgress(newState.state)
|
||||
}
|
||||
}
|
||||
|
||||
_getMainProgress = createSelector(
|
||||
() => this.props.currentState && this.props.currentState.state,
|
||||
() => this.state.intermediateProgress,
|
||||
(state, intermediateProgress) => {
|
||||
if (state == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const previousMilestone = this._milestones[state]
|
||||
const stepLength = (this._milestones[state + 1] || TOTAL_ESTIMATED_DURATION) - previousMilestone
|
||||
|
||||
return previousMilestone + intermediateProgress * stepLength
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
const { currentState, pool } = this.props
|
||||
|
||||
if (currentState == null || currentState.operation !== 'createSr') {
|
||||
return null
|
||||
}
|
||||
|
||||
const { state, states } = currentState
|
||||
|
||||
return <Row>
|
||||
<Col size={3}>
|
||||
<strong>{_('xosanCreatingOn', { pool: pool.name_label })}</strong>
|
||||
</Col>
|
||||
<Col size={3}>
|
||||
({state + 1}/{states.length}) {_(`xosanState_${states[state]}`)}
|
||||
</Col>
|
||||
<Col size={6}>
|
||||
<progress
|
||||
className='progress'
|
||||
max={TOTAL_ESTIMATED_DURATION}
|
||||
value={this._getMainProgress()}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import Icon from 'icon'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import {
|
||||
isInteger,
|
||||
map
|
||||
} from 'lodash'
|
||||
|
||||
@@ -64,7 +65,11 @@ const disperseGraph = (nSrs, redundancy, w, h) => {
|
||||
}
|
||||
|
||||
const replicationGraph = (nSrs, redundancy, w, h) => {
|
||||
const nGroups = nSrs / redundancy // Should always be an integer
|
||||
const nGroups = nSrs / redundancy
|
||||
|
||||
if (!isInteger(nGroups)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return graph(nGroups, redundancy, w, h, redundancy - 1)
|
||||
}
|
||||
|
||||
@@ -1,54 +1,46 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Collapse from 'collapse'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import Page from '../page'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import SortedTable from 'sorted-table'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Container, Col } from 'grid'
|
||||
import { Toggle } from 'form'
|
||||
import { SelectPif } from 'select-objects'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import {
|
||||
every,
|
||||
filter,
|
||||
find,
|
||||
forEach,
|
||||
isEmpty,
|
||||
keys,
|
||||
map,
|
||||
mapValues,
|
||||
pickBy,
|
||||
some
|
||||
} from 'lodash'
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
createSelector,
|
||||
createSort
|
||||
createSelector
|
||||
} from 'selectors'
|
||||
import {
|
||||
addSubscriptions,
|
||||
compareVersions,
|
||||
connectStore,
|
||||
cowSet,
|
||||
formatSize,
|
||||
isXosanPack,
|
||||
mapPlus
|
||||
isXosanPack
|
||||
} from 'utils'
|
||||
import {
|
||||
computeXosanPossibleOptions,
|
||||
createXosanSR,
|
||||
downloadAndInstallXosanPack,
|
||||
getVolumeInfo,
|
||||
deleteSr,
|
||||
registerXosan,
|
||||
restartHostsAgents,
|
||||
subscribeIsInstallingXosan,
|
||||
subscribePlugins,
|
||||
subscribeResourceCatalog
|
||||
subscribeResourceCatalog,
|
||||
subscribeVolumeInfo
|
||||
} from 'xo'
|
||||
|
||||
import Graph from './graph'
|
||||
import NewXosan from './new-xosan'
|
||||
import CreationProgress from './creation-progress'
|
||||
|
||||
export const INFO_TYPES = [ 'heal', 'status', 'info', 'statusDetail', 'hosts' ]
|
||||
|
||||
// ==================================================================
|
||||
|
||||
@@ -58,460 +50,173 @@ const HEADER = <Container>
|
||||
|
||||
// ==================================================================
|
||||
|
||||
@connectStore(() => ({
|
||||
vifs: createGetObjectsOfType('VIF'),
|
||||
vms: createGetObjectsOfType('VM'),
|
||||
vbds: createGetObjectsOfType('VBD'),
|
||||
vdis: createGetObjectsOfType('VDI')
|
||||
}))
|
||||
export class XosanVolumesTable extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
peers: null,
|
||||
volumesByConfig: null,
|
||||
volumesByID: null
|
||||
}
|
||||
}
|
||||
const XOSAN_COLUMNS = [
|
||||
{
|
||||
itemRenderer: (sr, { status }) => {
|
||||
const pbdsDetached = every(map(sr.pbds, 'attached')) ? null : _('xosanPbdsDetached')
|
||||
const badStatus = status && every(status[sr.id])
|
||||
? null
|
||||
: _('xosanBadStatus', { badStatuses: <ul>map(status, (_, status) => <li key={status}>{status}</li>)</ul> })
|
||||
|
||||
componentDidMount () {
|
||||
if (this.props.xosansrs && this.props.xosansrs.length > 0) {
|
||||
Promise.all(this.props.xosansrs.map(sr => getVolumeInfo(sr.id))).then(volumes => {
|
||||
const volumeConfig = {}
|
||||
volumes.forEach((volume, index) => {
|
||||
volumeConfig[this.props.xosansrs[index].id] = volume
|
||||
})
|
||||
this.setState({
|
||||
volumeConfig
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { xosansrs, hosts } = this.props
|
||||
return <div>
|
||||
<h3>{_('xosanSrTitle')}</h3>
|
||||
<table className='table table-striped'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{_('xosanName')}</th>
|
||||
<th>{_('xosanHosts')}</th>
|
||||
<th>{_('xosanVolumeId')}</th>
|
||||
<th>{_('xosanSize')}</th>
|
||||
<th>{_('xosanUsedSpace')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(xosansrs, sr => {
|
||||
const configsMap = {}
|
||||
forEach(sr.pbds, pbd => { configsMap[pbd.device_config['server']] = true })
|
||||
|
||||
return <tr key={sr.id}>
|
||||
<td>
|
||||
<Link to={`/srs/${sr.id}/xosan`}>{sr.name_label}</Link>
|
||||
</td>
|
||||
<td>
|
||||
{ map(sr.pbds, ({ host }) => find(hosts, [ 'id', host ]).name_label).join(', ') }
|
||||
</td>
|
||||
<td>
|
||||
{ this.state.volumeConfig && this.state.volumeConfig[sr.id] && this.state.volumeConfig[sr.id]['Volume ID'] }
|
||||
</td>
|
||||
<td>
|
||||
{formatSize(sr.size)}
|
||||
</td>
|
||||
<td>
|
||||
{sr.size > 0 &&
|
||||
<Tooltip content={_('spaceLeftTooltip', {
|
||||
used: String(Math.round((sr.physical_usage / sr.size) * 100)),
|
||||
free: formatSize(sr.size - sr.physical_usage)
|
||||
})}>
|
||||
<progress
|
||||
className='progress'
|
||||
max='100'
|
||||
style={{ margin: 0 }}
|
||||
value={(sr.physical_usage / sr.size) * 100}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
|
||||
const _findLatestTemplate = templates => {
|
||||
let latestTemplate = templates[0]
|
||||
|
||||
forEach(templates, pack => {
|
||||
if (compareVersions(pack.version, latestTemplate.version) > 0) {
|
||||
latestTemplate = pack
|
||||
}
|
||||
})
|
||||
|
||||
return latestTemplate
|
||||
}
|
||||
|
||||
class PoolAvailableSrs extends Component {
|
||||
state = {
|
||||
selectedSrs: {}
|
||||
}
|
||||
|
||||
_selectSr = (event, srId) => {
|
||||
const selectedSrs = { ...this.state.selectedSrs }
|
||||
selectedSrs[srId] = event.target.checked
|
||||
|
||||
computeXosanPossibleOptions(keys(pickBy(selectedSrs))).then(suggestions => {
|
||||
this.setState({
|
||||
selectedSrs,
|
||||
suggestion: 0,
|
||||
suggestions
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getPifPredicate = createSelector(
|
||||
() => this.props.pool,
|
||||
pool => pif => pif.vlan === -1 && pif.$host === pool.master
|
||||
)
|
||||
|
||||
_getNSelectedSrs = createSelector(
|
||||
() => this.state.selectedSrs,
|
||||
srs => filter(srs).length
|
||||
)
|
||||
|
||||
_getLatestTemplate = createSelector(
|
||||
() => this.props.templates,
|
||||
_findLatestTemplate
|
||||
)
|
||||
|
||||
_getDisableSrCheckbox = createSelector(
|
||||
() => this.state.selectedSrs,
|
||||
() => this.props.lvmsrs,
|
||||
(selectedSrs, lvmsrs) => sr =>
|
||||
!every(keys(pickBy(selectedSrs)), selectedSrId =>
|
||||
selectedSrId === sr.id ||
|
||||
find(lvmsrs, { id: selectedSrId }).$container !== sr.$container
|
||||
)
|
||||
)
|
||||
|
||||
_getDisableCreation = createSelector(
|
||||
() => this.state.suggestion,
|
||||
() => this.state.suggestions,
|
||||
() => this.state.pif,
|
||||
this._getNSelectedSrs,
|
||||
(suggestion, suggestions, pif, nSelectedSrs) =>
|
||||
!suggestions || !suggestions[suggestion] || !pif || nSelectedSrs < 2
|
||||
)
|
||||
|
||||
_createXosanVm = () => {
|
||||
const { pif, vlan, selectedSrs, suggestion, suggestions } = this.state
|
||||
|
||||
const params = suggestions[suggestion]
|
||||
|
||||
if (!params) {
|
||||
return
|
||||
}
|
||||
|
||||
return createXosanSR({
|
||||
template: this._getLatestTemplate(),
|
||||
pif,
|
||||
vlan: vlan || 0,
|
||||
srs: keys(pickBy(selectedSrs)),
|
||||
glusterType: params.layout,
|
||||
redundancy: params.redundancy
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
hosts,
|
||||
lvmsrs,
|
||||
pool
|
||||
} = this.props
|
||||
const {
|
||||
pif,
|
||||
selectedSrs,
|
||||
suggestion,
|
||||
suggestions,
|
||||
useVlan,
|
||||
vlan
|
||||
} = this.state
|
||||
|
||||
const disableSrCheckbox = this._getDisableSrCheckbox()
|
||||
|
||||
return <div className='mb-3'>
|
||||
<h3>{_('xosanAvailableSrsTitle')}</h3>
|
||||
<table className='table table-striped'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>{_('xosanName')}</th>
|
||||
<th>{_('xosanHost')}</th>
|
||||
<th>{_('xosanSize')}</th>
|
||||
<th>{_('xosanUsedSpace')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(lvmsrs, sr => {
|
||||
const host = find(hosts, [ 'id', sr.$container ])
|
||||
|
||||
return <tr key={sr.id}>
|
||||
<td>
|
||||
<input
|
||||
checked={selectedSrs[sr.id] || false}
|
||||
disabled={disableSrCheckbox(sr)}
|
||||
onChange={event => this._selectSr(event, sr.id)}
|
||||
type='checkbox'
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/srs/${sr.id}/general`}>{sr.name_label}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/hosts/${host.id}/general`}>{host.name_label}</Link>
|
||||
</td>
|
||||
<td>
|
||||
{formatSize(sr.size)}
|
||||
</td>
|
||||
<td>
|
||||
{sr.size > 0 &&
|
||||
<Tooltip content={_('spaceLeftTooltip', {
|
||||
used: String(Math.round((sr.physical_usage / sr.size) * 100)),
|
||||
free: formatSize(sr.size - sr.physical_usage)
|
||||
})}>
|
||||
<progress
|
||||
className='progress'
|
||||
max='100'
|
||||
value={(sr.physical_usage / sr.size) * 100}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>{_('xosanSuggestions')}</h3>
|
||||
{isEmpty(suggestions)
|
||||
? <em>{_('xosanSelect2Srs')}</em>
|
||||
: <div>
|
||||
<table className='table table-striped'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>{_('xosanLayout')}</th>
|
||||
<th>{_('xosanRedundancy')}</th>
|
||||
<th>{_('xosanCapacity')}</th>
|
||||
<th>{_('xosanAvailableSpace')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(suggestions, ({ layout, redundancy, capacity, availableSpace }, index) => <tr key={index}>
|
||||
<td>
|
||||
<input
|
||||
checked={+suggestion === index}
|
||||
name={`suggestion_${pool.id}`}
|
||||
onChange={this.linkState('suggestion')}
|
||||
type='radio'
|
||||
value={index}
|
||||
/>
|
||||
</td>
|
||||
<td>{layout}</td>
|
||||
<td>{redundancy}</td>
|
||||
<td>{capacity}</td>
|
||||
<td>{formatSize(availableSpace)}</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
<Graph
|
||||
height={160}
|
||||
layout={suggestions[suggestion].layout}
|
||||
nSrs={this._getNSelectedSrs()}
|
||||
redundancy={suggestions[suggestion].redundancy}
|
||||
width={600}
|
||||
/>
|
||||
<hr />
|
||||
<Container>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>
|
||||
<SelectPif
|
||||
onChange={this.linkState('pif')}
|
||||
predicate={this._getPifPredicate()}
|
||||
value={pif}
|
||||
/>
|
||||
</Col>
|
||||
<Col size={3}>
|
||||
<input
|
||||
className='form-control pull-right'
|
||||
disabled={!useVlan}
|
||||
onChange={this.linkState('vlan')}
|
||||
placeholder='VLAN'
|
||||
style={{ width: '70%' }}
|
||||
type='text'
|
||||
value={vlan}
|
||||
/>
|
||||
<Toggle className='pull-right mr-1' onChange={this.linkState('useVlan')} value={useVlan} />
|
||||
</Col>
|
||||
<Col size={3}>
|
||||
<ActionButton
|
||||
btnStyle='success'
|
||||
className='pull-right'
|
||||
disabled={this._getDisableCreation()}
|
||||
handler={this._createXosanVm}
|
||||
icon='add'
|
||||
>
|
||||
{_('xosanCreate')}
|
||||
</ActionButton>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</Container>
|
||||
</div>
|
||||
if (pbdsDetached != null || badStatus != null) {
|
||||
return <Tooltip content={pbdsDetached || badStatus}>
|
||||
<Icon icon={pbdsDetached ? 'busy' : 'halted'} />
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
return <Tooltip content={_('xosanRunning')}>
|
||||
<Icon icon='running' />
|
||||
</Tooltip>
|
||||
}
|
||||
},
|
||||
{
|
||||
name: _('xosanPool'),
|
||||
itemRenderer: sr => sr.pool == null ? null : <Link to={`/pools/${sr.pool.id}`}>{sr.pool.name_label}</Link>,
|
||||
sortCriteria: sr => sr.pool && sr.pool.name_label
|
||||
},
|
||||
{
|
||||
name: _('xosanName'),
|
||||
itemRenderer: sr => <Link to={`/srs/${sr.id}`}>{sr.name_label}</Link>,
|
||||
sortCriteria: sr => sr.name_label
|
||||
},
|
||||
{
|
||||
name: _('xosanHosts'),
|
||||
itemRenderer: sr => <span>
|
||||
{map(sr.hosts, (host, i) => [ i ? ', ' : null, <Link to={`/hosts/${host.id}`}>{host.name_label}</Link> ])}
|
||||
</span>
|
||||
},
|
||||
{
|
||||
name: _('xosanSize'),
|
||||
itemRenderer: sr => formatSize(sr.size),
|
||||
sortCriteria: sr => sr.size
|
||||
},
|
||||
{
|
||||
name: _('xosanUsedSpace'),
|
||||
itemRenderer: sr => sr.size > 0
|
||||
? <Tooltip content={_('spaceLeftTooltip', {
|
||||
used: String(Math.round((sr.physical_usage * 100) / sr.size)),
|
||||
free: formatSize(sr.size - sr.physical_usage)
|
||||
})}>
|
||||
<progress
|
||||
className='progress'
|
||||
max='100'
|
||||
value={(sr.physical_usage * 100) / sr.size}
|
||||
/>
|
||||
</Tooltip>
|
||||
: null,
|
||||
sortCriteria: sr => (sr.physical_usage * 100) / sr.size
|
||||
}
|
||||
]
|
||||
|
||||
const XOSAN_INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
handler: deleteSr,
|
||||
icon: 'delete',
|
||||
label: _('xosanDelete'),
|
||||
level: 'danger'
|
||||
}
|
||||
]
|
||||
|
||||
@connectStore(() => {
|
||||
const getIsInPool = createSelector(
|
||||
(_, { pool }) => pool != null && pool.id,
|
||||
poolId => obj => obj.$pool === poolId
|
||||
)
|
||||
const getHosts = createGetObjectsOfType('host')
|
||||
const getHostsByPool = getHosts.groupBy('$pool')
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
|
||||
const getPbdsBySr = createGetObjectsOfType('PBD').filter(getIsInPool).groupBy('SR')
|
||||
const getHosts = createGetObjectsOfType('host').filter(getIsInPool)
|
||||
|
||||
// LVM SRs that are connected
|
||||
const getLvmSrs = createSort(createSelector(
|
||||
createGetObjectsOfType('SR').filter(
|
||||
createSelector(
|
||||
getHosts,
|
||||
getIsInPool,
|
||||
(hosts, isInPool) =>
|
||||
sr =>
|
||||
isInPool(sr) &&
|
||||
!sr.shared &&
|
||||
sr.SR_type === 'lvm' &&
|
||||
find(hosts, { id: sr.$container }).power_state === 'Running'
|
||||
)
|
||||
),
|
||||
getPbdsBySr,
|
||||
(srs, pbdsBySr) => mapPlus(srs, (sr, push) => {
|
||||
let pbds
|
||||
if ((pbds = pbdsBySr[sr.id]).length) {
|
||||
push({ ...sr, pbds })
|
||||
}
|
||||
})
|
||||
), 'name_label')
|
||||
|
||||
const getXosanSrs = createSort(createSelector(
|
||||
createGetObjectsOfType('SR').filter(createSelector(
|
||||
(_, { pool }) => pool != null && pool.id,
|
||||
poolId =>
|
||||
sr => sr.$pool === poolId && sr.shared && sr.SR_type === 'xosan'
|
||||
)),
|
||||
getPbdsBySr,
|
||||
(srs, pbdsBySr) =>
|
||||
map(srs, sr => ({ ...sr, pbds: pbdsBySr[sr.id] }))
|
||||
), 'name_label')
|
||||
|
||||
const getTemplates = createSelector(
|
||||
(_, { catalog }) => catalog,
|
||||
catalog => filter(catalog.xosan, { type: 'xva' })
|
||||
)
|
||||
|
||||
// Hosts whose toolstack hasn't been restarted since XOSAN-pack installation
|
||||
const getHostsNeedRestart = createSelector(
|
||||
(_, { pool }) => pool && pool.xosanPackInstallationTime,
|
||||
getHosts,
|
||||
(xosanPackInstallationTime, hosts) => filter(hosts, host =>
|
||||
host.power_state === 'Running' &&
|
||||
xosanPackInstallationTime != null &&
|
||||
xosanPackInstallationTime > host.agentStartTime
|
||||
)
|
||||
)
|
||||
|
||||
const getIsMasterOffline = createSelector(
|
||||
getHosts,
|
||||
(_, { pool }) => pool.master,
|
||||
(hosts, id) => find(hosts, { id }).power_state !== 'Running'
|
||||
)
|
||||
|
||||
return {
|
||||
isMasterOffline: getIsMasterOffline,
|
||||
hosts: getHosts,
|
||||
lvmSrs: getLvmSrs,
|
||||
hostsNeedRestart: getHostsNeedRestart,
|
||||
templates: getTemplates,
|
||||
xosanSrs: getXosanSrs
|
||||
}
|
||||
})
|
||||
class Pool extends Component {
|
||||
componentDidMount () {
|
||||
this.componentWillUnmount = subscribeIsInstallingXosan(this.props.pool, isInstallingXosan => {
|
||||
this.setState({ isInstallingXosan })
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { xosanSrs, lvmSrs, hosts, noPack, templates, pool, hostsNeedRestart, isMasterOffline } = this.props
|
||||
const { isInstallingXosan } = this.state
|
||||
|
||||
if (isInstallingXosan) {
|
||||
return <em><Icon icon='loading' /> {_('xosanInstalling')}</em>
|
||||
}
|
||||
|
||||
if (noPack) {
|
||||
return <div className='mb-3'>
|
||||
<Icon icon='error' /> {_('xosanNeedPack')}
|
||||
<br />
|
||||
<ActionButton btnStyle='success' icon='export' handler={downloadAndInstallXosanPack} handlerParam={pool}>{_('xosanInstallIt')}</ActionButton>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (isMasterOffline) {
|
||||
return <div className='mb-3'>
|
||||
<Icon icon='error' /> {_('xosanMasterOffline')}
|
||||
</div>
|
||||
}
|
||||
|
||||
if (!isEmpty(hostsNeedRestart)) {
|
||||
return <div className='mb-3'>
|
||||
<Icon icon='error' /> {_('xosanNeedRestart')}
|
||||
<br />
|
||||
<ActionButton btnStyle='success' icon='host-restart-agent' handler={restartHostsAgents} handlerParam={hostsNeedRestart}>{_('xosanRestartAgents')}</ActionButton>
|
||||
</div>
|
||||
}
|
||||
|
||||
return xosanSrs && xosanSrs.length
|
||||
? <XosanVolumesTable hosts={hosts} xosansrs={xosanSrs} lvmsrs={lvmSrs} />
|
||||
: <PoolAvailableSrs hosts={hosts} pool={pool} lvmsrs={lvmSrs} templates={templates} />
|
||||
}
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
|
||||
@connectStore(() => ({
|
||||
pools: createGetObjectsOfType('pool'),
|
||||
noPacksByPool: createSelector(
|
||||
createGetObjectsOfType('host').groupBy('$pool'),
|
||||
const noPacksByPool = createSelector(
|
||||
getHostsByPool,
|
||||
hostsByPool => mapValues(hostsByPool, (poolHosts, poolId) =>
|
||||
!every(poolHosts, host => some(host.supplementalPacks, isXosanPack))
|
||||
)
|
||||
)
|
||||
}))
|
||||
|
||||
const getPbdsBySr = createGetObjectsOfType('PBD').groupBy('SR')
|
||||
const getXosanSrs = createSelector(
|
||||
createGetObjectsOfType('SR').filter([ sr => sr.shared && sr.SR_type === 'xosan' ]),
|
||||
getPbdsBySr,
|
||||
getPools,
|
||||
getHosts,
|
||||
(srs, pbdsBySr, pools, hosts) => {
|
||||
return map(srs, sr => ({
|
||||
...sr,
|
||||
pbds: pbdsBySr[sr.id],
|
||||
pool: find(pools, { id: sr.$pool }),
|
||||
hosts: map(pbdsBySr[sr.id], ({ host }) => find(hosts, [ 'id', host ])),
|
||||
config: sr.other_config['xo:xosan_config'] && JSON.parse(sr.other_config['xo:xosan_config'])
|
||||
}))
|
||||
}
|
||||
)
|
||||
|
||||
const getIsMasterOfflineByPool = createSelector(
|
||||
getHostsByPool,
|
||||
getPools,
|
||||
(hostsByPool, pools) => {
|
||||
const isMasterOfflineByPool = {}
|
||||
forEach(pools, pool => {
|
||||
const poolMaster = find(hostsByPool[pool.id], { id: pool.master })
|
||||
isMasterOfflineByPool[pool.id] = poolMaster && poolMaster.power_state !== 'Running'
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Hosts whose toolstack hasn't been restarted since XOSAN-pack installation
|
||||
const getHostsNeedRestartByPool = createSelector(
|
||||
getHostsByPool,
|
||||
getPools,
|
||||
(hostsByPool, pools) => {
|
||||
const hostsNeedRestartByPool = {}
|
||||
forEach(pools, pool => {
|
||||
hostsNeedRestartByPool[pool.id] = filter(hostsByPool[pool.id], host =>
|
||||
host.power_state === 'Running' &&
|
||||
pool.xosanPackInstallationTime !== null &&
|
||||
pool.xosanPackInstallationTime > host.agentStartTime
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const getPoolPredicate = createSelector(
|
||||
getXosanSrs,
|
||||
srs => pool => every(srs, sr => sr.$pool !== pool.id)
|
||||
)
|
||||
|
||||
return {
|
||||
isMasterOfflineByPool: getIsMasterOfflineByPool,
|
||||
hostsNeedRestartByPool: getHostsNeedRestartByPool,
|
||||
noPacksByPool,
|
||||
poolPredicate: getPoolPredicate,
|
||||
pools: getPools,
|
||||
xosanSrs: getXosanSrs
|
||||
}
|
||||
})
|
||||
@addSubscriptions({
|
||||
catalog: subscribeResourceCatalog,
|
||||
plugins: subscribePlugins
|
||||
})
|
||||
export default class Xosan extends Component {
|
||||
componentDidMount () {
|
||||
this._subscribeVolumeInfo(this.props.xosanSrs)
|
||||
}
|
||||
|
||||
componentWillReceiveProps ({ pools, xosanSrs }) {
|
||||
if (xosanSrs !== this.props.xosanSrs) this._subscribeVolumeInfo(xosanSrs)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.unsubscribeVolumeInfo != null) this.unsubscribeVolumeInfo()
|
||||
}
|
||||
|
||||
_subscribeVolumeInfo = srs => {
|
||||
const unsubscriptions = []
|
||||
forEach(srs, sr => {
|
||||
forEach(INFO_TYPES, infoType => unsubscriptions.push(
|
||||
subscribeVolumeInfo({ sr, infoType }, info => this.setState({
|
||||
status: cowSet(this.state.status, [ sr.id, infoType ], info)
|
||||
}))
|
||||
))
|
||||
})
|
||||
this.unsubscribeVolumeInfo = () => forEach(unsubscriptions, unsubscribe => unsubscribe())
|
||||
}
|
||||
|
||||
_getError = createSelector(
|
||||
() => this.props.plugins,
|
||||
() => this.props.catalog,
|
||||
@@ -544,24 +249,62 @@ export default class Xosan extends Component {
|
||||
}
|
||||
)
|
||||
|
||||
_onSrCreationStarted = () => this.setState({ showNewXosanForm: false })
|
||||
|
||||
render () {
|
||||
const { pools, noPacksByPool, catalog } = this.props
|
||||
const { xosanSrs, noPacksByPool, hostsNeedRestart, poolPredicate } = this.props
|
||||
const error = this._getError()
|
||||
|
||||
return <Page header={HEADER} title='xosan' formatTitle>
|
||||
{process.env.XOA_PLAN < 5
|
||||
? <Container>
|
||||
{error
|
||||
? <em>{error}</em>
|
||||
: map(pools, pool => {
|
||||
const noPack = noPacksByPool && noPacksByPool[pool.id]
|
||||
|
||||
return <Collapse key={pool.id} className='mb-1' buttonText={<span>{noPack && <Icon icon='error' />} {pool.name_label}</span>}>
|
||||
<div className='m-1'>
|
||||
<Pool pool={pool} noPack={noPack} catalog={catalog} />
|
||||
</div>
|
||||
</Collapse>
|
||||
})
|
||||
? <Row>
|
||||
<Col><em>{error}</em></Col>
|
||||
</Row>
|
||||
: [
|
||||
<Row className='mb-1'>
|
||||
<Col>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
handler={this.toggleState('showNewXosanForm')}
|
||||
icon={this.state.showNewXosanForm ? 'minus' : 'plus'}
|
||||
>
|
||||
{_('xosanNew')}
|
||||
</ActionButton>
|
||||
</Col>
|
||||
</Row>,
|
||||
<Row>
|
||||
<Col>
|
||||
{this.state.showNewXosanForm && <NewXosan
|
||||
hostsNeedRestart={hostsNeedRestart}
|
||||
noPacksByPool={noPacksByPool}
|
||||
poolPredicate={poolPredicate}
|
||||
onSrCreationStarted={this._onSrCreationStarted}
|
||||
/>}
|
||||
</Col>
|
||||
</Row>,
|
||||
<Row>
|
||||
<Col>
|
||||
{map(this.props.pools, pool => <CreationProgress key={pool.id} pool={pool} />)}
|
||||
</Col>
|
||||
</Row>,
|
||||
<Row>
|
||||
<Col>
|
||||
{isEmpty(xosanSrs)
|
||||
? <em>{_('xosanNoSrs')}</em>
|
||||
: <SortedTable
|
||||
collection={xosanSrs}
|
||||
columns={XOSAN_COLUMNS}
|
||||
individualActions={XOSAN_INDIVIDUAL_ACTIONS}
|
||||
userData={{
|
||||
status: this.state.status
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
]
|
||||
}
|
||||
</Container>
|
||||
: <Container>
|
||||
|
||||
472
src/xo-app/xosan/new-xosan.js
Normal file
472
src/xo-app/xosan/new-xosan.js
Normal file
@@ -0,0 +1,472 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import getEventValue from 'get-event-value'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { Toggle, SizeInput } from 'form'
|
||||
import { SelectPif, SelectPool } from 'select-objects'
|
||||
import {
|
||||
every,
|
||||
filter,
|
||||
find,
|
||||
forEach,
|
||||
groupBy,
|
||||
isEmpty,
|
||||
keys,
|
||||
map,
|
||||
pickBy
|
||||
} from 'lodash'
|
||||
import {
|
||||
createFilter,
|
||||
createGetObjectsOfType,
|
||||
createSelector,
|
||||
createSort
|
||||
} from 'selectors'
|
||||
import {
|
||||
addSubscriptions,
|
||||
compareVersions,
|
||||
connectStore,
|
||||
formatSize,
|
||||
mapPlus
|
||||
} from 'utils'
|
||||
import {
|
||||
computeXosanPossibleOptions,
|
||||
createXosanSR,
|
||||
downloadAndInstallXosanPack,
|
||||
restartHostsAgents,
|
||||
subscribeResourceCatalog
|
||||
} from 'xo'
|
||||
|
||||
import Graph from './graph'
|
||||
|
||||
const _findLatestTemplate = templates => {
|
||||
let latestTemplate = templates[0]
|
||||
|
||||
forEach(templates, template => {
|
||||
if (compareVersions(template.version, latestTemplate.version) > 0) {
|
||||
latestTemplate = template
|
||||
}
|
||||
})
|
||||
|
||||
return latestTemplate
|
||||
}
|
||||
|
||||
const DEFAULT_BRICKSIZE = 100 * 1024 * 1024 * 1024 // 100 GiB
|
||||
const DEFAULT_MEMORY = 2 * 1024 * 1024 * 1024 // 2 GiB
|
||||
|
||||
@addSubscriptions({
|
||||
catalog: subscribeResourceCatalog
|
||||
})
|
||||
@connectStore({
|
||||
pbds: createGetObjectsOfType('PBD'),
|
||||
hosts: createGetObjectsOfType('host'),
|
||||
srs: createGetObjectsOfType('SR')
|
||||
})
|
||||
export default class NewXosan extends Component {
|
||||
state = {
|
||||
selectedSrs: {},
|
||||
brickSize: DEFAULT_BRICKSIZE,
|
||||
ipRange: '172.31.100.0',
|
||||
memorySize: DEFAULT_MEMORY
|
||||
}
|
||||
|
||||
_selectPool = pool => {
|
||||
this.setState({
|
||||
selectedSrs: {},
|
||||
brickSize: DEFAULT_BRICKSIZE,
|
||||
memorySize: DEFAULT_MEMORY,
|
||||
pif: undefined,
|
||||
pool
|
||||
})
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this._refreshSuggestions()
|
||||
}
|
||||
|
||||
// Selector that doesn't return anything but updates the suggestions only if necessary
|
||||
_refreshSuggestions = createSelector(
|
||||
() => this.state.selectedSrs,
|
||||
() => this.state.brickSize,
|
||||
() => this.state.customBrickSize,
|
||||
async (selectedSrs, brickSize, customBrickSize) => {
|
||||
this.setState({
|
||||
suggestion: 0,
|
||||
suggestions: await computeXosanPossibleOptions(
|
||||
keys(pickBy(selectedSrs)),
|
||||
customBrickSize ? brickSize : undefined
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
_getIsInPool = createSelector(
|
||||
() => this.state.pool != null && this.state.pool.id,
|
||||
poolId => obj => obj.$pool === poolId
|
||||
)
|
||||
|
||||
_getPbdsBySr = createSelector(
|
||||
() => this.props.pbds,
|
||||
pbds => groupBy(filter(pbds, this._getIsInPool), 'SR')
|
||||
)
|
||||
|
||||
_getHosts = createSelector(
|
||||
() => this.props.hosts,
|
||||
hosts => filter(hosts, this._getIsInPool)
|
||||
)
|
||||
|
||||
// LVM SRs that are connected
|
||||
_getLvmSrs = createSort(createSelector(
|
||||
createFilter(
|
||||
() => this.props.srs,
|
||||
createSelector(
|
||||
this._getHosts,
|
||||
this._getIsInPool,
|
||||
(hosts, isInPool) =>
|
||||
sr =>
|
||||
isInPool(sr) &&
|
||||
!sr.shared &&
|
||||
sr.SR_type === 'lvm' &&
|
||||
find(hosts, { id: sr.$container }).power_state === 'Running'
|
||||
)
|
||||
),
|
||||
this._getPbdsBySr,
|
||||
(srs, pbdsBySr) => mapPlus(srs, (sr, push) => {
|
||||
let pbds
|
||||
if ((pbds = pbdsBySr[sr.id]).length) {
|
||||
push({ ...sr, pbds })
|
||||
}
|
||||
})
|
||||
), 'name_label')
|
||||
|
||||
_onCustomBrickSizeChange = async event => {
|
||||
const customBrickSize = getEventValue(event)
|
||||
this.setState({ customBrickSize })
|
||||
}
|
||||
|
||||
_onBrickSizeChange = async event => {
|
||||
const brickSize = getEventValue(event)
|
||||
this.setState({ brickSize })
|
||||
}
|
||||
|
||||
_selectSr = async (event, sr) => {
|
||||
const selectedSrs = { ...this.state.selectedSrs }
|
||||
selectedSrs[sr.id] = event.target.checked
|
||||
this.setState({ selectedSrs })
|
||||
}
|
||||
|
||||
_getPifPredicate = createSelector(
|
||||
() => this.state.pool,
|
||||
pool => pif => pif.vlan === -1 && pif.$host === (pool && pool.master)
|
||||
)
|
||||
|
||||
_getNSelectedSrs = createSelector(
|
||||
() => this.state.selectedSrs,
|
||||
srs => filter(srs).length
|
||||
)
|
||||
|
||||
_getLatestTemplate = createSelector(
|
||||
createFilter(
|
||||
() => this.props.catalog && map(this.props.catalog.xosan),
|
||||
[ ({ type }) => type === 'xva' ]
|
||||
),
|
||||
_findLatestTemplate
|
||||
)
|
||||
|
||||
_getDisableSrCheckbox = createSelector(
|
||||
() => this.state.selectedSrs,
|
||||
this._getLvmSrs,
|
||||
(selectedSrs, lvmsrs) => sr =>
|
||||
!every(keys(pickBy(selectedSrs)), selectedSrId =>
|
||||
selectedSrId === sr.id ||
|
||||
find(lvmsrs, { id: selectedSrId }).$container !== sr.$container
|
||||
)
|
||||
)
|
||||
|
||||
_getDisableCreation = createSelector(
|
||||
() => this.state.suggestion,
|
||||
() => this.state.suggestions,
|
||||
() => this.state.pif,
|
||||
this._getNSelectedSrs,
|
||||
(suggestion, suggestions, pif, nSelectedSrs) =>
|
||||
!suggestions || !suggestions[suggestion] || !pif || nSelectedSrs < 2 || suggestions[suggestion].availableSpace === 0
|
||||
)
|
||||
|
||||
_createXosanVm = () => {
|
||||
const params = this.state.suggestions[this.state.suggestion]
|
||||
|
||||
if (!params) {
|
||||
return
|
||||
}
|
||||
|
||||
createXosanSR({
|
||||
template: this._getLatestTemplate(),
|
||||
pif: this.state.pif,
|
||||
vlan: this.state.vlan || 0,
|
||||
srs: keys(pickBy(this.state.selectedSrs)),
|
||||
glusterType: params.layout,
|
||||
redundancy: params.redundancy,
|
||||
brickSize: this.state.customBrickSize ? this.state.brickSize : undefined,
|
||||
memorySize: this.state.memorySize,
|
||||
ipRange: this.state.customIpRange ? this.state.ipRange : undefined
|
||||
})
|
||||
|
||||
this.props.onSrCreationStarted()
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
brickSize,
|
||||
customBrickSize,
|
||||
customIpRange,
|
||||
ipRange,
|
||||
memorySize,
|
||||
pif,
|
||||
pool,
|
||||
selectedSrs,
|
||||
suggestion,
|
||||
suggestions,
|
||||
useVlan,
|
||||
vlan
|
||||
} = this.state
|
||||
|
||||
const {
|
||||
hostsNeedRestart,
|
||||
noPacksByPool,
|
||||
poolPredicate
|
||||
} = this.props
|
||||
|
||||
const lvmsrs = this._getLvmSrs()
|
||||
const hosts = this._getHosts()
|
||||
|
||||
const disableSrCheckbox = this._getDisableSrCheckbox()
|
||||
|
||||
return <Container>
|
||||
<Row className='mb-1'>
|
||||
<Col size={4}>
|
||||
<SelectPool
|
||||
onChange={this._selectPool}
|
||||
predicate={poolPredicate}
|
||||
value={pool}
|
||||
/>
|
||||
</Col>
|
||||
<Col size={4}>
|
||||
<SelectPif
|
||||
disabled={pool == null || noPacksByPool[pool.id] || !isEmpty(hostsNeedRestart)}
|
||||
onChange={this.linkState('pif')}
|
||||
predicate={this._getPifPredicate()}
|
||||
value={pif}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{pool != null && noPacksByPool[pool.id] && <Row>
|
||||
<Icon icon='error' /> {_('xosanNeedPack')}
|
||||
<br />
|
||||
<ActionButton
|
||||
btnStyle='success'
|
||||
handler={downloadAndInstallXosanPack}
|
||||
handlerParam={pool}
|
||||
icon='export'
|
||||
>
|
||||
{_('xosanInstallIt')}
|
||||
</ActionButton>
|
||||
</Row>}
|
||||
{!isEmpty(hostsNeedRestart) && <Row>
|
||||
<Icon icon='error' /> {_('xosanNeedRestart')}
|
||||
<br />
|
||||
<ActionButton
|
||||
btnStyle='success'
|
||||
handler={restartHostsAgents}
|
||||
handlerParam={hostsNeedRestart}
|
||||
icon='host-restart-agent'
|
||||
>
|
||||
{_('xosanRestartAgents')}
|
||||
</ActionButton>
|
||||
</Row>}
|
||||
{pool != null && !noPacksByPool[pool.id] && isEmpty(hostsNeedRestart) && [
|
||||
<Row>
|
||||
<em>{_('xosanSelect2Srs')}</em>
|
||||
<table className='table table-striped'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>{_('xosanName')}</th>
|
||||
<th>{_('xosanHost')}</th>
|
||||
<th>{_('xosanSize')}</th>
|
||||
<th>{_('xosanUsedSpace')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(lvmsrs, sr => {
|
||||
const host = find(hosts, [ 'id', sr.$container ])
|
||||
|
||||
return <tr key={sr.id}>
|
||||
<td>
|
||||
<input
|
||||
checked={selectedSrs[sr.id] || false}
|
||||
disabled={disableSrCheckbox(sr)}
|
||||
onChange={event => this._selectSr(event, sr)}
|
||||
type='checkbox'
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/srs/${sr.id}/general`}>{sr.name_label}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/hosts/${host.id}/general`}>{host.name_label}</Link>
|
||||
</td>
|
||||
<td>
|
||||
{formatSize(sr.size)}
|
||||
</td>
|
||||
<td>
|
||||
{sr.size > 0 &&
|
||||
<Tooltip content={_('spaceLeftTooltip', {
|
||||
used: String(Math.round((sr.physical_usage / sr.size) * 100)),
|
||||
free: formatSize(sr.size - sr.physical_usage)
|
||||
})}>
|
||||
<progress
|
||||
className='progress'
|
||||
max='100'
|
||||
value={(sr.physical_usage / sr.size) * 100}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</Row>,
|
||||
<Row>
|
||||
{!isEmpty(suggestions) && <div>
|
||||
<h3>{_('xosanSuggestions')}</h3>
|
||||
<table className='table table-striped'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>{_('xosanLayout')}</th>
|
||||
<th>{_('xosanRedundancy')}</th>
|
||||
<th>{_('xosanCapacity')}</th>
|
||||
<th>{_('xosanAvailableSpace')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(suggestions, ({ layout, redundancy, capacity, availableSpace }, index) => <tr key={index}>
|
||||
<td>
|
||||
<input
|
||||
checked={+suggestion === index}
|
||||
name={`suggestion_${pool.id}`}
|
||||
onChange={this.linkState('suggestion')}
|
||||
type='radio'
|
||||
value={index}
|
||||
/>
|
||||
</td>
|
||||
<td>{layout}</td>
|
||||
<td>{redundancy}</td>
|
||||
<td>{capacity}</td>
|
||||
<td>{
|
||||
availableSpace === 0
|
||||
? <strong className='text-danger'>0</strong>
|
||||
: formatSize(availableSpace)
|
||||
}</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
<Graph
|
||||
height={160}
|
||||
layout={suggestions[suggestion].layout}
|
||||
nSrs={this._getNSelectedSrs()}
|
||||
redundancy={suggestions[suggestion].redundancy}
|
||||
width={600}
|
||||
/>
|
||||
<hr />
|
||||
<Toggle
|
||||
onChange={this.toggleState('showAdvanced')}
|
||||
value={this.state.showAdvanced}
|
||||
/> {_('xosanAdvanced')}
|
||||
{' '}
|
||||
{this.state.showAdvanced && <Container className='mb-1'>
|
||||
<SingleLineRow>
|
||||
<Col>{_('xosanVlan')}</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow>
|
||||
<Col size={1}>
|
||||
<Toggle onChange={this.linkState('useVlan')} value={useVlan} />
|
||||
</Col>
|
||||
<Col size={3}>
|
||||
<input
|
||||
className='form-control'
|
||||
disabled={!useVlan}
|
||||
onChange={this.linkState('vlan')}
|
||||
placeholder='VLAN'
|
||||
type='text'
|
||||
value={vlan}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow>
|
||||
<Col>{_('xosanCustomIpNetwork')}</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow>
|
||||
<Col size={1}>
|
||||
<Toggle onChange={this.linkState('customIpRange')} value={customIpRange} />
|
||||
</Col>
|
||||
<Col size={3}>
|
||||
<input
|
||||
className='form-control'
|
||||
disabled={!customIpRange}
|
||||
onChange={this.linkState('ipRange')}
|
||||
placeholder='ipRange'
|
||||
type='text'
|
||||
value={ipRange}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow>
|
||||
<Col>{_('xosanBrickSize')}</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow>
|
||||
<Col size={1}>
|
||||
<Toggle className='mr-1' onChange={this._onCustomBrickSizeChange} value={customBrickSize} />
|
||||
</Col>
|
||||
<Col size={3}>
|
||||
<SizeInput
|
||||
readOnly={!customBrickSize}
|
||||
value={brickSize}
|
||||
onChange={this._onBrickSizeChange}
|
||||
required
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow>
|
||||
<Col size={4}>
|
||||
<label>{_('xosanMemorySize')}</label>
|
||||
<SizeInput value={memorySize} onChange={this.linkState('memorySize')} required />
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</Container>}
|
||||
<hr />
|
||||
</div>}
|
||||
</Row>,
|
||||
<Row>
|
||||
<Col>
|
||||
<ActionButton
|
||||
btnStyle='success'
|
||||
disabled={this._getDisableCreation()}
|
||||
handler={this._createXosanVm}
|
||||
icon='add'
|
||||
>
|
||||
{_('xosanCreate')}
|
||||
</ActionButton>
|
||||
</Col>
|
||||
</Row>
|
||||
]}
|
||||
<hr />
|
||||
</Container>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user