Compare commits
123 Commits
xo-web/v5.
...
exposeVMAp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1083aba33c | ||
|
|
d90156ff15 | ||
|
|
45b6e010df | ||
|
|
1fbdb7799e | ||
|
|
3050fd7ce1 | ||
|
|
1988934f8a | ||
|
|
5331ced4fe | ||
|
|
74066f7d44 | ||
|
|
98e3aa89bb | ||
|
|
070bb65740 | ||
|
|
4e51959e3b | ||
|
|
b5ff695c74 | ||
|
|
f879e2ace0 | ||
|
|
9aeabce4d8 | ||
|
|
2f17cd4ba9 | ||
|
|
60e70a08c1 | ||
|
|
6e0ba2bae3 | ||
|
|
feb996890e | ||
|
|
a06bc85142 | ||
|
|
f530aef92b | ||
|
|
2eb7330335 | ||
|
|
89157e7b7e | ||
|
|
3834e2ef91 | ||
|
|
a711231955 | ||
|
|
0a5e301b3e | ||
|
|
c82b9893c5 | ||
|
|
f4dfabc34c | ||
|
|
059521aeda | ||
|
|
debca09e2c | ||
|
|
0699cfc449 | ||
|
|
f0d85f4c4e | ||
|
|
1801f9cb06 | ||
|
|
4be018ad15 | ||
|
|
5dcf060975 | ||
|
|
59f6b1f0c8 | ||
|
|
ae38a85b19 | ||
|
|
324dbbcfc8 | ||
|
|
9770b77df4 | ||
|
|
0f91de389a | ||
|
|
7f5a623b37 | ||
|
|
c7cf73ff05 | ||
|
|
4aab425cef | ||
|
|
0d9666639f | ||
|
|
6c26c09685 | ||
|
|
819f650b48 | ||
|
|
353eba6365 | ||
|
|
063302b91d | ||
|
|
562b51bc2f | ||
|
|
e33a6f9a05 | ||
|
|
b9db4e7704 | ||
|
|
3270d9c3a7 | ||
|
|
6d7399f96c | ||
|
|
886ef87bc5 | ||
|
|
1e5dc9efe7 | ||
|
|
28ec66bf3b | ||
|
|
9199784a23 | ||
|
|
c7e447db6f | ||
|
|
f81615f8b6 | ||
|
|
12caceb02b | ||
|
|
30f71ab444 | ||
|
|
fe04481ca3 | ||
|
|
7766e8edcd | ||
|
|
31d417c9d3 | ||
|
|
5ed29197cf | ||
|
|
ff5f3e12d3 | ||
|
|
240180405c | ||
|
|
edca6495fc | ||
|
|
8a9b753b01 | ||
|
|
445fc696c9 | ||
|
|
492e2362be | ||
|
|
1acee209be | ||
|
|
6785c48709 | ||
|
|
808e674503 | ||
|
|
6b2650282d | ||
|
|
475be2ee30 | ||
|
|
12e1da4ef2 | ||
|
|
780d072bb7 | ||
|
|
f7e5a5cf92 | ||
|
|
3574c8de5c | ||
|
|
b09ab4d403 | ||
|
|
1997f4af51 | ||
|
|
347cd063a3 | ||
|
|
74a4519a33 | ||
|
|
20acf7cfb2 | ||
|
|
99bc34b2da | ||
|
|
f65b5e3ddd | ||
|
|
dc10492b84 | ||
|
|
6f7c10537b | ||
|
|
7f503cfc21 | ||
|
|
9dbef0c20a | ||
|
|
923166b4e3 | ||
|
|
b420128e40 | ||
|
|
7776a6ce23 | ||
|
|
8db949734a | ||
|
|
bb5bdfb9b2 | ||
|
|
9fac3ecd81 | ||
|
|
8a84cc2627 | ||
|
|
61179ec67d | ||
|
|
59fc5955ba | ||
|
|
e853ba6244 | ||
|
|
fb40ae7264 | ||
|
|
f629047be2 | ||
|
|
278d8adf1b | ||
|
|
87087d55aa | ||
|
|
46e95fe7eb | ||
|
|
090c9ea4d7 | ||
|
|
647eb7299e | ||
|
|
027652e80a | ||
|
|
185d380c36 | ||
|
|
9008b5c4e7 | ||
|
|
f5ad59803e | ||
|
|
81d1d7ba13 | ||
|
|
3328e71805 | ||
|
|
d7e3dbac26 | ||
|
|
905182bf2e | ||
|
|
a0146290ee | ||
|
|
173aa22432 | ||
|
|
9e5b871ebe | ||
|
|
8824ce55ec | ||
|
|
155edf5533 | ||
|
|
6d06e1f89d | ||
|
|
6d1e2c47d3 | ||
|
|
8b9b0346cb |
65
CHANGELOG.md
65
CHANGELOG.md
@@ -1,5 +1,68 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.9.0** (2017-05-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Allow DR to remove previous backup first [\#2157](https://github.com/vatesfr/xo-web/issues/2157)
|
||||
- Feature request - add amount of RAM to memory bars [\#2149](https://github.com/vatesfr/xo-web/issues/2149)
|
||||
- Make the acceptability of invalid certificates configurable [\#2138](https://github.com/vatesfr/xo-web/issues/2138)
|
||||
- label of VM names in tasks link [\#2135](https://github.com/vatesfr/xo-web/issues/2135)
|
||||
- Backup report timezone [\#2133](https://github.com/vatesfr/xo-web/issues/2133)
|
||||
- xo-server-recover-account [\#2129](https://github.com/vatesfr/xo-web/issues/2129)
|
||||
- Detect disks attached to control domain [\#2126](https://github.com/vatesfr/xo-web/issues/2126)
|
||||
- Add task description in Tasks view [\#2125](https://github.com/vatesfr/xo-web/issues/2125)
|
||||
- Host reboot warning after patching for 7.1 [\#2124](https://github.com/vatesfr/xo-web/issues/2124)
|
||||
- Continuous Replication - possibility run VM without a clone [\#2119](https://github.com/vatesfr/xo-web/issues/2119)
|
||||
- Unreachable host should be detected [\#2099](https://github.com/vatesfr/xo-web/issues/2099)
|
||||
- Orange icon when host is is disabled [\#2098](https://github.com/vatesfr/xo-web/issues/2098)
|
||||
- Enhanced backup report logs [\#2096](https://github.com/vatesfr/xo-web/issues/2096)
|
||||
- Only show failures when configured to report on failures [\#2095](https://github.com/vatesfr/xo-web/issues/2095)
|
||||
- "Add all" button in self service [\#2081](https://github.com/vatesfr/xo-web/issues/2081)
|
||||
- Patch and pack mechanism changed on Ely [\#2058](https://github.com/vatesfr/xo-web/issues/2058)
|
||||
- Tip or ask people to patch from pool view [\#2057](https://github.com/vatesfr/xo-web/issues/2057)
|
||||
- File restore - Remind compatible backup [\#1930](https://github.com/vatesfr/xo-web/issues/1930)
|
||||
- Reporting for halted vm time [\#1613](https://github.com/vatesfr/xo-web/issues/1613)
|
||||
- Add standalone XS server to a pool and patch it to the pool level [\#878](https://github.com/vatesfr/xo-web/issues/878)
|
||||
- Add Cores-per-sockets [\#130](https://github.com/vatesfr/xo-web/issues/130)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- VM creation is broken for non-admins [\#2168](https://github.com/vatesfr/xo-web/issues/2168)
|
||||
- Can't create cloud config drive [\#2162](https://github.com/vatesfr/xo-web/issues/2162)
|
||||
- Select is "moving" [\#2142](https://github.com/vatesfr/xo-web/issues/2142)
|
||||
- Select issue for affinity host [\#2141](https://github.com/vatesfr/xo-web/issues/2141)
|
||||
- Dashboard Storage Usage incorrect [\#2123](https://github.com/vatesfr/xo-web/issues/2123)
|
||||
- Detect unmerged *base copy* and prevent too long chains [\#2047](https://github.com/vatesfr/xo-web/issues/2047)
|
||||
|
||||
|
||||
## **5.8.0** (2017-04-28)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Limit About view info for non-admins [\#2109](https://github.com/vatesfr/xo-web/issues/2109)
|
||||
- Enabling/disabling boot device on HVM VM [\#2105](https://github.com/vatesfr/xo-web/issues/2105)
|
||||
- Filter: Hide snapshots in SR disk view [\#2102](https://github.com/vatesfr/xo-web/issues/2102)
|
||||
- Smarter XOSAN install [\#2084](https://github.com/vatesfr/xo-web/issues/2084)
|
||||
- PL translation [\#2079](https://github.com/vatesfr/xo-web/issues/2079)
|
||||
- Remove the "share this VM" option if not in self service [\#2061](https://github.com/vatesfr/xo-web/issues/2061)
|
||||
- "connected" status graphics are not the same on the host storage and networking tabs [\#2060](https://github.com/vatesfr/xo-web/issues/2060)
|
||||
- Ability to view and edit `vga` and `videoram` fields in VM view [\#158](https://github.com/vatesfr/xo-web/issues/158)
|
||||
- Performances [\#1](https://github.com/vatesfr/xen-api/issues/1)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Dashboard display issues [\#2108](https://github.com/vatesfr/xo-web/issues/2108)
|
||||
- Dashboard CPUs Usage [\#2105](https://github.com/vatesfr/xo-web/issues/2105)
|
||||
- [Dashboard/Overview] Warning [\#2090](https://github.com/vatesfr/xo-web/issues/2090)
|
||||
- VM creation displays all networks [\#2086](https://github.com/vatesfr/xo-web/issues/2086)
|
||||
- Cannot change HA mode for a VM [\#2080](https://github.com/vatesfr/xo-web/issues/2080)
|
||||
- [Smart backup] Tags selection does not work [\#2077](https://github.com/vatesfr/xo-web/issues/2077)
|
||||
- [Backup jobs] Timeout should be in seconds, not milliseconds [\#2076](https://github.com/vatesfr/xo-web/issues/2076)
|
||||
- Missing VM templates [\#2075](https://github.com/vatesfr/xo-web/issues/2075)
|
||||
- [transport-email] From header not set [\#2074](https://github.com/vatesfr/xo-web/issues/2074)
|
||||
- Missing objects should be displayed in backup edition [\#2052](https://github.com/vatesfr/xo-web/issues/2052)
|
||||
|
||||
## **5.7.0** (2017-03-31)
|
||||
|
||||
### Enhancements
|
||||
@@ -258,7 +321,7 @@ File level restore.
|
||||
- Tooltip on OS icon in VM view [\#1416](https://github.com/vatesfr/xo-web/issues/1416)
|
||||
- Display pool master [\#1407](https://github.com/vatesfr/xo-web/issues/1407)
|
||||
- Missing tooltips in VM creation view [\#1402](https://github.com/vatesfr/xo-web/issues/1402)
|
||||
- Handle VDB disconnect and connect [\#1397](https://github.com/vatesfr/xo-web/issues/1397)
|
||||
- Handle VBD disconnect and connect [\#1397](https://github.com/vatesfr/xo-web/issues/1397)
|
||||
- Eject host from a pool [\#1395](https://github.com/vatesfr/xo-web/issues/1395)
|
||||
- Improve pool general view [\#1393](https://github.com/vatesfr/xo-web/issues/1393)
|
||||
- Improve patching system [\#1392](https://github.com/vatesfr/xo-web/issues/1392)
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
})
|
||||
|
||||
29
package.json
29
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.7.9",
|
||||
"version": "5.9.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -61,13 +61,13 @@
|
||||
"dependency-check": "^2.5.1",
|
||||
"enzyme": "^2.6.0",
|
||||
"enzyme-to-json": "^1.4.4",
|
||||
"event-to-promise": "^0.7.0",
|
||||
"event-to-promise": "^0.8.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"font-mfizz": "github:fizzed/font-mfizz",
|
||||
"get-stream": "^2.3.0",
|
||||
"globby": "^6.0.0",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-autoprefixer": "^3.1.0",
|
||||
"gulp-autoprefixer": "^4.0.0",
|
||||
"gulp-csso": "^3.0.0",
|
||||
"gulp-embedlr": "^0.5.2",
|
||||
"gulp-plumber": "^1.1.0",
|
||||
@@ -75,13 +75,13 @@
|
||||
"gulp-refresh": "^1.1.0",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"gulp-sourcemaps": "^2.2.3",
|
||||
"gulp-uglify": "^2.0.0",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"gulp-watch": "^4.3.5",
|
||||
"human-format": "^0.7.0",
|
||||
"human-format": "^0.8.0",
|
||||
"husky": "^0.13.1",
|
||||
"index-modules": "^0.3.0",
|
||||
"is-ip": "^1.0.0",
|
||||
"jest": "^19.0.2",
|
||||
"jest": "^20.0.4",
|
||||
"jsonrpc-websocket-client": "^0.1.1",
|
||||
"kindof": "^2.0.0",
|
||||
"later": "^1.2.0",
|
||||
@@ -89,20 +89,20 @@
|
||||
"loose-envify": "^1.1.0",
|
||||
"make-error": "^1.2.1",
|
||||
"marked": "^0.3.5",
|
||||
"modular-css": "^4.1.1",
|
||||
"modular-css": "^5.1.6",
|
||||
"moment": "^2.13.0",
|
||||
"moment-timezone": "^0.5.4",
|
||||
"notifyjs": "^3.0.0",
|
||||
"novnc-node": "^0.5.3",
|
||||
"promise-toolbox": "^0.8.0",
|
||||
"promise-toolbox": "^0.9.4",
|
||||
"random-password": "^0.1.2",
|
||||
"react": "^15.4.1",
|
||||
"react-addons-shallow-compare": "^15.1.0",
|
||||
"react-addons-test-utils": "^15.4.1",
|
||||
"react-bootstrap-4": "^0.29.1",
|
||||
"react-chartist": "^0.12.0",
|
||||
"react-copy-to-clipboard": "^4.0.2",
|
||||
"react-debounce-input": "^2.4.0",
|
||||
"react-copy-to-clipboard": "^5.0.0",
|
||||
"react-debounce-input": "^3.0.0",
|
||||
"react-dnd": "^2.1.4",
|
||||
"react-dnd-html5-backend": "^2.1.2",
|
||||
"react-document-title": "^2.0.2",
|
||||
@@ -114,7 +114,7 @@
|
||||
"react-overlays": "^0.6.0",
|
||||
"react-redux": "^5.0.0",
|
||||
"react-router": "^3.0.0",
|
||||
"react-select": "^1.0.0-rc.3",
|
||||
"react-select": "^1.0.0-rc.4",
|
||||
"react-shortcuts": "^1.3.1",
|
||||
"react-sparklines": "^1.5.0",
|
||||
"react-virtualized": "^8.0.8",
|
||||
@@ -124,18 +124,19 @@
|
||||
"redux-devtools-dock-monitor": "^1.1.0",
|
||||
"redux-devtools-log-monitor": "^1.0.5",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.2.1",
|
||||
"reselect": "^2.5.4",
|
||||
"semver": "^5.3.0",
|
||||
"standard": "^10.0.0",
|
||||
"styled-components": "^1.4.4",
|
||||
"styled-components": "^2.1.0",
|
||||
"superagent": "^3.5.0",
|
||||
"tar-stream": "^1.5.2",
|
||||
"uglify-es": "^3.0.18",
|
||||
"uncontrollable-input": "^0.0.1",
|
||||
"vinyl": "^2.0.0",
|
||||
"watchify": "^3.7.0",
|
||||
"xml2js": "^0.4.17",
|
||||
"xo-acl-resolver": "^0.2.3",
|
||||
"xo-common": "0.1.0",
|
||||
"xo-common": "^0.1.1",
|
||||
"xo-lib": "^0.8.0",
|
||||
"xo-remote-parser": "^0.3"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import Button from './button'
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import logError from './log-error'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import Tooltip from './tooltip'
|
||||
import { error as _error } from './notification'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const ActionToggle = ({ className, value, ...props }) =>
|
||||
<ActionButton
|
||||
|
||||
@@ -54,20 +54,20 @@ export default class BaseComponent extends PureComponent {
|
||||
|
||||
// See https://preactjs.com/guide/linked-state
|
||||
linkState (name, targetPath) {
|
||||
const key = targetPath
|
||||
const key = targetPath !== undefined
|
||||
? `${name}##${targetPath}`
|
||||
: name
|
||||
|
||||
let linkedState = this._linkedState
|
||||
let cb
|
||||
if (!linkedState) {
|
||||
if (linkedState === null) {
|
||||
linkedState = this._linkedState = {}
|
||||
} else if ((cb = linkedState[key])) {
|
||||
} else if ((cb = linkedState[key]) !== undefined) {
|
||||
return cb
|
||||
}
|
||||
|
||||
let getValue
|
||||
if (targetPath) {
|
||||
if (targetPath !== undefined) {
|
||||
const path = targetPath.split('.')
|
||||
getValue = event => get(getEventValue(event), path, 0)
|
||||
} else {
|
||||
@@ -91,9 +91,9 @@ export default class BaseComponent extends PureComponent {
|
||||
toggleState (name) {
|
||||
let linkedState = this._linkedState
|
||||
let cb
|
||||
if (!linkedState) {
|
||||
if (linkedState === null) {
|
||||
linkedState = this._linkedState = {}
|
||||
} else if ((cb = linkedState[name])) {
|
||||
} else if ((cb = linkedState[name]) !== undefined) {
|
||||
return cb
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const Button = ({
|
||||
active,
|
||||
@@ -27,7 +27,7 @@ const Button = ({
|
||||
return <button {...props}>{children}</button>
|
||||
}
|
||||
|
||||
propTypes(Button)({
|
||||
propTypes({
|
||||
active: propTypes.bool,
|
||||
block: propTypes.bool,
|
||||
|
||||
@@ -51,6 +51,6 @@ propTypes(Button)({
|
||||
'large',
|
||||
'small'
|
||||
])
|
||||
})
|
||||
})(Button)
|
||||
|
||||
export { Button as default }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const CARD_STYLE = {
|
||||
minHeight: '100%'
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
@propTypes({
|
||||
children: propTypes.any.isRequired,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from 'react-bootstrap-4/lib'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
@uncontrollableInput({
|
||||
defaultValue: ''
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, { createElement } from 'react'
|
||||
import _ from '../intl'
|
||||
import Button from '../button'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
128
src/common/drag-n-drop-order.js
Normal file
128
src/common/drag-n-drop-order.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import HTML5Backend from 'react-dnd-html5-backend'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import { DragDropContext, DragSource, DropTarget } from 'react-dnd'
|
||||
import { Toggle } from 'form'
|
||||
|
||||
const orderItemSource = {
|
||||
beginDrag: props => ({
|
||||
id: props.id,
|
||||
index: props.index
|
||||
})
|
||||
}
|
||||
|
||||
const orderItemTarget = {
|
||||
hover: (props, monitor, component) => {
|
||||
const dragIndex = monitor.getItem().index
|
||||
const hoverIndex = props.index
|
||||
if (dragIndex === hoverIndex) {
|
||||
return
|
||||
}
|
||||
props.move(dragIndex, hoverIndex)
|
||||
monitor.getItem().index = hoverIndex
|
||||
}
|
||||
}
|
||||
|
||||
@DropTarget('orderItem', orderItemTarget, connect => ({
|
||||
connectDropTarget: connect.dropTarget()
|
||||
}))
|
||||
@DragSource('orderItem', orderItemSource, (connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging()
|
||||
}))
|
||||
@propTypes({
|
||||
connectDragSource: propTypes.func.isRequired,
|
||||
connectDropTarget: propTypes.func.isRequired,
|
||||
index: propTypes.number.isRequired,
|
||||
isDragging: propTypes.bool.isRequired,
|
||||
id: propTypes.any.isRequired,
|
||||
item: propTypes.object.isRequired,
|
||||
move: propTypes.func.isRequired
|
||||
})
|
||||
class OrderItem extends Component {
|
||||
_toggle = checked => {
|
||||
const { item } = this.props
|
||||
item.active = checked
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { item, connectDragSource, connectDropTarget, toggle } = this.props
|
||||
return connectDragSource(connectDropTarget(
|
||||
<li className='list-group-item'>
|
||||
<Icon icon='grab' />
|
||||
{' '}
|
||||
<Icon icon='grab' />
|
||||
{' '}
|
||||
{item.text}
|
||||
{toggle && <span className='pull-right'>
|
||||
<Toggle value={item.active} onChange={this._toggle} />
|
||||
</span>}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
onClose: propTypes.func
|
||||
})
|
||||
@DragDropContext(HTML5Backend)
|
||||
export default class DragNDropOrder extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
const { parseOrderParam, parseOrder } = props
|
||||
this.state = parseOrder(parseOrderParam)
|
||||
}
|
||||
|
||||
_moveOrderItem = (dragIndex, hoverIndex) => {
|
||||
const order = this.state.order.slice()
|
||||
const dragItem = order.splice(dragIndex, 1)
|
||||
if (dragItem.length) {
|
||||
order.splice(hoverIndex, 0, dragItem.pop())
|
||||
this.setState({order})
|
||||
}
|
||||
}
|
||||
|
||||
_reset = () => {
|
||||
const { parseOrderParam, parseOrder } = this.props
|
||||
this.state = this.setState(parseOrder(parseOrderParam))
|
||||
}
|
||||
|
||||
_save = () => {
|
||||
const { order, toggleActive } = this.state
|
||||
this.props.setOrder(this.props.parseOrderParam, order, toggleActive)
|
||||
}
|
||||
|
||||
_toggleOnChange = event => this.setState({toggleActive: event})
|
||||
|
||||
render () {
|
||||
const { order, toggleActive } = this.state
|
||||
const { toggleItems } = this.props
|
||||
return <form>
|
||||
{!toggleItems && <Toggle value={toggleActive} onChange={this._toggleOnChange} />}
|
||||
<ul>
|
||||
{map(order, (item, index) => <OrderItem
|
||||
toggle={toggleItems}
|
||||
key={index}
|
||||
index={index}
|
||||
id={item.id}
|
||||
// FIXME missing translation
|
||||
item={item}
|
||||
move={this._moveOrderItem}
|
||||
/>)}
|
||||
</ul>
|
||||
<fieldset className='form-inline'>
|
||||
<span className='pull-right'>
|
||||
<ActionButton icon='save' btnStyle='primary' handler={this._save}>{_('saveBootOption')}</ActionButton>
|
||||
{' '}
|
||||
<ActionButton icon='reset' handler={this._reset}>{_('resetBootOption')}</ActionButton>
|
||||
</span>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import Component from 'base-component'
|
||||
import propTypes from 'prop-types'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import ReactDropzone from 'react-dropzone'
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
import Icon from '../icon'
|
||||
import logError from '../log-error'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import Tooltip from '../tooltip'
|
||||
import { formatSize } from '../utils'
|
||||
import { SizeInput } from '../form'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import * as Grid from './grid'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
export const LabelCol = propTypes({
|
||||
children: propTypes.any.isRequired
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import Button from '../button'
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import {
|
||||
firstDefined,
|
||||
formatSizeRaw,
|
||||
|
||||
@@ -4,7 +4,7 @@ import find from 'lodash/find'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
import Select from './select'
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
List
|
||||
} from 'react-virtualized'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
const SELECT_MENU_STYLE = {
|
||||
overflow: 'hidden'
|
||||
|
||||
@@ -2,11 +2,9 @@ import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
|
||||
import Component from '../../base-component'
|
||||
import Icon from '../../icon'
|
||||
import propTypes from '../../prop-types'
|
||||
|
||||
import styles from './index.css'
|
||||
import Component from '../base-component'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
@uncontrollableInput()
|
||||
@propTypes({
|
||||
@@ -25,30 +23,24 @@ export default class Toggle extends Component {
|
||||
iconSize: 2
|
||||
}
|
||||
|
||||
_onChange = event => this.props.onChange(event.target.checked)
|
||||
_toggle = () => {
|
||||
const { props } = this
|
||||
props.onChange(!props.value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
|
||||
return (
|
||||
<label
|
||||
<Icon
|
||||
className={classNames(
|
||||
props.disabled ? 'text-muted' : props.value ? 'text-success' : null,
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
icon={props.icon || (props.value ? props.iconOn : props.iconOff)}
|
||||
size={props.iconSize}
|
||||
/>
|
||||
<input
|
||||
checked={props.value || false}
|
||||
className={styles.checkbox}
|
||||
disabled={props.disabled}
|
||||
onChange={this._onChange}
|
||||
type='checkbox'
|
||||
/>
|
||||
</label>
|
||||
icon={props.icon || (props.value ? props.iconOn : props.iconOff)}
|
||||
onClick={this._toggle}
|
||||
size={props.iconSize}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.checkbox {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
export const Col = propTypes({
|
||||
className: propTypes.string,
|
||||
|
||||
@@ -31,3 +31,8 @@ export const SR = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const vmGroup = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import Tags from './tags'
|
||||
import { createString, createProperty, toString } from './complex-matcher'
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import ActionButton from './action-button'
|
||||
import Component from './base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Link from './link'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import SortedTable from './sorted-table'
|
||||
import TabButton from './tab-button'
|
||||
import { connectStore } from './utils'
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
createSelector
|
||||
} from './selectors'
|
||||
import {
|
||||
getHostMissingPatches,
|
||||
installAllHostPatches,
|
||||
installAllPatchesOnPool
|
||||
installAllPatchesOnPool,
|
||||
subscribeHostMissingPatches
|
||||
} from './xo'
|
||||
|
||||
// ===================================================================
|
||||
@@ -89,11 +89,25 @@ class HostsPatchesTable extends Component {
|
||||
)
|
||||
)
|
||||
|
||||
_refreshMissingPatches = () => (
|
||||
Promise.all(
|
||||
map(this.props.hosts, this._refreshHostMissingPatches)
|
||||
_subscribeMissingPatches = (hosts = this.props.hosts) => {
|
||||
const unsubs = map(hosts, host =>
|
||||
subscribeHostMissingPatches(
|
||||
host,
|
||||
patches => this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[host.id]: patches.length
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if (this.unsubscribeMissingPatches !== undefined) {
|
||||
this.unsubscribeMissingPatches()
|
||||
}
|
||||
|
||||
this.unsubscribeMissingPatches = () => forEach(unsubs, unsub => unsub())
|
||||
}
|
||||
|
||||
_installAllMissingPatches = () => {
|
||||
const pools = {}
|
||||
@@ -104,100 +118,69 @@ class HostsPatchesTable extends Component {
|
||||
return Promise.all(map(
|
||||
keys(pools),
|
||||
installAllPatchesOnPool
|
||||
)).then(this._refreshMissingPatches)
|
||||
}
|
||||
|
||||
_refreshHostMissingPatches = host => (
|
||||
getHostMissingPatches(host).then(patches => {
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[host.id]: patches.length
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
_installAllHostPatches = host => (
|
||||
installAllHostPatches(host).then(() =>
|
||||
this._refreshHostMissingPatches(host)
|
||||
)
|
||||
)
|
||||
|
||||
componentWillMount () {
|
||||
this._refreshMissingPatches()
|
||||
))
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
// Force one Portal refresh.
|
||||
// Because Portal cannot see the container reference at first rendering.
|
||||
this.forceUpdate()
|
||||
this._subscribeMissingPatches()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
forEach(nextProps.hosts, host => {
|
||||
const { id } = host
|
||||
if (nextProps.hosts !== this.props.hosts) {
|
||||
this._subscribeMissingPatches(nextProps.hosts)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.missingPatches[id] !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[id]: 0
|
||||
}
|
||||
})
|
||||
|
||||
this._refreshHostMissingPatches(host)
|
||||
})
|
||||
componentWillUnmount () {
|
||||
this.unsubscribeMissingPatches()
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
buttonsGroupContainer,
|
||||
container,
|
||||
displayPools,
|
||||
pools,
|
||||
useTabButton
|
||||
} = this.props
|
||||
|
||||
const hosts = this._getHosts()
|
||||
const noPatches = isEmpty(hosts)
|
||||
const { props } = this
|
||||
|
||||
const Container = props.container || 'div'
|
||||
const Container = container || 'div'
|
||||
|
||||
const Button = this.props.useTabButton
|
||||
const Button = useTabButton
|
||||
? TabButton
|
||||
: ActionButton_
|
||||
|
||||
const Buttons = (
|
||||
<Container>
|
||||
<Button
|
||||
handler={this._refreshMissingPatches}
|
||||
icon='refresh'
|
||||
labelId='checkForUpdates'
|
||||
/>
|
||||
<Button
|
||||
btnStyle='primary'
|
||||
disabled={noPatches}
|
||||
handler={this._installAllMissingPatches}
|
||||
icon='host-patch-update'
|
||||
labelId='installPoolPatches'
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!noPatches
|
||||
? (
|
||||
<SortedTable
|
||||
collection={hosts}
|
||||
columns={props.displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
|
||||
columns={displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
|
||||
userData={{
|
||||
installAllHostPatches: this._installAllHostPatches,
|
||||
installAllHostPatches,
|
||||
missingPatches: this.state.missingPatches,
|
||||
pools: props.pools
|
||||
pools
|
||||
}}
|
||||
/>
|
||||
) : <p>{_('patchNothing')}</p>
|
||||
}
|
||||
<Portal container={() => props.buttonsGroupContainer()}>
|
||||
{Buttons}
|
||||
<Portal container={() => buttonsGroupContainer()}>
|
||||
<Container>
|
||||
<Button
|
||||
btnStyle='primary'
|
||||
disabled={noPatches}
|
||||
handler={this._installAllMissingPatches}
|
||||
icon='host-patch-update'
|
||||
labelId='installPoolPatches'
|
||||
/>
|
||||
</Container>
|
||||
</Portal>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import classNames from 'classnames'
|
||||
import isInteger from 'lodash/isInteger'
|
||||
import React, { PropTypes } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
const Icon = ({ className, icon, size = 1, fixedWidth }) => (
|
||||
<i className={classNames(
|
||||
className,
|
||||
icon ? `xo-icon-${icon}` : 'fa', // Without icon prop, is a placeholder.
|
||||
isInteger(size) ? `fa-${size}x` : `fa-${size}`,
|
||||
fixedWidth && 'fa-fw'
|
||||
)} />
|
||||
)
|
||||
Icon.propTypes = {
|
||||
fixedWidth: PropTypes.bool,
|
||||
icon: PropTypes.string,
|
||||
size: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number
|
||||
])
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const Icon = ({ icon, size = 1, fixedWidth, ...props }) => {
|
||||
props.className = classNames(
|
||||
props.className,
|
||||
icon !== undefined ? `xo-icon-${icon}` : 'fa', // Without icon prop, is a placeholder.
|
||||
isInteger(size) ? `fa-${size}x` : `fa-${size}`,
|
||||
fixedWidth && 'fa-fw'
|
||||
)
|
||||
|
||||
return <i {...props} />
|
||||
}
|
||||
propTypes(Icon)({
|
||||
fixedWidth: propTypes.bool,
|
||||
icon: propTypes.string,
|
||||
size: propTypes.oneOfType([
|
||||
propTypes.string,
|
||||
propTypes.number
|
||||
])
|
||||
})
|
||||
export default Icon
|
||||
|
||||
@@ -627,7 +627,7 @@ export default {
|
||||
editBackupReportTitle: undefined,
|
||||
|
||||
// Original text: 'Enable immediately after creation'
|
||||
editBackupReportEnable: undefined,
|
||||
editBackupScheduleEnabled: undefined,
|
||||
|
||||
// Original text: 'Depth'
|
||||
editBackupDepthTitle: undefined,
|
||||
@@ -1605,10 +1605,10 @@ export default {
|
||||
vdiRemove: undefined,
|
||||
|
||||
// Original text: "Boot flag"
|
||||
vdbBootableStatus: 'Etiqueta de Inicio',
|
||||
vbdBootableStatus: 'Etiqueta de Inicio',
|
||||
|
||||
// Original text: "Status"
|
||||
vdbStatus: 'Estado',
|
||||
vbdStatus: 'Estado',
|
||||
|
||||
// Original text: "Connected"
|
||||
vbdStatusConnected: 'Conectado',
|
||||
@@ -1626,19 +1626,19 @@ export default {
|
||||
vbdDisconnect: undefined,
|
||||
|
||||
// Original text: 'Bootable'
|
||||
vdbBootable: undefined,
|
||||
vbdBootable: undefined,
|
||||
|
||||
// Original text: 'Readonly'
|
||||
vdbReadonly: undefined,
|
||||
vbdReadonly: undefined,
|
||||
|
||||
// Original text: 'Create'
|
||||
vdbCreate: undefined,
|
||||
vbdCreate: undefined,
|
||||
|
||||
// Original text: 'Disk name'
|
||||
vdbNamePlaceHolder: undefined,
|
||||
vbdNamePlaceHolder: undefined,
|
||||
|
||||
// Original text: 'Size'
|
||||
vdbSizePlaceHolder: undefined,
|
||||
vbdSizePlaceHolder: undefined,
|
||||
|
||||
// Original text: 'Save'
|
||||
saveBootOption: undefined,
|
||||
|
||||
@@ -630,7 +630,7 @@ export default {
|
||||
editBackupReportTitle: 'Rapport',
|
||||
|
||||
// Original text: "Enable immediately after creation"
|
||||
editBackupReportEnable: 'Activer aussitôt après la création',
|
||||
editBackupScheduleEnabled: 'Executer en fonction de la planification',
|
||||
|
||||
// Original text: "Depth"
|
||||
editBackupDepthTitle: 'Profondeur',
|
||||
@@ -1608,10 +1608,10 @@ export default {
|
||||
vdiRemove: 'Supprimer le VDI',
|
||||
|
||||
// Original text: "Boot flag"
|
||||
vdbBootableStatus: 'Boot flag',
|
||||
vbdBootableStatus: 'Boot flag',
|
||||
|
||||
// Original text: "Status"
|
||||
vdbStatus: 'État',
|
||||
vbdStatus: 'État',
|
||||
|
||||
// Original text: "Connected"
|
||||
vbdStatusConnected: 'Connecté',
|
||||
@@ -1629,19 +1629,19 @@ export default {
|
||||
vbdDisconnect: 'Déconnecter un VBD',
|
||||
|
||||
// Original text: "Bootable"
|
||||
vdbBootable: 'Bootable',
|
||||
vbdBootable: 'Bootable',
|
||||
|
||||
// Original text: "Readonly"
|
||||
vdbReadonly: 'Lecture seule',
|
||||
vbdReadonly: 'Lecture seule',
|
||||
|
||||
// Original text: "Create"
|
||||
vdbCreate: 'Créer',
|
||||
vbdCreate: 'Créer',
|
||||
|
||||
// Original text: "Disk name"
|
||||
vdbNamePlaceHolder: 'Nom du disque',
|
||||
vbdNamePlaceHolder: 'Nom du disque',
|
||||
|
||||
// Original text: "Size"
|
||||
vdbSizePlaceHolder: 'Taille',
|
||||
vbdSizePlaceHolder: 'Taille',
|
||||
|
||||
// Original text: "Save"
|
||||
saveBootOption: 'Enregistrer',
|
||||
|
||||
@@ -627,7 +627,7 @@ export default {
|
||||
editBackupReportTitle: undefined,
|
||||
|
||||
// Original text: 'Enable immediately after creation'
|
||||
editBackupReportEnable: undefined,
|
||||
editBackupScheduleEnabled: undefined,
|
||||
|
||||
// Original text: 'Depth'
|
||||
editBackupDepthTitle: undefined,
|
||||
@@ -1605,10 +1605,10 @@ export default {
|
||||
vdiRemove: undefined,
|
||||
|
||||
// Original text: 'Boot flag'
|
||||
vdbBootableStatus: undefined,
|
||||
vbdBootableStatus: undefined,
|
||||
|
||||
// Original text: 'Status'
|
||||
vdbStatus: undefined,
|
||||
vbdStatus: undefined,
|
||||
|
||||
// Original text: 'Connected'
|
||||
vbdStatusConnected: undefined,
|
||||
@@ -1626,19 +1626,19 @@ export default {
|
||||
vbdDisconnect: undefined,
|
||||
|
||||
// Original text: 'Bootable'
|
||||
vdbBootable: undefined,
|
||||
vbdBootable: undefined,
|
||||
|
||||
// Original text: 'Readonly'
|
||||
vdbReadonly: undefined,
|
||||
vbdReadonly: undefined,
|
||||
|
||||
// Original text: 'Create'
|
||||
vdbCreate: undefined,
|
||||
vbdCreate: undefined,
|
||||
|
||||
// Original text: 'Disk name'
|
||||
vdbNamePlaceHolder: undefined,
|
||||
vbdNamePlaceHolder: undefined,
|
||||
|
||||
// Original text: 'Size'
|
||||
vdbSizePlaceHolder: undefined,
|
||||
vbdSizePlaceHolder: undefined,
|
||||
|
||||
// Original text: 'Save'
|
||||
saveBootOption: undefined,
|
||||
|
||||
@@ -726,7 +726,7 @@ export default {
|
||||
editBackupReportTitle: 'Riport',
|
||||
|
||||
// Original text: "Enable immediately after creation"
|
||||
editBackupReportEnable: 'Azonnal a létrehozás után',
|
||||
editBackupScheduleEnabled: 'Azonnal a létrehozás után',
|
||||
|
||||
// Original text: "Depth"
|
||||
editBackupDepthTitle: 'Mélység',
|
||||
@@ -1806,10 +1806,10 @@ export default {
|
||||
vdiRemove: 'VDI Eltávolítás',
|
||||
|
||||
// Original text: "Boot flag"
|
||||
vdbBootableStatus: 'Boot flag',
|
||||
vbdBootableStatus: 'Boot flag',
|
||||
|
||||
// Original text: "Status"
|
||||
vdbStatus: 'Állapot',
|
||||
vbdStatus: 'Állapot',
|
||||
|
||||
// Original text: "Connected"
|
||||
vbdStatusConnected: 'Kapcsolódva',
|
||||
@@ -1827,22 +1827,22 @@ export default {
|
||||
vbdDisconnect: 'VBD Lecsatlakozás',
|
||||
|
||||
// Original text: "Bootable"
|
||||
vdbBootable: 'Bootolható',
|
||||
vbdBootable: 'Bootolható',
|
||||
|
||||
// Original text: "Readonly"
|
||||
vdbReadonly: 'Csak olvasható',
|
||||
vbdReadonly: 'Csak olvasható',
|
||||
|
||||
// Original text: 'Action'
|
||||
vbdAction: undefined,
|
||||
|
||||
// Original text: "Create"
|
||||
vdbCreate: 'Létrehozás',
|
||||
vbdCreate: 'Létrehozás',
|
||||
|
||||
// Original text: "Disk name"
|
||||
vdbNamePlaceHolder: 'Diszk név',
|
||||
vbdNamePlaceHolder: 'Diszk név',
|
||||
|
||||
// Original text: "Size"
|
||||
vdbSizePlaceHolder: 'Méret',
|
||||
vbdSizePlaceHolder: 'Méret',
|
||||
|
||||
// Original text: "Save"
|
||||
saveBootOption: 'Mentés',
|
||||
|
||||
@@ -630,7 +630,7 @@ export default {
|
||||
editBackupReportTitle: 'Raport',
|
||||
|
||||
// Original text: "Enable immediately after creation"
|
||||
editBackupReportEnable: 'Uruchom natychamiast po utworzeniu',
|
||||
editBackupScheduleEnabled: 'Uruchom natychamiast po utworzeniu',
|
||||
|
||||
// Original text: "Depth"
|
||||
editBackupDepthTitle: 'Depth',
|
||||
@@ -1608,10 +1608,10 @@ export default {
|
||||
vdiRemove: 'Usuń VDI',
|
||||
|
||||
// Original text: "Boot flag"
|
||||
vdbBootableStatus: 'Boot flag',
|
||||
vbdBootableStatus: 'Boot flag',
|
||||
|
||||
// Original text: "Status"
|
||||
vdbStatus: 'Status',
|
||||
vbdStatus: 'Status',
|
||||
|
||||
// Original text: "Connected"
|
||||
vbdStatusConnected: 'Połączono',
|
||||
@@ -1629,19 +1629,19 @@ export default {
|
||||
vbdDisconnect: 'Rozłącz z VBD',
|
||||
|
||||
// Original text: "Bootable"
|
||||
vdbBootable: 'Bootable',
|
||||
vbdBootable: 'Bootable',
|
||||
|
||||
// Original text: "Readonly"
|
||||
vdbReadonly: 'Tylko do odcztu',
|
||||
vbdReadonly: 'Tylko do odcztu',
|
||||
|
||||
// Original text: "Create"
|
||||
vdbCreate: 'Utwórz',
|
||||
vbdCreate: 'Utwórz',
|
||||
|
||||
// Original text: "Disk name"
|
||||
vdbNamePlaceHolder: 'Nazwa dysku',
|
||||
vbdNamePlaceHolder: 'Nazwa dysku',
|
||||
|
||||
// Original text: "Size"
|
||||
vdbSizePlaceHolder: 'Rozmiar',
|
||||
vbdSizePlaceHolder: 'Rozmiar',
|
||||
|
||||
// Original text: "Save"
|
||||
saveBootOption: 'Zapisz',
|
||||
|
||||
@@ -627,7 +627,7 @@ export default {
|
||||
editBackupReportTitle: undefined,
|
||||
|
||||
// Original text: 'Enable immediately after creation'
|
||||
editBackupReportEnable: undefined,
|
||||
editBackupScheduleEnabled: undefined,
|
||||
|
||||
// Original text: 'Depth'
|
||||
editBackupDepthTitle: undefined,
|
||||
@@ -1605,10 +1605,10 @@ export default {
|
||||
vdiRemove: undefined,
|
||||
|
||||
// Original text: "Boot flag"
|
||||
vdbBootableStatus: 'Indicador de inicialização',
|
||||
vbdBootableStatus: 'Indicador de inicialização',
|
||||
|
||||
// Original text: "Status"
|
||||
vdbStatus: 'Status',
|
||||
vbdStatus: 'Status',
|
||||
|
||||
// Original text: "Connected"
|
||||
vbdStatusConnected: 'Conectado',
|
||||
@@ -1626,19 +1626,19 @@ export default {
|
||||
vbdDisconnect: undefined,
|
||||
|
||||
// Original text: 'Bootable'
|
||||
vdbBootable: undefined,
|
||||
vbdBootable: undefined,
|
||||
|
||||
// Original text: 'Readonly'
|
||||
vdbReadonly: undefined,
|
||||
vbdReadonly: undefined,
|
||||
|
||||
// Original text: 'Create'
|
||||
vdbCreate: undefined,
|
||||
vbdCreate: undefined,
|
||||
|
||||
// Original text: 'Disk name'
|
||||
vdbNamePlaceHolder: undefined,
|
||||
vbdNamePlaceHolder: undefined,
|
||||
|
||||
// Original text: 'Size'
|
||||
vdbSizePlaceHolder: undefined,
|
||||
vbdSizePlaceHolder: undefined,
|
||||
|
||||
// Original text: 'Save'
|
||||
saveBootOption: undefined,
|
||||
|
||||
@@ -1203,10 +1203,10 @@ export default {
|
||||
vdiVm: '虚拟机',
|
||||
|
||||
// Original text: "Boot flag"
|
||||
vdbBootableStatus: '启动标识',
|
||||
vbdBootableStatus: '启动标识',
|
||||
|
||||
// Original text: "Status"
|
||||
vdbStatus: '状态',
|
||||
vbdStatus: '状态',
|
||||
|
||||
// Original text: "Connected"
|
||||
vbdStatusConnected: '已连接',
|
||||
|
||||
@@ -18,13 +18,16 @@ var messages = {
|
||||
// ----- Modals -----
|
||||
alertOk: 'OK',
|
||||
confirmOk: 'OK',
|
||||
confirmCancel: 'Cancel',
|
||||
genericCancel: 'Cancel',
|
||||
|
||||
// ----- Filters -----
|
||||
onError: 'On error',
|
||||
successful: 'Successful',
|
||||
filterNoSnapshots: 'Full disks only',
|
||||
filterOnlySnapshots: 'Snapshots only',
|
||||
filterOnlyManaged: 'Managed disks',
|
||||
filterOnlyOrphaned: 'Orphaned disks',
|
||||
filterOnlyRegular: 'Normal disks',
|
||||
filterOnlySnapshots: 'Snapshot disks',
|
||||
filterOnlyUnmanaged: 'Unmanaged disks',
|
||||
|
||||
// ----- Copiable component -----
|
||||
copyToClipboard: 'Copy to clipboard',
|
||||
@@ -35,6 +38,7 @@ var messages = {
|
||||
// ----- Titles -----
|
||||
homePage: 'Home',
|
||||
homeVmPage: 'VMs',
|
||||
homeVmGroupPage: 'VM-Groups',
|
||||
homeHostPage: 'Hosts',
|
||||
homePoolPage: 'Pools',
|
||||
homeTemplatePage: 'Templates',
|
||||
@@ -63,6 +67,7 @@ var messages = {
|
||||
taskMenu: 'Tasks',
|
||||
taskPage: 'Tasks',
|
||||
newVmPage: 'VM',
|
||||
newVmGroupPage: 'VM-Group',
|
||||
newSrPage: 'Storage',
|
||||
newServerPage: 'Server',
|
||||
newImport: 'Import',
|
||||
@@ -118,6 +123,7 @@ var messages = {
|
||||
homeTypePool: 'Pool',
|
||||
homeTypeHost: 'Host',
|
||||
homeTypeVm: 'VM',
|
||||
homeTypeVmGroup: 'VM group',
|
||||
homeTypeSr: 'SR',
|
||||
homeTypeVmTemplate: 'Template',
|
||||
homeSort: 'Sort',
|
||||
@@ -273,9 +279,10 @@ var messages = {
|
||||
editBackupNot: 'Reverse',
|
||||
editBackupTagTitle: 'Tag',
|
||||
editBackupReportTitle: 'Report',
|
||||
editBackupReportEnable: 'Enable immediately after creation',
|
||||
editBackupScheduleEnabled: 'Automatically run as scheduled',
|
||||
editBackupDepthTitle: 'Depth',
|
||||
editBackupRemoteTitle: 'Remote',
|
||||
deleteOldBackupsFirst: 'Delete the old backups first',
|
||||
|
||||
// ------ New Remote -----
|
||||
remoteList: 'Remote stores for backup',
|
||||
@@ -384,7 +391,7 @@ var messages = {
|
||||
userLabel: 'User',
|
||||
adminLabel: 'Admin',
|
||||
noUserInGroup: 'No user in group',
|
||||
countUsers: '{users} user{users, plural, one {} other {s}}',
|
||||
countUsers: '{users, number} user{users, plural, one {} other {s}}',
|
||||
selectPermission: 'Select Permission',
|
||||
|
||||
// ----- Plugins ------
|
||||
@@ -489,6 +496,10 @@ var messages = {
|
||||
addSrLabel: 'Add SR',
|
||||
addVmLabel: 'Add VM',
|
||||
addHostLabel: 'Add Host',
|
||||
hostNeedsPatchUpdate: 'This host needs to install {patches, number} patch{patches, plural, one {} other {es}} before it can be added to the pool. This operation may be long.',
|
||||
hostNeedsPatchUpdateNoInstall: 'This host cannot be added to the pool because it\'s missing some patches.',
|
||||
addHostErrorTitle: 'Adding host failed',
|
||||
addHostNotHomogeneousErrorMessage: 'Host patches could not be homogenized.',
|
||||
disconnectServer: 'Disconnect',
|
||||
|
||||
// ----- Host actions ------
|
||||
@@ -502,7 +513,7 @@ var messages = {
|
||||
noHostsAvailableErrorTitle: 'Error while restarting host',
|
||||
noHostsAvailableErrorMessage: 'Some VMs cannot be migrated before restarting this host. Please try force reboot.',
|
||||
failHostBulkRestartTitle: 'Error while restarting hosts',
|
||||
failHostBulkRestartMessage: '{failedHosts}/{totalHosts} host{failedHosts, plural, one {} other {s}} could not be restarted.',
|
||||
failHostBulkRestartMessage: '{failedHosts, number}/{totalHosts, number} host{failedHosts, plural, one {} other {s}} could not be restarted.',
|
||||
rebootUpdateHostLabel: 'Reboot to apply updates',
|
||||
emergencyModeLabel: 'Emergency mode',
|
||||
// ----- Host tabs -----
|
||||
@@ -595,10 +606,13 @@ var messages = {
|
||||
hostAppliedPatches: 'Applied patches',
|
||||
hostMissingPatches: 'Missing patches',
|
||||
hostUpToDate: 'Host up-to-date!',
|
||||
installPatchWarningTitle: 'Non-recommended patch install',
|
||||
installPatchWarningContent: 'This will install a patch only on this host. This is NOT the recommended way: please go into the Pool patch view and follow instructions there. If you are sure about this, you can continue anyway',
|
||||
installPatchWarningReject: 'Go to pool',
|
||||
installPatchWarningResolve: 'Install',
|
||||
// ----- Pool patch tabs -----
|
||||
refreshPatches: 'Refresh patches',
|
||||
installPoolPatches: 'Install pool patches',
|
||||
checkForUpdates: 'Check for updates',
|
||||
// ----- Pool storage tabs -----
|
||||
defaultSr: 'Default SR',
|
||||
setAsDefaultSr: 'Set as default SR',
|
||||
@@ -613,6 +627,7 @@ var messages = {
|
||||
advancedTabName: 'Advanced',
|
||||
networkTabName: 'Network',
|
||||
disksTabName: 'Disk{disks, plural, one {} other {s}}',
|
||||
managementTabName: 'Management',
|
||||
|
||||
powerStateHalted: 'halted',
|
||||
powerStateRunning: 'running',
|
||||
@@ -625,6 +640,7 @@ var messages = {
|
||||
vmSettings: 'Started {ago}',
|
||||
vmCurrentStatus: 'Current status:',
|
||||
vmNotRunning: 'Not running',
|
||||
vmHaltedSince: 'Halted {ago}',
|
||||
|
||||
// ----- VM general tab -----
|
||||
noToolsDetected: 'No Xen tools detected',
|
||||
@@ -673,6 +689,8 @@ var messages = {
|
||||
vdiBootOrder: 'Boot order',
|
||||
vdiNameLabel: 'Name',
|
||||
vdiNameDescription: 'Description',
|
||||
vdiPool: 'Pool',
|
||||
vdiDisconnect: 'Disconnect',
|
||||
vdiTags: 'Tags',
|
||||
vdiSize: 'Size',
|
||||
vdiSr: 'SR',
|
||||
@@ -684,19 +702,20 @@ var messages = {
|
||||
vdiMigrateNoSrMessage: 'A target SR is required to migrate a VDI',
|
||||
vdiForget: 'Forget',
|
||||
vdiRemove: 'Remove VDI',
|
||||
vdbBootableStatus: 'Boot flag',
|
||||
vdbStatus: 'Status',
|
||||
noControlDomainVdis: 'No VDIs attached to Control Domain',
|
||||
vbdBootableStatus: 'Boot flag',
|
||||
vbdStatus: 'Status',
|
||||
vbdStatusConnected: 'Connected',
|
||||
vbdStatusDisconnected: 'Disconnected',
|
||||
vbdNoVbd: 'No disks',
|
||||
vbdConnect: 'Connect VBD',
|
||||
vbdDisconnect: 'Disconnect VBD',
|
||||
vdbBootable: 'Bootable',
|
||||
vdbReadonly: 'Readonly',
|
||||
vbdBootable: 'Bootable',
|
||||
vbdReadonly: 'Readonly',
|
||||
vbdAction: 'Action',
|
||||
vdbCreate: 'Create',
|
||||
vdbNamePlaceHolder: 'Disk name',
|
||||
vdbSizePlaceHolder: 'Size',
|
||||
vbdCreate: 'Create',
|
||||
vbdNamePlaceHolder: 'Disk name',
|
||||
vbdSizePlaceHolder: 'Size',
|
||||
cdDriveNotInstalled: 'CD drive not completely installed',
|
||||
cdDriveInstallation: 'Stop and start the VM to install the CD drive',
|
||||
saveBootOption: 'Save',
|
||||
@@ -770,6 +789,8 @@ var messages = {
|
||||
autoPowerOn: 'Auto power on',
|
||||
ha: 'HA',
|
||||
vmAffinityHost: 'Affinity host',
|
||||
vmVga: 'VGA',
|
||||
vmVideoram: 'Video RAM',
|
||||
noAffinityHost: 'None',
|
||||
originalTemplate: 'Original template',
|
||||
unknownOsName: 'Unknown',
|
||||
@@ -777,6 +798,11 @@ var messages = {
|
||||
unknownOriginalTemplate: 'Unknown',
|
||||
vmLimitsLabel: 'VM limits',
|
||||
vmCpuLimitsLabel: 'CPU limits',
|
||||
vmCpuTopology: 'Topology',
|
||||
vmChooseCoresPerSocket: 'Default behavior',
|
||||
vmCoresPerSocket: '{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket',
|
||||
vmCoresPerSocketIncorrectValue: 'Incorrect cores per socket value',
|
||||
vmCoresPerSocketIncorrectValueSolution: 'Please change the selected value to fix it.',
|
||||
vmMemoryLimitsLabel: 'Memory limits (min/max)',
|
||||
vmMaxVcpus: 'vCPUs max:',
|
||||
vmMaxRam: 'Memory max:',
|
||||
@@ -818,7 +844,7 @@ var messages = {
|
||||
srFree: 'free',
|
||||
srUsageStatePanel: 'Storage Usage',
|
||||
srTopUsageStatePanel: 'Top 5 SR Usage (in %)',
|
||||
vmsStates: '{running} running ({halted} halted)',
|
||||
vmsStates: '{running, number} running ({halted, number} halted)',
|
||||
dashboardStatsButtonRemoveAll: 'Clear selection',
|
||||
dashboardStatsButtonAddAllHost: 'Add all hosts',
|
||||
dashboardStatsButtonAddAllVM: 'Add all VMs',
|
||||
@@ -843,6 +869,7 @@ var messages = {
|
||||
orphanedVms: 'Orphaned VMs snapshot',
|
||||
noOrphanedObject: 'No orphans',
|
||||
removeAllOrphanedObject: 'Remove all orphaned snapshot VDIs',
|
||||
vdisOnControlDomain: 'VDIs attached to Control Domain',
|
||||
vmNameLabel: 'Name',
|
||||
vmNameDescription: 'Description',
|
||||
vmContainer: 'Resident on',
|
||||
@@ -897,7 +924,7 @@ var messages = {
|
||||
newVmDefaultCpuCap: 'Default: {value, number}',
|
||||
newVmCloudConfig: 'Cloud config',
|
||||
newVmCreateVms: 'Create VMs',
|
||||
newVmCreateVmsConfirm: 'Are you sure you want to create {nbVms} VMs?',
|
||||
newVmCreateVmsConfirm: 'Are you sure you want to create {nbVms, number} VMs?',
|
||||
newVmMultipleVms: 'Multiple VMs:',
|
||||
newVmSelectResourceSet: 'Select a resource set:',
|
||||
newVmMultipleVmsPattern: 'Name pattern:',
|
||||
@@ -911,6 +938,32 @@ var messages = {
|
||||
newVmHideAdvanced: 'Hide advanced settings',
|
||||
newVmShare: 'Share this VM',
|
||||
|
||||
// ----- VM-Group-----
|
||||
newVmGroupTitle: 'Create a new Vm-Group on ',
|
||||
newVmGroupNameLabel: 'Label',
|
||||
newVmGroupDescriptionLabel: 'Description',
|
||||
newVmGroupReset: 'Reset',
|
||||
newVmGroupCreate: 'Create',
|
||||
newVmGroupInfoPanel: 'Infos',
|
||||
|
||||
// ----- VM-Group item -----
|
||||
powerStateVmGroupRunning: 'All vms are running',
|
||||
powerStateVmGroupHalted: 'All vms are halted',
|
||||
powerStateVmGroupBusy: 'Contains vms halted and vms running',
|
||||
|
||||
// ----- VM-Group management Tab -----
|
||||
'attachVmButton': 'new VM',
|
||||
'vmsBootOrder': 'Boot order',
|
||||
'vmGroupLabel': 'Label',
|
||||
'vmGroupDescription': 'Description',
|
||||
'vmGroupActions': 'Actions',
|
||||
|
||||
// ----- VM-Group general Tab -----
|
||||
'vmGroupCurrentStatus': 'Current status',
|
||||
|
||||
// ----- VM-Group stats Tab -----
|
||||
'vmGroupAllVm': 'All VMs',
|
||||
|
||||
// ----- Self -----
|
||||
resourceSets: 'Resource sets',
|
||||
noResourceSets: 'No resource sets.',
|
||||
@@ -978,6 +1031,7 @@ var messages = {
|
||||
delta: 'delta',
|
||||
restoreBackups: 'Restore Backups',
|
||||
restoreBackupsInfo: 'Click on a VM to display restore options',
|
||||
restoreDeltaBackupsInfo: 'Only the files of Delta Backup which are not on a SMB remote can be restored',
|
||||
remoteEnabled: 'Enabled',
|
||||
remoteError: 'Error',
|
||||
noBackup: 'No backup available',
|
||||
@@ -1011,7 +1065,7 @@ var messages = {
|
||||
|
||||
// ----- Modals -----
|
||||
emergencyShutdownHostsModalTitle: 'Emergency shutdown Host{nHosts, plural, one {} other {s}}',
|
||||
emergencyShutdownHostsModalMessage: 'Are you sure you want to shutdown {nHosts} Host{nHosts, plural, one {} other {s}}?',
|
||||
emergencyShutdownHostsModalMessage: 'Are you sure you want to shutdown {nHosts, number} Host{nHosts, plural, one {} other {s}}?',
|
||||
stopHostModalTitle: 'Shutdown host',
|
||||
stopHostModalMessage: 'This will shutdown your host. Do you want to continue? If it\'s the pool master, your connection to the pool will be lost',
|
||||
addHostModalTitle: 'Add host',
|
||||
@@ -1019,25 +1073,32 @@ var messages = {
|
||||
restartHostModalTitle: 'Restart host',
|
||||
restartHostModalMessage: 'This will restart your host. Do you want to continue?',
|
||||
restartHostsAgentsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}',
|
||||
restartHostsAgentsModalMessage: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?',
|
||||
restartHostsAgentsModalMessage: 'Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?',
|
||||
restartHostsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}}',
|
||||
restartHostsModalMessage: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}}?',
|
||||
restartHostsModalMessage: 'Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}}?',
|
||||
startVmsModalTitle: 'Start VM{vms, plural, one {} other {s}}',
|
||||
startVmsModalMessage: 'Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?',
|
||||
cloneAndStartVM: 'Start a copy',
|
||||
forceStartVm: 'Force start',
|
||||
forceStartVmModalTitle: 'Forbidden operation',
|
||||
blockedStartVmModalMessage: 'Start operation for this vm is blocked.',
|
||||
blockedStartVmsModalMessage: 'Forbidden operation start for {nVms, number} vm{nVms, plural, one {} other {s}}.',
|
||||
startVmsModalMessage: 'Are you sure you want to start {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
failedVmsErrorMessage: '{nVms, number} vm{nVms, plural, one {} other {s}} are failed. Please see your logs to get more information',
|
||||
failedVmsErrorTitle: 'Start failed',
|
||||
stopHostsModalTitle: 'Stop Host{nHosts, plural, one {} other {s}}',
|
||||
stopHostsModalMessage: 'Are you sure you want to stop {nHosts} Host{nHosts, plural, one {} other {s}}?',
|
||||
stopHostsModalMessage: 'Are you sure you want to stop {nHosts, number} Host{nHosts, plural, one {} other {s}}?',
|
||||
stopVmsModalTitle: 'Stop VM{vms, plural, one {} other {s}}',
|
||||
stopVmsModalMessage: 'Are you sure you want to stop {vms} VM{vms, plural, one {} other {s}}?',
|
||||
stopVmsModalMessage: 'Are you sure you want to stop {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
restartVmModalTitle: 'Restart VM',
|
||||
restartVmModalMessage: 'Are you sure you want to restart {name}?',
|
||||
stopVmModalTitle: 'Stop VM',
|
||||
stopVmModalMessage: 'Are you sure you want to stop {name}?',
|
||||
restartVmsModalTitle: 'Restart VM{vms, plural, one {} other {s}}',
|
||||
restartVmsModalMessage: 'Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?',
|
||||
restartVmsModalMessage: 'Are you sure you want to restart {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
snapshotVmsModalTitle: 'Snapshot VM{vms, plural, one {} other {s}}',
|
||||
snapshotVmsModalMessage: 'Are you sure you want to snapshot {vms} VM{vms, plural, one {} other {s}}?',
|
||||
snapshotVmsModalMessage: 'Are you sure you want to snapshot {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
deleteVmsModalTitle: 'Delete VM{vms, plural, one {} other {s}}',
|
||||
deleteVmsModalMessage: 'Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
|
||||
deleteVmsModalMessage: 'Are you sure you want to delete {vms, number} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
|
||||
deleteVmModalTitle: 'Delete VM',
|
||||
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
|
||||
migrateVmModalTitle: 'Migrate VM',
|
||||
@@ -1055,6 +1116,8 @@ var messages = {
|
||||
migrateVmNetwork: 'Network',
|
||||
migrateVmNoTargetHost: 'No target host',
|
||||
migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
|
||||
deleteVmGroupModalTitle: 'Delete VM-Group',
|
||||
deleteVmGroupModalMessage: 'Are you sure you want to delete this VMGroup ?',
|
||||
deleteVdiModalTitle: 'Delete VDI',
|
||||
deleteVdiModalMessage: 'Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST',
|
||||
revertVmModalTitle: 'Revert your VM',
|
||||
@@ -1085,6 +1148,9 @@ var messages = {
|
||||
serverPassword: 'Password',
|
||||
serverAction: 'Action',
|
||||
serverReadOnly: 'Read Only',
|
||||
serverUnauthorizedCertificates: 'Unauthorized Certificates',
|
||||
serverAllowUnauthorizedCertificates: 'Allow Unauthorized Certificates',
|
||||
serverUnauthorizedCertificatesInfo: 'Enable it if your certificate is rejected, but it\'s not recommended because your connection will not be secured.',
|
||||
serverDisconnect: 'Disconnect server',
|
||||
serverPlaceHolderUser: 'username',
|
||||
serverPlaceHolderPassword: 'password',
|
||||
@@ -1094,12 +1160,14 @@ var messages = {
|
||||
serverError: 'Error',
|
||||
serverAddFailed: 'Adding server failed',
|
||||
serverStatus: 'Status',
|
||||
serverConnectionFailed: 'Connection failed',
|
||||
serverConnectionFailed: 'Connection failed. Click for more information.',
|
||||
serverConnecting: 'Connecting...',
|
||||
serverConnected: 'Connected',
|
||||
serverDisconnected: 'Disconnected',
|
||||
serverAuthFailed: 'Authentication error',
|
||||
serverUnknownError: 'Unknown error',
|
||||
serverSelfSignedCertError: 'Invalid self-signed certificate',
|
||||
serverSelfSignedCertQuestion: 'Do you want to accept self-signed certificate for this server even though it would decrease security?',
|
||||
|
||||
// ----- Copy VM -----
|
||||
copyVm: 'Copy VM',
|
||||
@@ -1349,6 +1417,9 @@ var messages = {
|
||||
xosanUsedSpace: 'Used space',
|
||||
xosanNeedPack: 'XOSAN pack needs to be installed on each host of the pool.',
|
||||
xosanInstallIt: 'Install it now!',
|
||||
xosanNeedRestart: 'Some hosts need their toolstack to be restarted before you can create an XOSAN',
|
||||
xosanRestartAgents: 'Restart toolstacks',
|
||||
xosanMasterOffline: 'Pool master is not running',
|
||||
xosanInstallPackTitle: 'Install XOSAN pack on {pool}',
|
||||
xosanSelect2Srs: 'Select at least 2 SRs',
|
||||
xosanLayout: 'Layout',
|
||||
|
||||
@@ -4,7 +4,7 @@ import _ from 'intl'
|
||||
import ActionButton from './action-button'
|
||||
import Component from './base-component'
|
||||
import Icon from 'icon'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import Tooltip from 'tooltip'
|
||||
import { alert } from 'modal'
|
||||
import { connectStore } from './utils'
|
||||
|
||||
@@ -5,7 +5,7 @@ import { filter, map } from 'lodash'
|
||||
import _ from '../intl'
|
||||
import Button from '../button'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import { EMPTY_ARRAY } from '../utils'
|
||||
|
||||
import GenericInput from './generic-input'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react'
|
||||
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { EMPTY_OBJECT } from '../utils'
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { keyBy, map } from 'lodash'
|
||||
|
||||
import _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import { EMPTY_OBJECT } from '../utils'
|
||||
|
||||
import GenericInput from './generic-input'
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react'
|
||||
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
|
||||
import Combobox from '../combobox'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
@@ -14,23 +15,30 @@ import { PrimitiveInputWrapper } from './helpers'
|
||||
})
|
||||
@uncontrollableInput()
|
||||
export default class StringInput extends Component {
|
||||
// the value of this input is undefined not '' when empty to make
|
||||
// it homogenous with when the user has never touched this input
|
||||
_onChange = event => {
|
||||
const value = getEventValue(event)
|
||||
this.props.onChange(value !== '' ? value : undefined)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { required, schema } = this.props
|
||||
const {
|
||||
disabled,
|
||||
onChange,
|
||||
password,
|
||||
placeholder = schema.default,
|
||||
value,
|
||||
...props
|
||||
} = this.props
|
||||
delete props.onChange
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<Combobox
|
||||
value={value || ''}
|
||||
value={value !== undefined ? value : ''}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
onChange={this._onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={placeholder || schema.default}
|
||||
required={required}
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import { routerShape } from 'react-router/lib/PropTypes'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import isArray from 'lodash/isArray'
|
||||
import isString from 'lodash/isString'
|
||||
import map from 'lodash/map'
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
import { Modal as ReactModal } from 'react-bootstrap-4/lib'
|
||||
|
||||
import _ from './intl'
|
||||
import Button from './button'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import Tooltip from './tooltip'
|
||||
import {
|
||||
disable as disableShortcuts,
|
||||
enable as enableShortcuts
|
||||
@@ -23,54 +25,33 @@ const modal = (content, onClose) => {
|
||||
instance.setState({ content, onClose, showModal: true }, disableShortcuts)
|
||||
}
|
||||
|
||||
export const alert = (title, body) => {
|
||||
return new Promise(resolve => {
|
||||
const { Body, Footer, Header, Title } = ReactModal
|
||||
modal(
|
||||
<div>
|
||||
<Header closeButton>
|
||||
<Title>{title}</Title>
|
||||
</Header>
|
||||
<Body>{body}</Body>
|
||||
<Footer>
|
||||
<Button bsStyle='primary' onClick={() => {
|
||||
resolve()
|
||||
instance.close()
|
||||
}}>
|
||||
{_('alertOk')}
|
||||
</Button>
|
||||
</Footer>
|
||||
</div>,
|
||||
resolve
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const _addRef = (component, ref) => {
|
||||
if (isString(component) || isArray(component)) {
|
||||
return component
|
||||
@propTypes({
|
||||
buttons: propTypes.arrayOf(propTypes.shape({
|
||||
btnStyle: propTypes.string,
|
||||
icon: propTypes.string,
|
||||
label: propTypes.string.isRequired,
|
||||
tooltip: propTypes.node,
|
||||
value: propTypes.any
|
||||
})).isRequired,
|
||||
children: propTypes.node.isRequired,
|
||||
icon: propTypes.string,
|
||||
title: propTypes.node.isRequired
|
||||
})
|
||||
class GenericModal extends Component {
|
||||
_getBodyValue = () => {
|
||||
const { body } = this.refs
|
||||
if (body !== undefined) {
|
||||
return body.getWrappedInstance === undefined
|
||||
? body.value
|
||||
: body.getWrappedInstance().value
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return cloneElement(component, { ref })
|
||||
} catch (_) {} // Stateless component.
|
||||
return component
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
children: propTypes.node.isRequired,
|
||||
title: propTypes.node.isRequired,
|
||||
icon: propTypes.string
|
||||
})
|
||||
class Confirm extends Component {
|
||||
_resolve = () => {
|
||||
const { body } = this.refs
|
||||
this.props.resolve(body && (body.getWrappedInstance
|
||||
? body.getWrappedInstance().value
|
||||
: body.value
|
||||
))
|
||||
_resolve = (value = this._getBodyValue()) => {
|
||||
this.props.resolve(value)
|
||||
instance.close()
|
||||
}
|
||||
|
||||
_reject = () => {
|
||||
this.props.reject()
|
||||
instance.close()
|
||||
@@ -78,7 +59,12 @@ class Confirm extends Component {
|
||||
|
||||
render () {
|
||||
const { Body, Footer, Header, Title } = ReactModal
|
||||
const { title, icon } = this.props
|
||||
|
||||
const {
|
||||
buttons,
|
||||
icon,
|
||||
title
|
||||
} = this.props
|
||||
|
||||
const body = _addRef(this.props.children, 'body')
|
||||
|
||||
@@ -95,39 +81,99 @@ class Confirm extends Component {
|
||||
{body}
|
||||
</Body>
|
||||
<Footer>
|
||||
<Button
|
||||
btnStyle='primary'
|
||||
onClick={this._resolve}
|
||||
style={this._style}
|
||||
>
|
||||
{_('confirmOk')}
|
||||
</Button>
|
||||
{' '}
|
||||
<Button
|
||||
onClick={this._reject}
|
||||
>
|
||||
{_('confirmCancel')}
|
||||
</Button>
|
||||
{map(buttons, ({
|
||||
label,
|
||||
tooltip,
|
||||
value,
|
||||
icon,
|
||||
...props
|
||||
}) => {
|
||||
const button = <Button
|
||||
onClick={() => this._resolve(value)}
|
||||
key={value}
|
||||
{...props}
|
||||
>
|
||||
{icon !== undefined && <Icon icon={icon} fixedWidth />}
|
||||
{label}
|
||||
</Button>
|
||||
return <span>
|
||||
{tooltip !== undefined
|
||||
? <Tooltip content={tooltip}>{button}</Tooltip>
|
||||
: button
|
||||
}
|
||||
{' '}
|
||||
</span>
|
||||
})}
|
||||
{this.props.reject !== undefined &&
|
||||
<Button onClick={this._reject} >
|
||||
{_('genericCancel')}
|
||||
</Button>
|
||||
}
|
||||
</Footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const ALERT_BUTTONS = [ { label: _('alertOk'), value: 'ok' } ]
|
||||
|
||||
export const alert = (title, body) => (
|
||||
new Promise(resolve => {
|
||||
modal(
|
||||
<GenericModal
|
||||
buttons={ALERT_BUTTONS}
|
||||
resolve={resolve}
|
||||
title={title}
|
||||
>
|
||||
{body}
|
||||
</GenericModal>,
|
||||
resolve
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const _addRef = (component, ref) => {
|
||||
if (isString(component) || isArray(component)) {
|
||||
return component
|
||||
}
|
||||
|
||||
try {
|
||||
return cloneElement(component, { ref })
|
||||
} catch (_) {} // Stateless component.
|
||||
return component
|
||||
}
|
||||
|
||||
const CONFIRM_BUTTONS = [ { btnStyle: 'primary', label: _('confirmOk') } ]
|
||||
|
||||
export const confirm = ({
|
||||
body,
|
||||
title,
|
||||
icon = 'alarm'
|
||||
icon = 'alarm',
|
||||
title
|
||||
}) => (
|
||||
chooseAction({
|
||||
body,
|
||||
buttons: CONFIRM_BUTTONS,
|
||||
icon,
|
||||
title
|
||||
})
|
||||
)
|
||||
|
||||
export const chooseAction = ({
|
||||
body,
|
||||
buttons,
|
||||
icon,
|
||||
title
|
||||
}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
modal(
|
||||
<Confirm
|
||||
title={title}
|
||||
resolve={resolve}
|
||||
reject={reject}
|
||||
<GenericModal
|
||||
buttons={buttons}
|
||||
icon={icon}
|
||||
reject={reject}
|
||||
resolve={resolve}
|
||||
title={title}
|
||||
>
|
||||
{body}
|
||||
</Confirm>,
|
||||
</GenericModal>,
|
||||
reject
|
||||
)
|
||||
})
|
||||
|
||||
33
src/common/prop-types-decorator.js
Normal file
33
src/common/prop-types-decorator.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import assign from 'lodash/assign'
|
||||
import { PropTypes } from 'react'
|
||||
|
||||
// Decorators to help declaring properties and context types on React
|
||||
// components without using the tedious static properties syntax.
|
||||
//
|
||||
// ```js
|
||||
// @propTypes({
|
||||
// children: propTypes.node.isRequired
|
||||
// }, {
|
||||
// store: propTypes.object.isRequired
|
||||
// })
|
||||
// class MyComponent extends React.Component {}
|
||||
// ```
|
||||
const propTypes = (propTypes, contextTypes) => target => {
|
||||
if (propTypes !== undefined) {
|
||||
target.propTypes = {
|
||||
...target.propTypes,
|
||||
...propTypes
|
||||
}
|
||||
}
|
||||
if (contextTypes !== undefined) {
|
||||
target.contextTypes = {
|
||||
...target.contextTypes,
|
||||
...contextTypes
|
||||
}
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
assign(propTypes, PropTypes)
|
||||
|
||||
export { propTypes as default }
|
||||
@@ -1,22 +0,0 @@
|
||||
import assign from 'lodash/assign'
|
||||
import { PropTypes } from 'react'
|
||||
|
||||
// Decorators to help declaring on React components without using the
|
||||
// tedious static properties syntax.
|
||||
//
|
||||
// ```js
|
||||
// @propTypes({
|
||||
// children: propTypes.node.isRequired
|
||||
// })
|
||||
// class MyComponent extends React.Component {}
|
||||
// ```
|
||||
const propTypes = types => target => {
|
||||
target.propTypes = {
|
||||
...target.propTypes,
|
||||
...types
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
assign(propTypes, PropTypes)
|
||||
export { propTypes as default }
|
||||
2
src/common/react-novnc.js
vendored
2
src/common/react-novnc.js
vendored
@@ -8,7 +8,7 @@ import {
|
||||
} from 'url'
|
||||
import { enable as enableShortcuts, disable as disableShortcuts } from 'shortcuts'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const parseRelativeUrl = url => parseUrl(resolveUrl(String(window.location), url))
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from 'intl'
|
||||
import React from 'react'
|
||||
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import { createGetObject } from './selectors'
|
||||
import { isSrWritable } from './xo'
|
||||
import {
|
||||
@@ -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>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import _ from './intl'
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import TimezonePicker from './timezone-picker'
|
||||
import Icon from './icon'
|
||||
import Tooltip from './tooltip'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import propTypes from 'prop-types'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import _ from './intl'
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import renderXoItem from './render-xo-item'
|
||||
import store from './store'
|
||||
import Tooltip from './tooltip'
|
||||
|
||||
@@ -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,
|
||||
@@ -448,6 +458,24 @@ export const createGetTags = collectionSelectors => {
|
||||
return _extendCollectionSelector(getTags, 'tag')
|
||||
}
|
||||
|
||||
export const createGetVmLastShutdownTime = (getVmId = (_, {vm}) => vm != null ? vm.id : undefined) => create(
|
||||
getVmId,
|
||||
createGetObjectsOfType('message'),
|
||||
(vmId, messages) => {
|
||||
let max = null
|
||||
forEach(messages, message => {
|
||||
if (
|
||||
message.$object === vmId &&
|
||||
message.name === 'VM_SHUTDOWN' &&
|
||||
(max === null || message.time > max)
|
||||
) {
|
||||
max = message.time
|
||||
}
|
||||
})
|
||||
return max
|
||||
}
|
||||
)
|
||||
|
||||
export const createGetObjectMessages = objectSelector =>
|
||||
createGetObjectsOfType('message').filter(
|
||||
create(
|
||||
@@ -463,9 +491,10 @@ export const createGetObjectMessages = objectSelector =>
|
||||
export const getObject = createGetObject((_, id) => id)
|
||||
|
||||
export const createDoesHostNeedRestart = hostSelector => {
|
||||
// Returns the first patch of the host which requires it to be
|
||||
// restarted.
|
||||
const restartPoolPatch = createGetObjectsOfType('pool_patch').pick(
|
||||
// XS < 7.1
|
||||
const patchRequiresReboot = createGetObjectsOfType('pool_patch').pick(
|
||||
// Returns the first patch of the host which requires it to be
|
||||
// restarted.
|
||||
create(
|
||||
createGetObjectsOfType('host_patch').pick(
|
||||
(state, props) => {
|
||||
@@ -485,7 +514,11 @@ export const createDoesHostNeedRestart = hostSelector => {
|
||||
action === 'restartHost' || action === 'restartXapi'
|
||||
) ])
|
||||
|
||||
return (state, props) => restartPoolPatch(state, props) !== undefined
|
||||
return create(
|
||||
hostSelector,
|
||||
(...args) => args,
|
||||
(host, args) => host.rebootRequired || !!patchRequiresReboot(...args)
|
||||
)
|
||||
}
|
||||
|
||||
export const createGetHostMetrics = hostSelector =>
|
||||
@@ -509,3 +542,17 @@ export const createGetHostMetrics = hostSelector =>
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
export const createGetVmDisks = vmSelector =>
|
||||
createGetObjectsOfType('VDI').pick(
|
||||
create(
|
||||
createGetObjectsOfType('VBD').pick(
|
||||
(state, props) => vmSelector(state, props).$VBDs
|
||||
),
|
||||
_createCollectionWrapper(vbds => map(vbds, vbd =>
|
||||
vbd.is_cd_drive
|
||||
? undefined
|
||||
: vbd.VDI
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { cloneElement } from 'react'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const SINGLE_LINE_STYLE = { display: 'flex' }
|
||||
const COL_STYLE = { marginTop: 'auto', marginBottom: 'auto' }
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Portal } from 'react-overlays'
|
||||
import Button from '../button'
|
||||
import Component from '../base-component'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import SingleLineRow from '../single-line-row'
|
||||
import { BlockLink } from '../link'
|
||||
import { Container, Col } from '../grid'
|
||||
@@ -33,6 +33,7 @@ import styles from './index.css'
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
defaultFilter: propTypes.string,
|
||||
filters: propTypes.object,
|
||||
nFilteredItems: propTypes.number.isRequired,
|
||||
nItems: propTypes.number.isRequired,
|
||||
@@ -75,10 +76,10 @@ class TableFilter extends Component {
|
||||
</Dropdown>
|
||||
</div>}
|
||||
<input
|
||||
type='text'
|
||||
ref='filter'
|
||||
onChange={this._onChange}
|
||||
className='form-control'
|
||||
defaultValue={props.defaultFilter}
|
||||
onChange={this._onChange}
|
||||
ref='filter'
|
||||
/>
|
||||
<div className='input-group-btn'>
|
||||
<Button onClick={this._cleanFilter}>
|
||||
@@ -137,14 +138,16 @@ const DEFAULT_ITEMS_PER_PAGE = 10
|
||||
|
||||
@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
|
||||
@@ -176,7 +179,10 @@ export default class SortedTable extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const { defaultFilter } = props
|
||||
|
||||
this.state = {
|
||||
filter: defaultFilter !== undefined ? props.filters[defaultFilter] : undefined,
|
||||
selectedColumn,
|
||||
itemsPerPage: props.itemsPerPage || DEFAULT_ITEMS_PER_PAGE
|
||||
}
|
||||
@@ -192,7 +198,7 @@ export default class SortedTable extends Component {
|
||||
createFilter(
|
||||
() => this.props.collection,
|
||||
createSelector(
|
||||
() => this.state.filter || '',
|
||||
() => this.state.filter,
|
||||
createMatcher
|
||||
)
|
||||
),
|
||||
@@ -291,6 +297,7 @@ export default class SortedTable extends Component {
|
||||
|
||||
const filterInstance = (
|
||||
<TableFilter
|
||||
defaultFilter={state.filter}
|
||||
filters={filters}
|
||||
nFilteredItems={nFilteredItems}
|
||||
nItems={this._getTotalNumberOfItems()}
|
||||
@@ -318,11 +325,21 @@ export default class SortedTable extends Component {
|
||||
</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)}
|
||||
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
|
||||
|
||||
|
||||
@@ -2,28 +2,31 @@ import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const Button = styled(ActionButton)`
|
||||
background-color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateBg`]}
|
||||
border: 2px solid ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]}
|
||||
color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]}
|
||||
background-color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateBg`]};
|
||||
border: 2px solid ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};
|
||||
color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};
|
||||
`
|
||||
|
||||
const StateButton = ({
|
||||
disabledHandler,
|
||||
disabledHandlerParam,
|
||||
disabledLabel,
|
||||
disabledTooltip,
|
||||
|
||||
enabledLabel,
|
||||
enabledTooltip,
|
||||
enabledHandler,
|
||||
enabledHandlerParam,
|
||||
|
||||
state,
|
||||
...props
|
||||
}) =>
|
||||
<Button
|
||||
handler={state ? enabledHandler : disabledHandler}
|
||||
handlerParam={state ? enabledHandlerParam : disabledHandlerParam}
|
||||
tooltip={state ? enabledTooltip : disabledTooltip}
|
||||
{...props}
|
||||
icon={state ? 'running' : 'halted'}
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react'
|
||||
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
const INPUT_STYLE = {
|
||||
margin: '2px',
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react'
|
||||
|
||||
import _ from './intl'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import { getXoServerTimezone } from './xo'
|
||||
import { Select } from './form'
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import ReactDOM from 'react-dom'
|
||||
|
||||
import Component from '../base-component'
|
||||
import getPosition from './get-position'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ export { default as Debug } from './debug'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Returns the first defined (non-null, non-undefined) value.
|
||||
// Returns the first defined (non-undefined) value.
|
||||
export const firstDefined = function () {
|
||||
const n = arguments.length
|
||||
for (let i = 0; i < n; ++i) {
|
||||
@@ -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 => {
|
||||
@@ -537,3 +523,21 @@ export const compareVersions = makeNiceCompare((v1, v2) => {
|
||||
|
||||
export const isXosanPack = ({ name }) =>
|
||||
startsWith(name, 'XOSAN')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const getCoresPerSocketPossibilities = (maxCoresPerSocket, vCPUs) => {
|
||||
// According to : https://www.citrix.com/blogs/2014/03/11/citrix-xenserver-setting-more-than-one-vcpu-per-vm-to-improve-application-performance-and-server-consolidation-e-g-for-cad3-d-graphical-applications/
|
||||
const maxVCPUs = 16
|
||||
|
||||
const options = []
|
||||
if (maxCoresPerSocket !== undefined && vCPUs !== '') {
|
||||
const ratio = vCPUs / maxVCPUs
|
||||
|
||||
for (let coresPerSocket = maxCoresPerSocket; coresPerSocket >= ratio; coresPerSocket--) {
|
||||
if (vCPUs % coresPerSocket === 0) options.push(coresPerSocket)
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import classNames from 'classnames'
|
||||
import every from 'lodash/every'
|
||||
import map from 'lodash/map'
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
|
||||
import _ from '../intl'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const Wizard = ({ children }) => {
|
||||
const allDone = every(React.Children.toArray(children), (child) => child.props.done || child.props.summary)
|
||||
const allDone = every(React.Children.toArray(children), child =>
|
||||
child.props.done || child.props.summary
|
||||
)
|
||||
|
||||
return <ul className={styles.wizard}>
|
||||
{map(React.Children.toArray(children), (child, key) => cloneElement(child, { allDone, key }))}
|
||||
{React.Children.map(children, (child, key) =>
|
||||
child && cloneElement(child, { allDone, key })
|
||||
)}
|
||||
</ul>
|
||||
}
|
||||
export { Wizard as default }
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
find,
|
||||
flatten,
|
||||
floor,
|
||||
forEach,
|
||||
map,
|
||||
max,
|
||||
size,
|
||||
@@ -15,8 +16,8 @@ import {
|
||||
values
|
||||
} from 'lodash'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
import { computeArraysSum } from '../xo-stats'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import { computeArraysSum, computeArraysAvg } from '../xo-stats'
|
||||
import { formatSize } from '../utils'
|
||||
|
||||
import styles from './index.css'
|
||||
@@ -214,6 +215,54 @@ export const PoolCpuLineChart = injectIntl(propTypes({
|
||||
)
|
||||
}))
|
||||
|
||||
export const VmGroupCpuLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstVmData = data[0]
|
||||
const length = getStatsLength(firstVmData.stats.cpus)
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
}
|
||||
|
||||
const series = map(data, ({ vm, stats }) => ({
|
||||
name: vm,
|
||||
data: computeArraysSum(stats.cpus)
|
||||
}))
|
||||
|
||||
if (addSumSeries) {
|
||||
series.push({
|
||||
name: intl.formatMessage(messages.vmGroupAllVm),
|
||||
data: computeArraysSum(map(series, 'data')),
|
||||
className: styles.dashedLine
|
||||
})
|
||||
}
|
||||
|
||||
const nbCpusByVm = map(data, ({ stats }) => stats.cpus.length)
|
||||
|
||||
return (
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
intl,
|
||||
nValues: length,
|
||||
endTimestamp: firstVmData.endTimestamp,
|
||||
interval: firstVmData.interval,
|
||||
valueTransform: value => `${floor(value)}%`
|
||||
}),
|
||||
high: 100 * (addSumSeries ? sum(nbCpusByVm) : max(nbCpusByVm)),
|
||||
...options
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}))
|
||||
|
||||
export const MemoryLineChart = injectIntl(propTypes({
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
@@ -302,6 +351,57 @@ export const PoolMemoryLineChart = injectIntl(propTypes({
|
||||
)
|
||||
}))
|
||||
|
||||
export const VmGroupMemoryLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstVmData = data[0]
|
||||
const {
|
||||
memory,
|
||||
memoryUsed
|
||||
} = firstVmData.stats
|
||||
|
||||
if (!memory || !memoryUsed) {
|
||||
return templateError
|
||||
}
|
||||
|
||||
const series = map(data, ({ vm, stats }) => ({
|
||||
name: vm,
|
||||
data: stats.memoryUsed
|
||||
}))
|
||||
|
||||
if (addSumSeries) {
|
||||
series.push({
|
||||
name: intl.formatMessage(messages.vmGroupAllVm),
|
||||
data: computeArraysSum(map(data, 'stats.memoryUsed')),
|
||||
className: styles.dashedLine
|
||||
})
|
||||
}
|
||||
|
||||
const currentMemoryByHost = map(data, ({ stats }) => stats.memory[stats.memory.length - 1])
|
||||
|
||||
return (
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
intl,
|
||||
nValues: firstVmData.stats.memoryUsed.length,
|
||||
endTimestamp: firstVmData.endTimestamp,
|
||||
interval: firstVmData.interval,
|
||||
valueTransform: formatSize
|
||||
}),
|
||||
high: addSumSeries ? sum(currentMemoryByHost) : max(currentMemoryByHost),
|
||||
...options
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}))
|
||||
|
||||
export const XvdLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
@@ -334,6 +434,60 @@ export const XvdLineChart = injectIntl(propTypes({
|
||||
)
|
||||
}))
|
||||
|
||||
export const VmGroupXvdLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstVmData = data[0]
|
||||
const {
|
||||
memory,
|
||||
memoryUsed
|
||||
} = firstVmData.stats
|
||||
|
||||
if (!memory || !memoryUsed) {
|
||||
return templateError
|
||||
}
|
||||
|
||||
const series = flatten(map(data, ({ stats, vm }) =>
|
||||
map(stats.xvds, (xvd, key) => {
|
||||
return {
|
||||
name: `${vm} (${key})`,
|
||||
data: computeArraysAvg(stats.xvds[key])
|
||||
}
|
||||
})
|
||||
))
|
||||
|
||||
const datas = []
|
||||
forEach(series, ({ data }) => datas.push(data))
|
||||
if (addSumSeries) {
|
||||
series.push({
|
||||
name: intl.formatMessage(messages.vmGroupAllVm),
|
||||
data: computeArraysSum(datas),
|
||||
className: styles.dashedLine
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
intl,
|
||||
nValues: firstVmData.stats.xvds.r.length,
|
||||
endTimestamp: data.endTimestamp,
|
||||
interval: data.interval,
|
||||
valueTransform: formatSize
|
||||
}),
|
||||
...options
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}))
|
||||
|
||||
export const VifLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
@@ -443,6 +597,48 @@ export const PoolPifLineChart = injectIntl(propTypes({
|
||||
)
|
||||
}))
|
||||
|
||||
export const VmGroupVifLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstVmData = data[0]
|
||||
const length = firstVmData.stats && getStatsLength(firstVmData.stats.vifs.rx)
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
}
|
||||
const series = addSumSeries
|
||||
? map(ios, io => ({
|
||||
name: `${intl.formatMessage(messages.vmGroupAllVm)} (${io})`,
|
||||
data: computeArraysSum(map(data, ({ stats }) => computeArraysSum(stats.vifs[io])))
|
||||
}))
|
||||
: flatten(map(data, ({ stats, vm }) =>
|
||||
map(ios, io => ({
|
||||
name: `${vm} (${io})`,
|
||||
data: computeArraysSum(stats.vifs[io])
|
||||
}))
|
||||
))
|
||||
return (
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
intl,
|
||||
nValues: length,
|
||||
endTimestamp: firstVmData.endTimestamp,
|
||||
interval: firstVmData.interval,
|
||||
valueTransform: formatSize
|
||||
}),
|
||||
...options
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}))
|
||||
|
||||
export const LoadLineChart = injectIntl(propTypes({
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
|
||||
@@ -6,7 +6,7 @@ import map from 'lodash/map'
|
||||
import times from 'lodash/times'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import { setStyles } from './d3-utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
SparklinesLine
|
||||
} from 'react-sparklines'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import {
|
||||
computeArraysAvg,
|
||||
computeObjectsAvg
|
||||
|
||||
@@ -5,7 +5,7 @@ import map from 'lodash/map'
|
||||
|
||||
import Component from '../base-component'
|
||||
import _ from '../intl'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import { Toggle } from '../form'
|
||||
import { setStyles } from '../d3-utils'
|
||||
import {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { FormattedTime } from 'react-intl'
|
||||
|
||||
import _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import _ from 'intl'
|
||||
import BaseComponent from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import { Col } from 'grid'
|
||||
import { connectStore } from 'utils'
|
||||
import { createCollectionWrapper, createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { forEach } from 'lodash'
|
||||
import { createCollectionWrapper, createGetObjectsOfType, createSelector, createGetObject } from 'selectors'
|
||||
import { SelectHost } from 'select-objects'
|
||||
import {
|
||||
differenceBy,
|
||||
forEach
|
||||
} from 'lodash'
|
||||
|
||||
@connectStore(() => ({
|
||||
singleHosts: createSelector(
|
||||
@@ -30,10 +34,20 @@ import { SelectHost } from 'select-objects'
|
||||
})
|
||||
return singleHosts
|
||||
})
|
||||
),
|
||||
poolMasterPatches: createSelector(
|
||||
createGetObject(
|
||||
(_, props) => props.pool.master
|
||||
),
|
||||
({ patches }) => patches
|
||||
)
|
||||
}), { withRef: true })
|
||||
export default class AddHostModal extends BaseComponent {
|
||||
get value () {
|
||||
if (process.env.XOA_PLAN < 2 && this.state.nMissingPatches) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return this.state
|
||||
}
|
||||
|
||||
@@ -42,18 +56,40 @@ export default class AddHostModal extends BaseComponent {
|
||||
singleHosts => host => singleHosts[host.id]
|
||||
)
|
||||
|
||||
_onChangeHost = host => {
|
||||
this.setState({
|
||||
host,
|
||||
nMissingPatches: host
|
||||
? differenceBy(this.props.poolMasterPatches, host.patches, 'name').length
|
||||
: undefined
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { nMissingPatches } = this.state
|
||||
|
||||
return <div>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('addHostSelectHost')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectHost
|
||||
onChange={this.linkState('host')}
|
||||
onChange={this._onChangeHost}
|
||||
predicate={this._getHostPredicate()}
|
||||
value={this.state.host}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<br />
|
||||
{nMissingPatches > 0 && <SingleLineRow>
|
||||
<Col>
|
||||
<span className='text-danger'>
|
||||
<Icon icon='error' /> {process.env.XOA_PLAN > 1
|
||||
? _('hostNeedsPatchUpdate', { patches: nMissingPatches })
|
||||
: _('hostNeedsPatchUpdateNoInstall')
|
||||
}
|
||||
</span>
|
||||
</Col>
|
||||
</SingleLineRow>}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as FormGrid from '../../form-grid'
|
||||
import _ from '../../intl'
|
||||
import Combobox from '../../combobox'
|
||||
import Component from '../../base-component'
|
||||
import propTypes from '../../prop-types'
|
||||
import propTypes from '../../prop-types-decorator'
|
||||
import { createSelector } from '../../selectors'
|
||||
|
||||
@propTypes({
|
||||
|
||||
@@ -15,16 +15,18 @@ import sortBy from 'lodash/sortBy'
|
||||
import throttle from 'lodash/throttle'
|
||||
import Xo from 'xo-lib'
|
||||
import { createBackoff } from 'jsonrpc-websocket-client'
|
||||
import { noHostsAvailable } from 'xo-common/api-errors'
|
||||
import { reflect } from 'promise-toolbox'
|
||||
import { lastly, reflect, tap } from 'promise-toolbox'
|
||||
import { forbiddenOperation, noHostsAvailable } from 'xo-common/api-errors'
|
||||
import { resolve } from 'url'
|
||||
|
||||
import _ from '../intl'
|
||||
import invoke from '../invoke'
|
||||
import logError from '../log-error'
|
||||
import { alert, confirm } from '../modal'
|
||||
import store from 'store'
|
||||
import { getObject } from 'selectors'
|
||||
import { alert, chooseAction, confirm } from '../modal'
|
||||
import { error, info, success } from '../notification'
|
||||
import { noop, rethrow, tap, resolveId, resolveIds } from '../utils'
|
||||
import { noop, resolveId, resolveIds } from '../utils'
|
||||
import {
|
||||
connected,
|
||||
disconnected,
|
||||
@@ -41,6 +43,10 @@ export const XEN_DEFAULT_CPU_CAP = 0
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const XEN_VIDEORAM_VALUES = [1, 2, 4, 8, 16]
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const isSrWritable = sr => sr && sr.content_type !== 'iso' && sr.size > 0
|
||||
export const isSrShared = sr => sr && sr.$PBDs.length > 1
|
||||
export const isVmRunning = vm => vm && vm.power_state === 'Running'
|
||||
@@ -86,7 +92,7 @@ const _call = (method, params) => {
|
||||
let promise = _signIn.then(() => xo.call(method, params))
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
promise = promise::rethrow(error => {
|
||||
promise = promise::tap(null, error => {
|
||||
console.error('XO error', {
|
||||
method,
|
||||
params,
|
||||
@@ -269,6 +275,8 @@ export const subscribeIpPools = createSubscription(() => _call('ipPool.getAll'))
|
||||
|
||||
export const subscribeResourceCatalog = createSubscription(() => _call('cloud.getResourceCatalog'))
|
||||
|
||||
export const subscribeVmGroups = createSubscription(() => _call('vmGroup.get'))
|
||||
|
||||
const xosanSubscriptions = {}
|
||||
export const subscribeIsInstallingXosan = (pool, cb) => {
|
||||
const poolId = resolveId(pool)
|
||||
@@ -280,6 +288,28 @@ export const subscribeIsInstallingXosan = (pool, cb) => {
|
||||
return xosanSubscriptions[poolId](cb)
|
||||
}
|
||||
|
||||
const missingPatchesByHost = {}
|
||||
export const subscribeHostMissingPatches = (host, cb) => {
|
||||
const hostId = resolveId(host)
|
||||
|
||||
if (missingPatchesByHost[hostId] == null) {
|
||||
missingPatchesByHost[hostId] = createSubscription(() => _call('host.listMissingPatches', { host: hostId }))
|
||||
}
|
||||
|
||||
return missingPatchesByHost[hostId](cb)
|
||||
}
|
||||
subscribeHostMissingPatches.forceRefresh = host => {
|
||||
if (host === undefined) {
|
||||
forEach(missingPatchesByHost, subscription => subscription.forceRefresh())
|
||||
return
|
||||
}
|
||||
|
||||
const subscription = missingPatchesByHost[resolveId(host)]
|
||||
if (subscription !== undefined) {
|
||||
subscription.forceRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
// System ============================================================
|
||||
|
||||
export const apiMethods = _call('system.getMethodsInfo')
|
||||
@@ -308,8 +338,9 @@ export const exportConfig = () => (
|
||||
|
||||
export const addServer = (host, username, password, label) => (
|
||||
_call('server.add', { host, label, password, username })::tap(
|
||||
subscribeServers.forceRefresh
|
||||
)::rethrow(() => error(_('serverError'), _('serverAddFailed')))
|
||||
subscribeServers.forceRefresh,
|
||||
() => error(_('serverError'), _('serverAddFailed'))
|
||||
)
|
||||
)
|
||||
|
||||
export const editServer = (server, props) => (
|
||||
@@ -319,7 +350,7 @@ export const editServer = (server, props) => (
|
||||
)
|
||||
|
||||
export const connectServer = server => (
|
||||
_call('server.connect', { id: resolveId(server) })::tap(
|
||||
_call('server.connect', { id: resolveId(server) })::lastly(
|
||||
subscribeServers.forceRefresh
|
||||
)
|
||||
)
|
||||
@@ -346,6 +377,7 @@ import AddHostModalBody from './add-host-modal' // eslint-disable-line import/fi
|
||||
export const addHostToPool = (pool, host) => {
|
||||
if (host) {
|
||||
return confirm({
|
||||
icon: 'add',
|
||||
title: _('addHostModalTitle'),
|
||||
body: _('addHostModalMessage', { pool: pool.name_label, host: host.name_label })
|
||||
}).then(() =>
|
||||
@@ -354,6 +386,7 @@ export const addHostToPool = (pool, host) => {
|
||||
}
|
||||
|
||||
return confirm({
|
||||
icon: 'add',
|
||||
title: _('addHostModalTitle'),
|
||||
body: <AddHostModalBody pool={pool} />
|
||||
}).then(
|
||||
@@ -362,7 +395,13 @@ export const addHostToPool = (pool, host) => {
|
||||
error(_('addHostNoHost'), _('addHostNoHostMessage'))
|
||||
return
|
||||
}
|
||||
_call('pool.mergeInto', { source: params.host.$pool, target: pool.id, force: true })
|
||||
return _call('pool.mergeInto', { source: params.host.$pool, target: pool.id, force: true }).catch(error => {
|
||||
if (error.code !== 'HOSTS_NOT_HOMOGENEOUS') {
|
||||
throw error
|
||||
}
|
||||
|
||||
error(_('addHostErrorTitle'), _('addHostNotHomogeneousErrorMessage'))
|
||||
})
|
||||
},
|
||||
noop
|
||||
)
|
||||
@@ -436,7 +475,7 @@ export const restartHostsAgents = hosts => {
|
||||
title: _('restartHostsAgentsModalTitle', { nHosts }),
|
||||
body: _('restartHostsAgentsModalMessage', { nHosts })
|
||||
}).then(
|
||||
() => map(hosts, restartHostAgent),
|
||||
() => Promise.all(map(hosts, restartHostAgent)),
|
||||
noop
|
||||
)
|
||||
}
|
||||
@@ -494,15 +533,21 @@ export const emergencyShutdownHosts = hosts => {
|
||||
}
|
||||
|
||||
export const installHostPatch = (host, { uuid }) => (
|
||||
_call('host.installPatch', { host: resolveId(host), patch: uuid })
|
||||
_call('host.installPatch', { host: resolveId(host), patch: uuid })::tap(
|
||||
() => subscribeHostMissingPatches.forceRefresh(host)
|
||||
)
|
||||
)
|
||||
|
||||
export const installAllHostPatches = host => (
|
||||
_call('host.installAllPatches', { host: resolveId(host) })
|
||||
_call('host.installAllPatches', { host: resolveId(host) })::tap(
|
||||
() => subscribeHostMissingPatches.forceRefresh(host)
|
||||
)
|
||||
)
|
||||
|
||||
export const installAllPatchesOnPool = pool => (
|
||||
_call('pool.installAllPatches', { pool: resolveId(pool) })
|
||||
_call('pool.installAllPatches', { pool: resolveId(pool) })::tap(
|
||||
() => subscribeHostMissingPatches.forceRefresh()
|
||||
)
|
||||
)
|
||||
|
||||
export const installSupplementalPack = (host, file) => {
|
||||
@@ -567,8 +612,38 @@ export const unpauseContainer = (vm, container) => (
|
||||
|
||||
// VM ----------------------------------------------------------------
|
||||
|
||||
const chooseActionToUnblockForbiddenStartVm = props => (
|
||||
chooseAction({
|
||||
icon: 'alarm',
|
||||
buttons: [
|
||||
{ label: _('cloneAndStartVM'), value: 'clone', btnStyle: 'success' },
|
||||
{ label: _('forceStartVm'), value: 'force', btnStyle: 'danger' }
|
||||
],
|
||||
...props
|
||||
})
|
||||
)
|
||||
|
||||
const cloneAndStartVM = async vm => (
|
||||
_call('vm.start', { id: await cloneVm(vm) })
|
||||
)
|
||||
|
||||
export const startVm = vm => (
|
||||
_call('vm.start', { id: resolveId(vm) })
|
||||
_call('vm.start', { id: resolveId(vm) }).catch(async reason => {
|
||||
if (!forbiddenOperation.is(reason)) {
|
||||
throw reason
|
||||
}
|
||||
|
||||
const choice = await chooseActionToUnblockForbiddenStartVm({
|
||||
body: _('blockedStartVmModalMessage'),
|
||||
title: _('forceStartVmModalTitle')
|
||||
})
|
||||
|
||||
if (choice === 'clone') {
|
||||
return cloneAndStartVM(vm)
|
||||
}
|
||||
|
||||
return _call('vm.start', { id: resolveId(vm), force: true })
|
||||
})
|
||||
)
|
||||
|
||||
export const startVms = vms => (
|
||||
@@ -576,7 +651,52 @@ export const startVms = vms => (
|
||||
title: _('startVmsModalTitle', { vms: vms.length }),
|
||||
body: _('startVmsModalMessage', { vms: vms.length })
|
||||
}).then(
|
||||
() => map(vms, vmId => startVm({ id: vmId })),
|
||||
async () => {
|
||||
const forbiddenStart = []
|
||||
let nErrors = 0
|
||||
|
||||
await Promise.all(map(
|
||||
vms,
|
||||
id => _call('vm.start', { id }).catch(reason => {
|
||||
if (forbiddenOperation.is(reason)) {
|
||||
forbiddenStart.push(id)
|
||||
} else {
|
||||
nErrors++
|
||||
}
|
||||
})
|
||||
))
|
||||
|
||||
if (forbiddenStart.length === 0) {
|
||||
if (nErrors === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
return error(_('failedVmsErrorTitle'), _('failedVmsErrorMessage', {nVms: nErrors}))
|
||||
}
|
||||
|
||||
const choice = await chooseActionToUnblockForbiddenStartVm({
|
||||
body: _('blockedStartVmsModalMessage', {nVms: forbiddenStart.length}),
|
||||
title: _('forceStartVmModalTitle')
|
||||
}).catch(noop)
|
||||
|
||||
if (nErrors !== 0) {
|
||||
error(_('failedVmsErrorTitle'), _('failedVmsErrorMessage', {nVms: nErrors}))
|
||||
}
|
||||
|
||||
if (choice === 'clone') {
|
||||
return Promise.all(map(
|
||||
forbiddenStart,
|
||||
async id => cloneAndStartVM(getObject(store.getState(), id))
|
||||
))
|
||||
}
|
||||
|
||||
if (choice === 'force') {
|
||||
return Promise.all(map(
|
||||
forbiddenStart,
|
||||
id => _call('vm.start', { id, force: true })
|
||||
))
|
||||
}
|
||||
},
|
||||
noop
|
||||
)
|
||||
)
|
||||
@@ -917,6 +1037,10 @@ export const attachDiskToVm = (vdi, vm, { bootable, mode, position }) => (
|
||||
})
|
||||
)
|
||||
|
||||
export const removeAppliance = vm => {
|
||||
_call('vm.removeAppliance', { id: resolveId(vm) })
|
||||
}
|
||||
|
||||
// DISK ---------------------------------------------------------------
|
||||
|
||||
export const createDisk = (name, size, sr) => (
|
||||
@@ -960,7 +1084,7 @@ export const migrateVdi = (vdi, sr) => (
|
||||
_call('vdi.migrate', { id: resolveId(vdi), sr_id: resolveId(sr) })
|
||||
)
|
||||
|
||||
// VDB ---------------------------------------------------------------
|
||||
// VBD ---------------------------------------------------------------
|
||||
|
||||
export const connectVbd = vbd => (
|
||||
_call('vbd.connect', { id: resolveId(vbd) })
|
||||
@@ -1319,16 +1443,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'))
|
||||
)
|
||||
)
|
||||
@@ -1350,8 +1472,7 @@ export const configurePlugin = (id, configuration) =>
|
||||
() => {
|
||||
info(_('pluginConfigurationSuccess'), _('pluginConfigurationChanges'))
|
||||
subscribePlugins.forceRefresh()
|
||||
}
|
||||
)::rethrow(
|
||||
},
|
||||
err => error(_('pluginError'), JSON.stringify(err.data) || _('unknownPluginError'))
|
||||
)
|
||||
|
||||
@@ -1399,7 +1520,8 @@ export const recomputeResourceSetsLimits = () => (
|
||||
// Remote ------------------------------------------------------------
|
||||
|
||||
export const getRemote = remote => (
|
||||
_call('remote.get', resolveIds({ id: remote }))::rethrow(
|
||||
_call('remote.get', resolveIds({ id: remote }))::tap(
|
||||
null,
|
||||
err => error(_('getRemote'), err.message || String(err))
|
||||
)
|
||||
)
|
||||
@@ -1436,20 +1558,21 @@ export const editRemote = (remote, { name, url }) => (
|
||||
|
||||
export const listRemote = remote => (
|
||||
_call('remote.list', resolveIds({ id: remote }))::tap(
|
||||
subscribeRemotes.forceRefresh
|
||||
)::rethrow(
|
||||
subscribeRemotes.forceRefresh,
|
||||
err => error(_('listRemote'), err.message || String(err))
|
||||
)
|
||||
)
|
||||
|
||||
export const listRemoteBackups = remote => (
|
||||
_call('backup.list', resolveIds({ remote }))::rethrow(
|
||||
_call('backup.list', resolveIds({ remote }))::tap(
|
||||
null,
|
||||
err => error(_('listRemote'), err.message || String(err))
|
||||
)
|
||||
)
|
||||
|
||||
export const testRemote = remote => (
|
||||
_call('remote.test', resolveIds({ id: remote }))::rethrow(
|
||||
_call('remote.test', resolveIds({ id: remote }))::tap(
|
||||
null,
|
||||
err => error(_('testRemote'), err.message || String(err))
|
||||
)
|
||||
)
|
||||
@@ -1556,16 +1679,14 @@ export const deleteApiLog = id => (
|
||||
|
||||
export const addAcl = ({ subject, object, action }) => (
|
||||
_call('acl.add', resolveIds({ subject, object, action }))::tap(
|
||||
subscribeAcls.forceRefresh
|
||||
)::rethrow(
|
||||
subscribeAcls.forceRefresh,
|
||||
err => error('Add ACL', err.message || String(err))
|
||||
)
|
||||
)
|
||||
|
||||
export const removeAcl = ({ subject, object, action }) => (
|
||||
_call('acl.remove', resolveIds({ subject, object, action }))::tap(
|
||||
subscribeAcls.forceRefresh
|
||||
)::rethrow(
|
||||
subscribeAcls.forceRefresh,
|
||||
err => error('Remove ACL', err.message || String(err))
|
||||
)
|
||||
)
|
||||
@@ -1580,14 +1701,15 @@ export const editAcl = (
|
||||
) => (
|
||||
_call('acl.remove', resolveIds({ subject, object, action }))
|
||||
.then(() => _call('acl.add', resolveIds({ subject: newSubject, object: newObject, action: newAction })))
|
||||
::tap(subscribeAcls.forceRefresh)
|
||||
::rethrow(err => error('Edit ACL', err.message || String(err)))
|
||||
::tap(
|
||||
subscribeAcls.forceRefresh,
|
||||
err => error('Edit ACL', err.message || String(err))
|
||||
)
|
||||
)
|
||||
|
||||
export const createGroup = name => (
|
||||
_call('group.create', { name })::tap(
|
||||
subscribeGroups.forceRefresh
|
||||
):: rethrow(
|
||||
subscribeGroups.forceRefresh,
|
||||
err => error(_('createGroup'), err.message || String(err))
|
||||
)
|
||||
)
|
||||
@@ -1598,35 +1720,35 @@ export const setGroupName = (group, name) => (
|
||||
)
|
||||
)
|
||||
|
||||
export const deleteGroup = group => (
|
||||
export const deleteGroup = group =>
|
||||
confirm({
|
||||
title: _('deleteGroup'),
|
||||
body: <p>{_('deleteGroupConfirm')}</p>
|
||||
}).then(() => _call('group.delete', resolveIds({ id: group })))
|
||||
::tap(subscribeGroups.forceRefresh)
|
||||
::rethrow(err => error(_('deleteGroup'), err.message || String(err)))
|
||||
)
|
||||
}).then(() =>
|
||||
_call('group.delete', resolveIds({ id: group }))::tap(
|
||||
subscribeGroups.forceRefresh,
|
||||
err => error(_('deleteGroup'), err.message || String(err))
|
||||
),
|
||||
noop
|
||||
)
|
||||
|
||||
export const removeUserFromGroup = (user, group) => (
|
||||
_call('group.removeUser', resolveIds({ id: group, userId: user }))::tap(
|
||||
subscribeGroups.forceRefresh
|
||||
)::rethrow(
|
||||
subscribeGroups.forceRefresh,
|
||||
err => error(_('removeUserFromGroup'), err.message || String(err))
|
||||
)
|
||||
)
|
||||
|
||||
export const addUserToGroup = (user, group) => (
|
||||
_call('group.addUser', resolveIds({ id: group, userId: user }))::tap(
|
||||
subscribeGroups.forceRefresh
|
||||
)::rethrow(
|
||||
subscribeGroups.forceRefresh,
|
||||
err => error('Add User', err.message || String(err))
|
||||
)
|
||||
)
|
||||
|
||||
export const createUser = (email, password, permission) => (
|
||||
_call('user.create', { email, password, permission })::tap(
|
||||
subscribeUsers.forceRefresh
|
||||
)::rethrow(
|
||||
subscribeUsers.forceRefresh,
|
||||
err => error('Create user', err.message || String(err))
|
||||
)
|
||||
)
|
||||
@@ -1636,9 +1758,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))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1834,3 +1957,33 @@ export const downloadAndInstallXosanPack = pool =>
|
||||
)
|
||||
|
||||
export const registerXosan = namespace => _call('cloud.registerResource', { namespace: 'xosan' })
|
||||
|
||||
// VM-Group ----------------------------------------------------------------------
|
||||
|
||||
export const startVmGroup = vmGroup => {
|
||||
_call('vmGroup.start', { id: resolveId(vmGroup) })
|
||||
}
|
||||
export const shutdownVmGroup = vmGroup => _call('vmGroup.shutdown', { id: resolveId(vmGroup) })
|
||||
export const rebootVmGroup = async vmGroup => {
|
||||
await shutdownVmGroup(vmGroup)
|
||||
await startVmGroup(vmGroup)
|
||||
}
|
||||
export const editVmGroup = (vmGroup, props) => _call('vmGroup.set', { id: resolveId(vmGroup), ...props })
|
||||
export const deleteVmGroup = (vmGroup, vms) =>
|
||||
confirm({
|
||||
title: _('deleteVmGroupModalTitle'),
|
||||
body: _('deleteVmGroupModalMessage')
|
||||
}).then(() => {
|
||||
forEach(vms, vm => removeAppliance(vm))
|
||||
_call('vmGroup.destroy', { id: resolveId(vmGroup) })
|
||||
}, Promise.reject())
|
||||
export const createVmGroup = ({ pool, name_label, name_description }) => _call('vmGroup.create', { id: resolveId(pool), name_label, name_description })
|
||||
export const startVmGroups = (vmGroupIds) => {
|
||||
forEach(vmGroupIds, id => startVmGroup({id: id}))
|
||||
}
|
||||
export const shutdownVmGroups = (vmGroupIds) => {
|
||||
forEach(vmGroupIds, id => shutdownVmGroup({id: id}))
|
||||
}
|
||||
export const rebootVmGroups = (vmGroupIds) => {
|
||||
forEach(vmGroupIds, id => rebootVmGroup({id: id}))
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import _ from './intl'
|
||||
import Icon from './icon'
|
||||
import Link from './link'
|
||||
import propTypes from './prop-types'
|
||||
import propTypes from './prop-types-decorator'
|
||||
import { Card, CardHeader, CardBlock } from './card'
|
||||
import { connectStore, getXoaPlan } from './utils'
|
||||
import { isAdmin } from 'selectors'
|
||||
|
||||
@@ -375,6 +375,12 @@
|
||||
@extend .xo-status-busy;
|
||||
}
|
||||
|
||||
&-disabled {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@extend .xo-status-busy;
|
||||
}
|
||||
|
||||
&-all-connected {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@@ -441,6 +447,11 @@
|
||||
@extend .fa-server;
|
||||
@extend .text-danger;
|
||||
}
|
||||
&-disabled {
|
||||
@extend .fa;
|
||||
@extend .fa-server;
|
||||
@extend .text-warning;
|
||||
}
|
||||
&-working {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}
|
||||
|
||||
.usage-element-highlight {
|
||||
background-color: $brand-primary;
|
||||
background-color: $brand-warning;
|
||||
}
|
||||
|
||||
.usage-element-others {
|
||||
|
||||
3
src/xo-app/backup/file-restore/index.css
Normal file
3
src/xo-app/backup/file-restore/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.listRestoreBackupInfos {
|
||||
list-style-type: none;
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from 'xo'
|
||||
|
||||
import RestoreFileModalBody from './restore-file-modal'
|
||||
import styles from './index.css'
|
||||
|
||||
const VM_COLUMNS = [
|
||||
{
|
||||
@@ -118,9 +119,18 @@ export default class FileRestore extends Component {
|
||||
? <Container>
|
||||
<h2>{_('restoreFiles')}</h2>
|
||||
{isEmpty(backupInfoByVm)
|
||||
? _('noBackup')
|
||||
? <div>
|
||||
<em><Icon icon='info' /> {_('restoreDeltaBackupsInfo')}</em>
|
||||
<div>
|
||||
<a>{_('noBackup')}</a>
|
||||
</div>
|
||||
</div>
|
||||
: <div>
|
||||
<em><Icon icon='info' /> {_('restoreBackupsInfo')}</em>
|
||||
<ul className={styles.listRestoreBackupInfos}>
|
||||
<li><em><Icon icon='info' /> {_('restoreBackupsInfo')}</em></li>
|
||||
<li><em><Icon icon='info' /> {_('restoreDeltaBackupsInfo')}</em></li>
|
||||
</ul>
|
||||
|
||||
<SortedTable collection={backupInfoByVm} columns={VM_COLUMNS} rowAction={openImportModal} defaultColumn={2} />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -85,7 +85,8 @@ export default class RestoreFileModalBody extends Component {
|
||||
return scanFiles(backup.remoteId, disk, path, partition).then(
|
||||
rawFiles => this.setState({
|
||||
files: formatFilesOptions(rawFiles, path),
|
||||
scanningFiles: false
|
||||
scanningFiles: false,
|
||||
scanFilesError: false
|
||||
}),
|
||||
error => {
|
||||
this.setState({
|
||||
@@ -104,7 +105,8 @@ export default class RestoreFileModalBody extends Component {
|
||||
partition: undefined,
|
||||
file: undefined,
|
||||
selectedFiles: undefined,
|
||||
scanDiskError: false
|
||||
scanDiskError: false,
|
||||
scanFilesError: false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -113,7 +115,8 @@ export default class RestoreFileModalBody extends Component {
|
||||
partition: undefined,
|
||||
file: undefined,
|
||||
selectedFiles: undefined,
|
||||
scanDiskError: false
|
||||
scanDiskError: false,
|
||||
scanFilesError: false
|
||||
})
|
||||
|
||||
if (!disk) {
|
||||
|
||||
@@ -132,7 +132,7 @@ const COMMON_SCHEMA = {
|
||||
},
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
title: _('editBackupReportEnable')
|
||||
title: _('editBackupScheduleEnabled')
|
||||
}
|
||||
},
|
||||
required: [ 'tag', 'vms', '_reportWhen' ]
|
||||
@@ -190,6 +190,15 @@ const DISASTER_RECOVERY_SCHEMA = {
|
||||
properties: {
|
||||
...COMMON_SCHEMA.properties,
|
||||
depth: DEPTH_PROPERTY,
|
||||
deleteOldBackupsFirst: {
|
||||
type: 'boolean',
|
||||
title: _('deleteOldBackupsFirst'),
|
||||
description: [
|
||||
'Delete the old backups before copy the vms.',
|
||||
'',
|
||||
'If the backup fails, you will lose your old backups.'
|
||||
].join('\n')
|
||||
},
|
||||
sr: {
|
||||
type: 'string',
|
||||
'xo:type': 'sr',
|
||||
@@ -282,6 +291,7 @@ class TimeoutInput extends Component {
|
||||
return <input
|
||||
{...props}
|
||||
onChange={this._onChange}
|
||||
min='1'
|
||||
type='number'
|
||||
value={value == null ? '' : String(value / 1e3)}
|
||||
/>
|
||||
@@ -326,19 +336,22 @@ const constructPattern = ({ not, values } = EMPTY_OBJECT, valueTransform = ident
|
||||
export default class New extends Component {
|
||||
_getParams = createSelector(
|
||||
() => this.props.job,
|
||||
job => {
|
||||
() => this.props.schedule,
|
||||
(job, schedule) => {
|
||||
if (!job) {
|
||||
return { main: {}, vms: { vms: [] } }
|
||||
}
|
||||
|
||||
const { items } = job.paramsVector
|
||||
const enabled = schedule != null && schedule.enabled
|
||||
|
||||
// legacy backup jobs
|
||||
if (items.length === 1) {
|
||||
const { ...main } = items[0].values[0]
|
||||
|
||||
return {
|
||||
main,
|
||||
main: {
|
||||
enabled,
|
||||
...items[0].values[0]
|
||||
},
|
||||
vms: { vms: map(items[0].values.slice(1), extractId) }
|
||||
}
|
||||
}
|
||||
@@ -349,7 +362,10 @@ export default class New extends Component {
|
||||
const { $pool, tags } = pattern
|
||||
|
||||
return {
|
||||
main: items[0].values[0],
|
||||
main: {
|
||||
enabled,
|
||||
...items[0].values[0]
|
||||
},
|
||||
vms: {
|
||||
$pool: destructPattern($pool),
|
||||
power_state: pattern.power_state,
|
||||
@@ -360,7 +376,10 @@ export default class New extends Component {
|
||||
|
||||
// normal backup
|
||||
return {
|
||||
main: items[1].values[0],
|
||||
main: {
|
||||
enabled,
|
||||
...items[1].values[0]
|
||||
},
|
||||
vms: { vms: map(items[0].values, extractId) }
|
||||
}
|
||||
}
|
||||
@@ -402,7 +421,6 @@ export default class New extends Component {
|
||||
const vms = this._getVmsParam()
|
||||
|
||||
const job = {
|
||||
...props.job,
|
||||
...state.job,
|
||||
|
||||
type: 'call',
|
||||
@@ -481,6 +499,7 @@ export default class New extends Component {
|
||||
return editSchedule({
|
||||
id: props.schedule.id,
|
||||
cron: scheduling.cronPattern,
|
||||
enabled,
|
||||
timezone: scheduling.timezone
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import Component from 'base-component'
|
||||
import get from 'lodash/get'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import Tooltip from 'tooltip'
|
||||
@@ -15,11 +11,26 @@ import React from 'react'
|
||||
import xml2js from 'xml2js'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { confirm } from 'modal'
|
||||
import { deleteMessage, deleteVdi, deleteOrphanedVdis, deleteVm, isSrWritable } from 'xo'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { fromCallback } from 'promise-toolbox'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
deleteMessage,
|
||||
deleteOrphanedVdis,
|
||||
deleteVbd,
|
||||
deleteVdi,
|
||||
deleteVm,
|
||||
isSrWritable
|
||||
} from 'xo'
|
||||
import {
|
||||
flatten,
|
||||
get,
|
||||
isEmpty,
|
||||
map,
|
||||
mapValues
|
||||
} from 'lodash'
|
||||
import {
|
||||
createCollectionWrapper,
|
||||
createGetObject,
|
||||
createGetObjectsOfType,
|
||||
createSelector
|
||||
@@ -27,6 +38,7 @@ import {
|
||||
import {
|
||||
connectStore,
|
||||
formatSize,
|
||||
mapPlus,
|
||||
noop
|
||||
} from 'utils'
|
||||
|
||||
@@ -102,10 +114,21 @@ const SR_COLUMNS = [
|
||||
}
|
||||
]
|
||||
|
||||
const VDI_COLUMNS = [
|
||||
const ORPHANED_VDI_COLUMNS = [
|
||||
{
|
||||
name: _('snapshotDate'),
|
||||
itemRenderer: vdi => <span><FormattedTime value={vdi.snapshot_time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={vdi.snapshot_time * 1000} />)</span>,
|
||||
itemRenderer: vdi => <span>
|
||||
<FormattedTime
|
||||
day='numeric'
|
||||
hour='numeric'
|
||||
minute='numeric'
|
||||
month='long'
|
||||
value={vdi.snapshot_time * 1000}
|
||||
year='numeric'
|
||||
/>
|
||||
{' '}
|
||||
(<FormattedRelative value={vdi.snapshot_time * 1000} />)
|
||||
</span>,
|
||||
sortCriteria: vdi => vdi.snapshot_time,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
@@ -141,10 +164,58 @@ const VDI_COLUMNS = [
|
||||
}
|
||||
]
|
||||
|
||||
const CONTROL_DOMAIN_VDI_COLUMNS = [
|
||||
{
|
||||
name: _('vdiNameLabel'),
|
||||
itemRenderer: vdi => vdi && vdi.name_label,
|
||||
sortCriteria: vdi => vdi && vdi.name_label
|
||||
},
|
||||
{
|
||||
name: _('vdiNameDescription'),
|
||||
itemRenderer: vdi => vdi && vdi.name_description,
|
||||
sortCriteria: vdi => vdi && vdi.name_description
|
||||
},
|
||||
{
|
||||
name: _('vdiPool'),
|
||||
itemRenderer: vdi => vdi && vdi.pool && <Link to={`pools/${vdi.pool.id}`}>{vdi.pool.name_label}</Link>,
|
||||
sortCriteria: vdi => vdi && vdi.pool && vdi.pool.name_label
|
||||
},
|
||||
{
|
||||
name: _('vdiSize'),
|
||||
itemRenderer: vdi => vdi && formatSize(vdi.size),
|
||||
sortCriteria: vdi => vdi && vdi.size
|
||||
},
|
||||
{
|
||||
name: _('vdiSr'),
|
||||
itemRenderer: vdi => vdi && vdi.sr && <Link to={`srs/${vdi.sr.id}`}>{vdi.sr.name_label}</Link>,
|
||||
sortCriteria: vdi => vdi && vdi.sr && vdi.sr.name_label
|
||||
},
|
||||
{
|
||||
name: _('vdiAction'),
|
||||
itemRenderer: vdi => vdi && vdi.vbd && <ActionRowButton
|
||||
btnStyle='danger'
|
||||
handler={deleteVbd}
|
||||
handlerParam={vdi.vbd}
|
||||
icon='delete'
|
||||
/>
|
||||
}
|
||||
]
|
||||
|
||||
const VM_COLUMNS = [
|
||||
{
|
||||
name: _('snapshotDate'),
|
||||
itemRenderer: vm => <span><FormattedTime value={vm.snapshot_time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={vm.snapshot_time * 1000} />)</span>,
|
||||
itemRenderer: vm => <span>
|
||||
<FormattedTime
|
||||
day='numeric'
|
||||
hour='numeric'
|
||||
minute='numeric'
|
||||
month='long'
|
||||
value={vm.snapshot_time * 1000}
|
||||
year='numeric'
|
||||
/>
|
||||
{' '}
|
||||
(<FormattedRelative value={vm.snapshot_time * 1000} />)
|
||||
</span>,
|
||||
sortCriteria: vm => vm.snapshot_time,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
@@ -178,9 +249,18 @@ const VM_COLUMNS = [
|
||||
const ALARM_COLUMNS = [
|
||||
{
|
||||
name: _('alarmDate'),
|
||||
itemRenderer: message => (
|
||||
<span><FormattedTime value={message.time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={message.time * 1000} />)</span>
|
||||
),
|
||||
itemRenderer: message => <span>
|
||||
<FormattedTime
|
||||
day='numeric'
|
||||
hour='numeric'
|
||||
minute='numeric'
|
||||
month='long'
|
||||
value={message.time * 1000}
|
||||
year='numeric'
|
||||
/>
|
||||
{' '}
|
||||
(<FormattedRelative value={message.time * 1000} />)
|
||||
</span>,
|
||||
sortCriteria: message => message.time,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
@@ -224,8 +304,40 @@ const ALARM_COLUMNS = [
|
||||
|
||||
@connectStore(() => {
|
||||
const getOrphanVdiSnapshots = createGetObjectsOfType('VDI-snapshot')
|
||||
.filter([ snapshot => !snapshot.$snapshot_of ])
|
||||
.filter([ _ => !_.$snapshot_of && _.$VBDs.length === 0 ])
|
||||
.sort()
|
||||
const getControlDomainVbds = createGetObjectsOfType('VBD')
|
||||
.pick(
|
||||
createSelector(
|
||||
createGetObjectsOfType('VM-controller'),
|
||||
createCollectionWrapper(
|
||||
vmControllers => flatten(map(vmControllers, '$VBDs'))
|
||||
)
|
||||
)
|
||||
)
|
||||
.sort()
|
||||
const getControlDomainVdis = createSelector(
|
||||
getControlDomainVbds,
|
||||
createGetObjectsOfType('VDI'),
|
||||
createGetObjectsOfType('pool'),
|
||||
createGetObjectsOfType('SR'),
|
||||
(vbds, vdis, pools, srs) =>
|
||||
mapPlus(vbds, (vbd, push) => {
|
||||
const vdi = vdis[vbd.VDI]
|
||||
|
||||
if (vdi == null) {
|
||||
return
|
||||
}
|
||||
|
||||
push({
|
||||
...vdi,
|
||||
pool: pools[vbd.$pool],
|
||||
sr: srs[vdi.$SR],
|
||||
vbd
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
const getOrphanVmSnapshots = createGetObjectsOfType('VM-snapshot')
|
||||
.filter([ snapshot => !snapshot.$snapshot_of ])
|
||||
.sort()
|
||||
@@ -241,6 +353,7 @@ const ALARM_COLUMNS = [
|
||||
|
||||
return {
|
||||
alertMessages: getAlertMessages,
|
||||
controlDomainVdis: getControlDomainVdis,
|
||||
userSrs: getUserSrs,
|
||||
vdiOrphaned: getOrphanVdiSnapshots,
|
||||
vdiSr: getVdiSrs,
|
||||
@@ -362,7 +475,7 @@ export default class Health extends Component {
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<SortedTable collection={this.props.vdiOrphaned} columns={VDI_COLUMNS} />
|
||||
<SortedTable collection={this.props.vdiOrphaned} columns={ORPHANED_VDI_COLUMNS} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
@@ -371,6 +484,21 @@ export default class Health extends Component {
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<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} />
|
||||
}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<Card>
|
||||
|
||||
@@ -4,7 +4,7 @@ import ChartistGraph from 'react-chartist'
|
||||
import Component from 'base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import propTypes from 'prop-types'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import Link, { BlockLink } from 'link'
|
||||
import map from 'lodash/map'
|
||||
import HostsPatchesTable from 'hosts-patches-table'
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
createGetObjectsOfType,
|
||||
createGetHostMetrics,
|
||||
createSelector,
|
||||
createTop
|
||||
createTop,
|
||||
isAdmin
|
||||
} from 'selectors'
|
||||
import {
|
||||
connectStore,
|
||||
@@ -68,23 +69,19 @@ class PatchesCard extends Component {
|
||||
|
||||
const getHostMetrics = createGetHostMetrics(getHosts)
|
||||
|
||||
const userSrs = createTop(
|
||||
createGetObjectsOfType('SR').filter(
|
||||
[ isSrWritable ]
|
||||
),
|
||||
[ sr => sr.physical_usage / sr.size ],
|
||||
5
|
||||
const writableSrs = createGetObjectsOfType('SR').filter(
|
||||
[ isSrWritable ]
|
||||
)
|
||||
|
||||
const getSrMetrics = createCollectionWrapper(
|
||||
createSelector(
|
||||
userSrs,
|
||||
userSrs => {
|
||||
writableSrs,
|
||||
writableSrs => {
|
||||
const metrics = {
|
||||
srTotal: 0,
|
||||
srUsage: 0
|
||||
}
|
||||
forEach(userSrs, sr => {
|
||||
forEach(writableSrs, sr => {
|
||||
metrics.srUsage += sr.physical_usage
|
||||
metrics.srTotal += sr.size
|
||||
})
|
||||
@@ -136,13 +133,18 @@ class PatchesCard extends Component {
|
||||
return {
|
||||
hostMetrics: getHostMetrics,
|
||||
hosts: getHosts,
|
||||
isAdmin,
|
||||
nAlarmMessages: getNumberOfAlarmMessages,
|
||||
nHosts: getNumberOfHosts,
|
||||
nPools: getNumberOfPools,
|
||||
nTasks: getNumberOfTasks,
|
||||
nVms: getNumberOfVms,
|
||||
srMetrics: getSrMetrics,
|
||||
userSrs: userSrs,
|
||||
topWritableSrs: createTop(
|
||||
writableSrs,
|
||||
[ sr => sr.physical_usage / sr.size ],
|
||||
5
|
||||
),
|
||||
vmMetrics: getVmMetrics
|
||||
}
|
||||
})
|
||||
@@ -238,8 +240,8 @@ export default class Overview extends Component {
|
||||
/>
|
||||
<p className='text-xs-center'>
|
||||
{_('ofUsage', {
|
||||
total: `${props.vmMetrics.vcpus} vCPUs`,
|
||||
usage: `${props.hostMetrics.cpus} CPUs`
|
||||
total: `${props.hostMetrics.cpus} CPUs`,
|
||||
usage: `${props.vmMetrics.vcpus} vCPUs`
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
@@ -306,7 +308,10 @@ export default class Overview extends Component {
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<p className={styles.bigCardContent}>
|
||||
<Link to='/settings/users'>{nUsers}</Link>
|
||||
{props.isAdmin
|
||||
? <Link to='/settings/users'>{nUsers}</Link>
|
||||
: <p>{nUsers}</p>
|
||||
}
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
@@ -345,8 +350,8 @@ export default class Overview extends Component {
|
||||
<ChartistGraph
|
||||
style={{strokeWidth: '30px'}}
|
||||
data={{
|
||||
labels: map(props.userSrs, 'name_label'),
|
||||
series: map(props.userSrs, sr => (sr.physical_usage / sr.size) * 100)
|
||||
labels: map(props.topWritableSrs, 'name_label'),
|
||||
series: map(props.topWritableSrs, sr => (sr.physical_usage / sr.size) * 100)
|
||||
}}
|
||||
options={{ showLabel: false, showGrid: false, distributeSeries: true, high: 100 }}
|
||||
type='Bar'
|
||||
|
||||
@@ -5,7 +5,7 @@ import Component from 'base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import propTypes from 'prop-types'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
|
||||
@@ -79,7 +79,7 @@ class MiniStats extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
@connectStore(({
|
||||
@connectStore(() => ({
|
||||
container: createGetObject((_, props) => props.item.$pool),
|
||||
needsRestart: createDoesHostNeedRestart((_, props) => props.item),
|
||||
nVms: createGetObjectsOfType('VM').count(
|
||||
@@ -106,6 +106,7 @@ export default class HostItem extends Component {
|
||||
|
||||
render () {
|
||||
const { item: host, container, expandAll, selected, nVms } = this.props
|
||||
const toolTipContent = host.power_state === `Running` && !host.enabled ? `disabled` : _(`powerState${host.power_state}`)
|
||||
return <div className={styles.item}>
|
||||
<BlockLink to={`/hosts/${host.id}`}>
|
||||
<SingleLineRow>
|
||||
@@ -115,13 +116,15 @@ export default class HostItem extends Component {
|
||||
|
||||
<Tooltip
|
||||
content={isEmpty(host.current_operations)
|
||||
? _(`powerState${host.power_state}`)
|
||||
: <div>{_(`powerState${host.power_state}`)}{' ('}{map(host.current_operations)[0]}{')'}</div>
|
||||
? toolTipContent
|
||||
: <div>{toolTipContent}{' ('}{map(host.current_operations)[0]}{')'}</div>
|
||||
}
|
||||
>
|
||||
{isEmpty(host.current_operations)
|
||||
? <Icon icon={`${host.power_state.toLowerCase()}`} />
|
||||
: <Icon icon='busy' />
|
||||
{!isEmpty(host.current_operations)
|
||||
? <Icon icon='busy' />
|
||||
: (host.power_state === 'Running' && !host.enabled)
|
||||
? <Icon icon='disabled' />
|
||||
: <Icon icon={`${host.power_state.toLowerCase()}`} />
|
||||
}
|
||||
</Tooltip>
|
||||
|
||||
|
||||
@@ -42,12 +42,15 @@ import {
|
||||
forgetSrs,
|
||||
isSrShared,
|
||||
migrateVms,
|
||||
rebootVmGroups,
|
||||
reconnectAllHostsSrs,
|
||||
rescanSrs,
|
||||
restartHosts,
|
||||
restartHostsAgents,
|
||||
restartVms,
|
||||
shutdownVmGroups,
|
||||
snapshotVms,
|
||||
startVmGroups,
|
||||
startVms,
|
||||
stopHosts,
|
||||
stopVms,
|
||||
@@ -86,6 +89,7 @@ import {
|
||||
import styles from './index.css'
|
||||
import HostItem from './host-item'
|
||||
import PoolItem from './pool-item'
|
||||
import VmGroupItem from './vm-group-item'
|
||||
import VmItem from './vm-item'
|
||||
import TemplateItem from './template-item'
|
||||
import SrItem from './sr-item'
|
||||
@@ -150,6 +154,20 @@ const OPTIONS = {
|
||||
{ labelId: 'homeSortByCpus', sortBy: 'CPUs.number', sortOrder: 'desc' }
|
||||
]
|
||||
},
|
||||
'VmGroup': {
|
||||
defaultFilter: '',
|
||||
filters: homeFilters.vmGroup,
|
||||
mainActions: [
|
||||
{ handler: shutdownVmGroups, icon: 'vm-stop', tooltip: _('stopVmLabel') },
|
||||
{ handler: startVmGroups, icon: 'vm-start', tooltip: _('startVmLabel') },
|
||||
{ handler: rebootVmGroups, icon: 'vm-reboot', tooltip: _('rebootVmLabel') }
|
||||
],
|
||||
Item: VmGroupItem,
|
||||
sortOptions: [
|
||||
{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
|
||||
{ labelId: 'homeSortByPowerstate', sortBy: 'power_state', sortOrder: 'desc' }
|
||||
]
|
||||
},
|
||||
pool: {
|
||||
defaultFilter: '',
|
||||
filters: homeFilters.pool,
|
||||
@@ -196,6 +214,7 @@ const OPTIONS = {
|
||||
|
||||
const TYPES = {
|
||||
VM: _('homeTypeVm'),
|
||||
VmGroup: _('homeTypeVmGroup'),
|
||||
'VM-template': _('homeTypeVmTemplate'),
|
||||
host: _('homeTypeHost'),
|
||||
pool: _('homeTypePool'),
|
||||
@@ -212,14 +231,18 @@ const DEFAULT_TYPE = 'VM'
|
||||
createGetObjectsOfType('host'),
|
||||
hosts => state => isEmpty(hosts(state))
|
||||
)
|
||||
const type = (_, props) => props.location.query.t || DEFAULT_TYPE
|
||||
const getType = (_, props) => props.location.query.t || DEFAULT_TYPE
|
||||
const getObjectsByType = createGetObjectsOfType(getType)
|
||||
|
||||
return {
|
||||
areObjectsFetched,
|
||||
items: createGetObjectsOfType(type),
|
||||
noServersConnected,
|
||||
type,
|
||||
user: getUser
|
||||
return (state, props) => {
|
||||
const type = getType(state, props)
|
||||
return {
|
||||
areObjectsFetched: areObjectsFetched(state, props),
|
||||
items: getObjectsByType(state, props),
|
||||
noServersConnected: noServersConnected(state, props),
|
||||
type,
|
||||
user: getUser(state, props)
|
||||
}
|
||||
}
|
||||
})
|
||||
export default class Home extends Component {
|
||||
@@ -806,7 +829,7 @@ export default class Home extends Component {
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
{' '}
|
||||
<OverlayTrigger
|
||||
{type !== 'VmGroup' && <OverlayTrigger
|
||||
autoFocus
|
||||
trigger='click'
|
||||
rootClose
|
||||
@@ -824,7 +847,7 @@ export default class Home extends Component {
|
||||
}
|
||||
>
|
||||
<Button btnStyle='link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
|
||||
</OverlayTrigger>
|
||||
</OverlayTrigger> }
|
||||
{' '}
|
||||
<DropdownButton bsStyle='link' id='sort' title={_('homeSortBy')}>
|
||||
{map(options.sortOptions, ({ labelId, sortBy: _sortBy, sortOrder }, key) => (
|
||||
|
||||
77
src/xo-app/home/vm-group-item.js
Normal file
77
src/xo-app/home/vm-group-item.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Ellipsis, { EllipsisContainer } from 'ellipsis'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import Tooltip from 'tooltip'
|
||||
import { BlockLink } from 'link'
|
||||
import { Col } from 'grid'
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObject } from 'selectors'
|
||||
import { editVmGroup } from 'xo'
|
||||
import { Text } from 'editable'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@connectStore(() => {
|
||||
return (state, props) => {
|
||||
const vms = {}
|
||||
forEach(props.item.$VMs, vmId => {
|
||||
const getVM = createGetObject(() => vmId)
|
||||
vms[vmId] = getVM(state, props)
|
||||
})
|
||||
return {
|
||||
vms
|
||||
}
|
||||
}
|
||||
})
|
||||
export default class VmGroupItem extends Component {
|
||||
toggleState = stateField => () => this.setState({ [stateField]: !this.state[stateField] })
|
||||
_onSelect = () => this.props.onSelect(this.props.item.id)
|
||||
_setNameDescription = description => editVmGroup(this.props.item, {name_description: description})
|
||||
_setNameLabel = label => editVmGroup(this.props.item, {name_label: label})
|
||||
_getVmGroupState = (vmGroup) => {
|
||||
const states = map(this.props.vms, vm => vm.power_state)
|
||||
return isEmpty(states)
|
||||
? 'Busy'
|
||||
: states.indexOf('Halted') === -1
|
||||
? states[0]
|
||||
: states.indexOf('Running') === -1
|
||||
? states[0]
|
||||
: 'Busy'
|
||||
}
|
||||
|
||||
render () {
|
||||
const { item: vmGroup, selected, vms } = this.props
|
||||
return <div className={styles.item}>
|
||||
<BlockLink to={`/vm-group/${vmGroup.id}`}>
|
||||
<SingleLineRow>
|
||||
<Col smallSize={10} mediumSize={9} largeSize={3}>
|
||||
<EllipsisContainer>
|
||||
<input type='checkbox' checked={selected} onChange={this._onSelect} value={vmGroup.id} />
|
||||
|
||||
<Ellipsis>
|
||||
<Tooltip content={_(`powerStateVmGroup${this._getVmGroupState(vmGroup)}`)}>
|
||||
<Icon icon={isEmpty(vms) ? 'halted' : `${this._getVmGroupState(vmGroup).toLowerCase()}`} />
|
||||
</Tooltip>
|
||||
<Text value={vmGroup.name_label} onChange={this._setNameLabel} useLongClick />
|
||||
</Ellipsis>
|
||||
|
||||
</EllipsisContainer>
|
||||
</Col>
|
||||
<Col mediumSize={4} className='hidden-md-down'>
|
||||
<EllipsisContainer>
|
||||
<Ellipsis>
|
||||
<Text value={vmGroup.name_description} onChange={this._setNameDescription} useLongClick />
|
||||
</Ellipsis>
|
||||
</EllipsisContainer>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</BlockLink>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,9 @@ import {
|
||||
import {
|
||||
createFinder,
|
||||
createGetObject,
|
||||
createSelector
|
||||
createGetVmDisks,
|
||||
createSelector,
|
||||
createSumBy
|
||||
} from 'selectors'
|
||||
|
||||
import styles from './index.css'
|
||||
@@ -39,9 +41,13 @@ 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
|
||||
@@ -164,6 +170,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>
|
||||
|
||||
@@ -7,8 +7,14 @@ import Page from '../page'
|
||||
import React, { cloneElement, Component } from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Text } from 'editable'
|
||||
import { editHost, fetchHostStats, getHostMissingPatches, installAllHostPatches, installHostPatch } from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
editHost,
|
||||
fetchHostStats,
|
||||
installAllHostPatches,
|
||||
installHostPatch,
|
||||
subscribeHostMissingPatches
|
||||
} from 'xo'
|
||||
import {
|
||||
connectStore,
|
||||
routes
|
||||
@@ -139,6 +145,10 @@ export default class Host extends Component {
|
||||
}
|
||||
|
||||
loop (host = this.props.host) {
|
||||
if (host == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.cancel) {
|
||||
this.cancel()
|
||||
}
|
||||
@@ -166,22 +176,19 @@ export default class Host extends Component {
|
||||
}
|
||||
loop = ::this.loop
|
||||
|
||||
_getMissingPatches (host) {
|
||||
getHostMissingPatches(host).then(missingPatches => {
|
||||
this.setState({ missingPatches: sortBy(missingPatches, (patch) => -patch.time) })
|
||||
})
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
if (!this.props.host) {
|
||||
return
|
||||
}
|
||||
componentDidMount () {
|
||||
this.loop()
|
||||
this._getMissingPatches(this.props.host)
|
||||
this.unsubscribeHostMissingPatches = subscribeHostMissingPatches(
|
||||
this.props.routeParams.id,
|
||||
missingPatches => this.setState({
|
||||
missingPatches: sortBy(missingPatches, patch => -patch.time)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearTimeout(this.timeout)
|
||||
this.unsubscribeHostMissingPatches()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
@@ -195,10 +202,6 @@ export default class Host extends Component {
|
||||
this.context.router.push('/')
|
||||
}
|
||||
|
||||
if (!hostCur) {
|
||||
this._getMissingPatches(hostNext)
|
||||
}
|
||||
|
||||
if (!isRunning(hostCur) && isRunning(hostNext)) {
|
||||
this.loop(hostNext)
|
||||
} else if (isRunning(hostCur) && !isRunning(hostNext)) {
|
||||
@@ -210,16 +213,12 @@ export default class Host extends Component {
|
||||
|
||||
_installAllPatches = () => {
|
||||
const { host } = this.props
|
||||
return installAllHostPatches(host).then(() => {
|
||||
this._getMissingPatches(host)
|
||||
})
|
||||
return installAllHostPatches(host)
|
||||
}
|
||||
|
||||
_installPatch = patch => {
|
||||
const { host } = this.props
|
||||
return installHostPatch(host, patch).then(() => {
|
||||
this._getMissingPatches(host)
|
||||
})
|
||||
return installHostPatch(host, patch)
|
||||
}
|
||||
|
||||
_setNameDescription = nameDescription => editHost(this.props.host, { name_description: nameDescription })
|
||||
@@ -235,7 +234,7 @@ export default class Host extends Component {
|
||||
<Row>
|
||||
<Col mediumSize={6} className='header-title'>
|
||||
<h2>
|
||||
<Icon icon={`host-${host.power_state.toLowerCase()}`} />
|
||||
<Icon icon={host.power_state === 'Running' && !host.enabled ? 'host-disabled' : `host-${host.power_state.toLowerCase()}`} />
|
||||
{' '}
|
||||
<Text
|
||||
value={host.name_label}
|
||||
|
||||
@@ -85,11 +85,11 @@ export default ({
|
||||
<Usage total={host.memory.size}>
|
||||
<UsageElement
|
||||
highlight
|
||||
tooltip='XenServer'
|
||||
tooltip={`XenServer (${formatSize(vmController.memory.size)})`}
|
||||
value={vmController.memory.size}
|
||||
/>
|
||||
{map(vms, vm => <UsageElement
|
||||
tooltip={vm.name_label}
|
||||
tooltip={`${vm.name_label} (${formatSize(vm.memory.size)})`}
|
||||
key={vm.id}
|
||||
value={vm.memory.size}
|
||||
href={`#/vms/${vm.id}`}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { Component } from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { chooseAction } from 'modal'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createDoesHostNeedRestart, createSelector } from 'selectors'
|
||||
@@ -42,11 +43,10 @@ const MISSING_PATCH_COLUMNS = [
|
||||
},
|
||||
{
|
||||
name: _('patchAction'),
|
||||
itemRenderer: (patch, installPatch) => (
|
||||
itemRenderer: (patch, {installPatch, _installPatchWarning}) => (
|
||||
<ActionRowButton
|
||||
btnStyle='primary'
|
||||
handler={installPatch}
|
||||
handlerParam={patch}
|
||||
handler={() => _installPatchWarning(patch, installPatch)}
|
||||
icon='host-patch-update'
|
||||
/>
|
||||
)
|
||||
@@ -111,6 +111,29 @@ const INSTALLED_PATCH_COLUMNS_2 = [
|
||||
needsRestart: createDoesHostNeedRestart((_, props) => props.host)
|
||||
}))
|
||||
export default class HostPatches extends Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
|
||||
_chooseActionPatch = async doInstall => {
|
||||
const choice = await chooseAction({
|
||||
body: <p>{_('installPatchWarningContent')}</p>,
|
||||
buttons: [
|
||||
{ label: _('installPatchWarningResolve'), value: 'install', btnStyle: 'primary' },
|
||||
{ label: _('installPatchWarningReject'), value: 'goToPool' }
|
||||
],
|
||||
title: _('installPatchWarningTitle')
|
||||
})
|
||||
|
||||
return choice === 'install'
|
||||
? doInstall()
|
||||
: this.context.router.push(`/pools/${this.props.host.$pool}/patches`)
|
||||
}
|
||||
|
||||
_installPatchWarning = (patch, installPatch) => this._chooseActionPatch(() => installPatch(patch))
|
||||
|
||||
_installAllPatchesWarning = installAllPatches => this._chooseActionPatch(installAllPatches)
|
||||
|
||||
_getPatches = createSelector(
|
||||
() => this.props.host,
|
||||
() => this.props.hostPatches,
|
||||
@@ -136,7 +159,7 @@ export default class HostPatches extends Component {
|
||||
render () {
|
||||
const { host, missingPatches, installAllPatches, installPatch } = this.props
|
||||
const { patches, columns } = this._getPatches()
|
||||
|
||||
const hasMissingPatches = !isEmpty(missingPatches)
|
||||
return process.env.XOA_PLAN > 1
|
||||
? <Container>
|
||||
<Row>
|
||||
@@ -148,26 +171,20 @@ export default class HostPatches extends Component {
|
||||
icon='host-reboot'
|
||||
labelId='rebootUpdateHostLabel'
|
||||
/>}
|
||||
{isEmpty(missingPatches)
|
||||
? <TabButton
|
||||
disabled
|
||||
handler={installAllPatches}
|
||||
icon='success'
|
||||
labelId='hostUpToDate'
|
||||
/>
|
||||
: <TabButton
|
||||
btnStyle='primary'
|
||||
handler={installAllPatches}
|
||||
icon='host-patch-update'
|
||||
labelId='patchUpdateButton'
|
||||
/>
|
||||
}
|
||||
<TabButton
|
||||
disabled={!hasMissingPatches}
|
||||
btnStyle={hasMissingPatches ? 'primary' : undefined}
|
||||
handler={this._installAllPatchesWarning}
|
||||
handlerParam={installAllPatches}
|
||||
icon={hasMissingPatches ? 'host-patch-update' : 'success'}
|
||||
labelId={hasMissingPatches ? 'patchUpdateButton' : 'hostUpToDate'}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{!isEmpty(missingPatches) && <Row>
|
||||
{hasMissingPatches && <Row>
|
||||
<Col>
|
||||
<h3>{_('hostMissingPatches')}</h3>
|
||||
<SortedTable collection={missingPatches} userData={installPatch} columns={MISSING_PATCH_COLUMNS} />
|
||||
<SortedTable collection={missingPatches} userData={{installPatch, _installPatchWarning: this._installPatchWarning}} columns={MISSING_PATCH_COLUMNS} />
|
||||
</Col>
|
||||
</Row>}
|
||||
<Row>
|
||||
|
||||
@@ -36,6 +36,7 @@ import Sr from './sr'
|
||||
import Tasks from './tasks'
|
||||
import User from './user'
|
||||
import Vm from './vm'
|
||||
import VmGroup from './vm-group'
|
||||
import VmImport from './vm-import'
|
||||
import XoaUpdates from './xoa-updates'
|
||||
import Xosan from './xosan'
|
||||
@@ -86,6 +87,7 @@ const BODY_STYLE = {
|
||||
'vms/import': VmImport,
|
||||
'vms/new': NewVm,
|
||||
'vms/:id': Vm,
|
||||
'vm-group/:id': VmGroup,
|
||||
'xoa-update': XoaUpdates,
|
||||
'xosan': Xosan
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import Icon from 'icon'
|
||||
import includes from 'lodash/includes'
|
||||
import map from 'lodash/map'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import propTypes from 'prop-types'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React, { Component } from 'react'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
import SortedTable from 'sorted-table'
|
||||
@@ -71,20 +71,31 @@ class JobReturn extends Component {
|
||||
}
|
||||
|
||||
const Log = props => <ul className='list-group'>
|
||||
{map(props.log.calls, call => <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 /> ])}
|
||||
{call.returnedValue && <span>{' '}<JobReturn id={call.returnedValue} /></span>}
|
||||
{call.error &&
|
||||
<span className='text-danger'>
|
||||
<Icon icon='error' />
|
||||
{' '}
|
||||
{call.error.message
|
||||
? <strong>{call.error.message}</strong>
|
||||
: JSON.stringify(call.error)
|
||||
}
|
||||
</span>}
|
||||
</li>)}
|
||||
{map(props.log.calls, call => {
|
||||
const { returnedValue } = call
|
||||
let id
|
||||
if (returnedValue != null) {
|
||||
id = returnedValue.id
|
||||
if (id === undefined && typeof returnedValue === 'string') {
|
||||
id = returnedValue
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
</span>}
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
|
||||
const showCalls = log => alert(_('jobModalTitle', { job: log.jobId }), <Log log={log} />)
|
||||
|
||||
@@ -127,6 +127,7 @@ export default class Menu extends Component {
|
||||
const items = [
|
||||
{ to: '/home', icon: 'menu-home', label: 'homePage', subMenu: [
|
||||
{ to: '/home?t=VM', icon: 'vm', label: 'homeVmPage' },
|
||||
{ to: '/home?t=VmGroup', icon: 'vm', label: 'homeVmGroupPage' },
|
||||
nHosts !== 0 && { to: '/home?t=host', icon: 'host', label: 'homeHostPage' },
|
||||
!isEmpty(pools) && { to: '/home?t=pool', icon: 'pool', label: 'homePoolPage' },
|
||||
isAdmin && { to: '/home?t=VM-template', icon: 'template', label: 'homeTemplatePage' },
|
||||
@@ -162,11 +163,12 @@ export default class Menu extends Component {
|
||||
{ to: '/jobs/new', icon: 'menu-jobs-new', label: 'jobsNewPage' },
|
||||
{ to: '/jobs/schedules', icon: 'menu-jobs-schedule', label: 'jobsSchedulingPage' }
|
||||
]},
|
||||
{ to: '/about', icon: 'menu-about', label: 'aboutPage' },
|
||||
isAdmin && { to: '/about', icon: 'menu-about', label: 'aboutPage' },
|
||||
{ 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' },
|
||||
{ to: '/new/vm-group', icon: 'menu-new-vm', label: 'newVmGroupPage' },
|
||||
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' }
|
||||
@@ -197,7 +199,7 @@ export default class Menu extends Component {
|
||||
)}
|
||||
<li> </li>
|
||||
<li> </li>
|
||||
<li className='nav-item xo-menu-item'>
|
||||
{ (isAdmin || +process.env.XOA_PLAN === 5) && <li className='nav-item xo-menu-item'>
|
||||
<Link className='nav-link' style={{display: 'flex'}} to={'/about'}>
|
||||
{+process.env.XOA_PLAN === 5
|
||||
? <span>
|
||||
@@ -227,7 +229,7 @@ export default class Menu extends Component {
|
||||
</span>
|
||||
}
|
||||
</Link>
|
||||
</li>
|
||||
</li>}
|
||||
<li> </li>
|
||||
<li> </li>
|
||||
<li className='nav-item xo-menu-item'>
|
||||
|
||||
@@ -4,7 +4,6 @@ import BaseComponent from 'base-component'
|
||||
import Button from 'button'
|
||||
import classNames from 'classnames'
|
||||
import DebounceInput from 'react-debounce-input'
|
||||
import getEventValue from 'get-event-value'
|
||||
import Icon from 'icon'
|
||||
import isIp from 'is-ip'
|
||||
import Page from '../page'
|
||||
@@ -24,7 +23,6 @@ import {
|
||||
forEach,
|
||||
get,
|
||||
includes,
|
||||
isArray,
|
||||
isEmpty,
|
||||
join,
|
||||
map,
|
||||
@@ -70,6 +68,7 @@ import {
|
||||
connectStore,
|
||||
firstDefined,
|
||||
formatSize,
|
||||
getCoresPerSocketPossibilities,
|
||||
noop,
|
||||
resolveResourceSet
|
||||
} from 'utils'
|
||||
@@ -363,6 +362,7 @@ export default class NewVm extends BaseComponent {
|
||||
VIFs: _VIFs,
|
||||
resourceSet: resourceSet && resourceSet.id,
|
||||
// vm.set parameters
|
||||
coresPerSocket: state.coresPerSocket,
|
||||
CPUs: state.CPUs,
|
||||
cpuWeight: state.cpuWeight === '' ? null : state.cpuWeight,
|
||||
cpuCap: state.cpuCap === '' ? null : state.cpuCap,
|
||||
@@ -593,58 +593,19 @@ export default class NewVm extends BaseComponent {
|
||||
})
|
||||
)
|
||||
|
||||
_getCoresPerSocketPossibilities = createSelector(
|
||||
() => {
|
||||
const { pool } = this.props
|
||||
if (pool !== undefined) {
|
||||
return pool.cpus.cores
|
||||
}
|
||||
},
|
||||
() => this.state.state.CPUs,
|
||||
getCoresPerSocketPossibilities
|
||||
)
|
||||
|
||||
// On change -------------------------------------------------------------------
|
||||
/*
|
||||
* if index: the element to be modified should be an array/object
|
||||
* if stateObjectProp: the array/object contains objects and stateObjectProp needs to be modified
|
||||
* if targetObjectProp: the event target value is an object and the new value is the targetObjectProp of this object
|
||||
*
|
||||
* SCHEMA: EXAMPLE:
|
||||
*
|
||||
* state: { this.state.state: {
|
||||
* [prop]: { existingDisks: {
|
||||
* [index]: { 0: {
|
||||
* [stateObjectProp]: TO BE MODIFIED name_label: TO BE MODIFIED
|
||||
* ... name_description
|
||||
* } ...
|
||||
* ... }
|
||||
* } 1: {...}
|
||||
* ... }
|
||||
* } }
|
||||
*/
|
||||
_getOnChange (prop, index, stateObjectProp, targetObjectProp) {
|
||||
return event => {
|
||||
let value
|
||||
if (index !== undefined) { // The element should be an array or an object
|
||||
value = this.state.state[prop]
|
||||
value = isArray(value) ? [ ...value ] : { ...value } // Clone the element
|
||||
let eventValue = getEventValue(event)
|
||||
eventValue = targetObjectProp ? eventValue[targetObjectProp] : eventValue // Get the new value
|
||||
if (value[index] && stateObjectProp) {
|
||||
value[index][stateObjectProp] = eventValue
|
||||
} else {
|
||||
value[index] = eventValue
|
||||
}
|
||||
} else {
|
||||
value = getEventValue(event)
|
||||
}
|
||||
this._setState({ [prop]: value })
|
||||
}
|
||||
}
|
||||
_getOnChangeCheckbox (prop, index, stateObjectProp) {
|
||||
return event => {
|
||||
let value
|
||||
if (index !== undefined) {
|
||||
value = this.state.state[prop]
|
||||
value = [ ...value ]
|
||||
let eventValue = event.target.checked
|
||||
stateObjectProp ? value[index][stateObjectProp] = eventValue : value[index] = eventValue
|
||||
} else {
|
||||
value = event.target.checked
|
||||
}
|
||||
this._setState({ [prop]: value })
|
||||
}
|
||||
}
|
||||
|
||||
_onChangeSshKeys = keys => this._setState({ sshKeys: map(keys, key => key.id) })
|
||||
|
||||
_updateNbVms = () => {
|
||||
@@ -859,7 +820,7 @@ export default class NewVm extends BaseComponent {
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
onChange={this._getOnChange('name_label')}
|
||||
onChange={this._linkState('name_label')}
|
||||
value={name_label}
|
||||
/>
|
||||
</Item>
|
||||
@@ -867,7 +828,7 @@ export default class NewVm extends BaseComponent {
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
onChange={this._getOnChange('name_description')}
|
||||
onChange={this._linkState('name_description')}
|
||||
value={name_description}
|
||||
/>
|
||||
</Item>
|
||||
@@ -880,7 +841,8 @@ export default class NewVm extends BaseComponent {
|
||||
}
|
||||
|
||||
_renderPerformances = () => {
|
||||
const { CPUs, memoryDynamicMax } = this.state.state
|
||||
const { CPUs, memoryDynamicMax, coresPerSocket } = this.state.state
|
||||
|
||||
return <Section icon='new-vm-perf' title='newVmPerfPanel' done={this._isPerformancesDone()}>
|
||||
<SectionContent>
|
||||
<Item label={_('newVmVcpusLabel')}>
|
||||
@@ -888,7 +850,7 @@ export default class NewVm extends BaseComponent {
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
min={0}
|
||||
onChange={this._getOnChange('CPUs')}
|
||||
onChange={this._linkState('CPUs')}
|
||||
type='number'
|
||||
value={CPUs}
|
||||
/>
|
||||
@@ -900,6 +862,25 @@ export default class NewVm extends BaseComponent {
|
||||
value={firstDefined(memoryDynamicMax, null)}
|
||||
/>
|
||||
</Item>
|
||||
<Item label={_('vmCpuTopology')}>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this._linkState('coresPerSocket')}
|
||||
value={coresPerSocket}
|
||||
>
|
||||
{_('vmChooseCoresPerSocket', message => <option value=''>{message}</option>)}
|
||||
{map(
|
||||
this._getCoresPerSocketPossibilities(),
|
||||
coresPerSocket => _(
|
||||
'vmCoresPerSocket', {
|
||||
nSockets: CPUs / coresPerSocket,
|
||||
nCores: coresPerSocket
|
||||
},
|
||||
message => <option key={coresPerSocket} value={coresPerSocket}>{message}</option>
|
||||
)
|
||||
)}
|
||||
</select>
|
||||
</Item>
|
||||
</SectionContent>
|
||||
</Section>
|
||||
}
|
||||
@@ -938,7 +919,7 @@ export default class NewVm extends BaseComponent {
|
||||
<span className={styles.configDriveToggle}>
|
||||
<Toggle
|
||||
value={configDrive}
|
||||
onChange={this._getOnChange('configDrive')}
|
||||
onChange={this._linkState('configDrive')}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
@@ -949,7 +930,7 @@ export default class NewVm extends BaseComponent {
|
||||
checked={installMethod === 'SSH'}
|
||||
disabled={!configDrive}
|
||||
name='installMethod'
|
||||
onChange={this._getOnChange('installMethod')}
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='SSH'
|
||||
/>
|
||||
@@ -962,7 +943,7 @@ export default class NewVm extends BaseComponent {
|
||||
className='form-control'
|
||||
disabled={!configDrive || installMethod !== 'SSH'}
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
onChange={this._getOnChange('newSshKey')}
|
||||
onChange={this._linkState('newSshKey')}
|
||||
value={newSshKey}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
@@ -985,7 +966,7 @@ export default class NewVm extends BaseComponent {
|
||||
checked={installMethod === 'customConfig'}
|
||||
disabled={!configDrive}
|
||||
name='installMethod'
|
||||
onChange={this._getOnChange('installMethod')}
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='customConfig'
|
||||
/>
|
||||
@@ -997,7 +978,7 @@ export default class NewVm extends BaseComponent {
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
disabled={!configDrive || installMethod !== 'customConfig'}
|
||||
element='textarea'
|
||||
onChange={this._getOnChange('customConfig')}
|
||||
onChange={this._linkState('customConfig')}
|
||||
value={customConfig}
|
||||
/>
|
||||
</LineItem>
|
||||
@@ -1008,7 +989,7 @@ export default class NewVm extends BaseComponent {
|
||||
<input
|
||||
checked={installMethod === 'ISO'}
|
||||
name='installMethod'
|
||||
onChange={this._getOnChange('installMethod')}
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='ISO'
|
||||
/>
|
||||
@@ -1018,13 +999,13 @@ export default class NewVm extends BaseComponent {
|
||||
<span className={styles.inlineSelect}>
|
||||
{this.props.pool ? <SelectVdi
|
||||
disabled={installMethod !== 'ISO'}
|
||||
onChange={this._getOnChange('installIso')}
|
||||
onChange={this._linkState('installIso')}
|
||||
srPredicate={this._getIsoPredicate()}
|
||||
value={installIso}
|
||||
/>
|
||||
: <SelectResourceSetsVdi
|
||||
disabled={installMethod !== 'ISO'}
|
||||
onChange={this._getOnChange('installIso')}
|
||||
onChange={this._linkState('installIso')}
|
||||
resourceSet={this._getResolvedResourceSet()}
|
||||
srPredicate={this._getIsoPredicate()}
|
||||
value={installIso}
|
||||
@@ -1038,7 +1019,7 @@ export default class NewVm extends BaseComponent {
|
||||
<input
|
||||
checked={installMethod === 'network'}
|
||||
name='installMethod'
|
||||
onChange={this._getOnChange('installMethod')}
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='network'
|
||||
/>
|
||||
@@ -1050,7 +1031,7 @@ export default class NewVm extends BaseComponent {
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
disabled={installMethod !== 'network'}
|
||||
key='networkInput'
|
||||
onChange={this._getOnChange('installNetwork')}
|
||||
onChange={this._linkState('installNetwork')}
|
||||
placeholder={formatMessage(messages.newVmInstallNetworkPlaceHolder)}
|
||||
value={installNetwork}
|
||||
/>
|
||||
@@ -1059,7 +1040,7 @@ export default class NewVm extends BaseComponent {
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
onChange={this._getOnChange('pv_args')}
|
||||
onChange={this._linkState('pv_args')}
|
||||
value={pv_args}
|
||||
/>
|
||||
</Item>
|
||||
@@ -1068,7 +1049,7 @@ export default class NewVm extends BaseComponent {
|
||||
<input
|
||||
checked={installMethod === 'PXE'}
|
||||
name='installMethod'
|
||||
onChange={this._getOnChange('installMethod')}
|
||||
onChange={this._linkState('installMethod')}
|
||||
type='radio'
|
||||
value='PXE'
|
||||
/>
|
||||
@@ -1083,7 +1064,7 @@ export default class NewVm extends BaseComponent {
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
element='textarea'
|
||||
onChange={this._getOnChange('cloudConfig')}
|
||||
onChange={this._linkState('cloudConfig')}
|
||||
rows={7}
|
||||
value={cloudConfig}
|
||||
/>
|
||||
@@ -1167,12 +1148,12 @@ export default class NewVm extends BaseComponent {
|
||||
<Item label={_('newVmSrLabel')}>
|
||||
<span className={styles.inlineSelect}>
|
||||
{pool ? <SelectSr
|
||||
onChange={this._getOnChange('existingDisks', index, '$SR', 'id')}
|
||||
onChange={this._linkState(`existingDisks.${index}.$SR`, 'id')}
|
||||
predicate={this._getSrPredicate()}
|
||||
value={disk.$SR}
|
||||
/>
|
||||
: <SelectResourceSetsSr
|
||||
onChange={this._getOnChange('existingDisks', index, '$SR', 'id')}
|
||||
onChange={this._linkState(`existingDisks.${index}.$SR`, 'id')}
|
||||
predicate={this._getSrPredicate()}
|
||||
resourceSet={resourceSet}
|
||||
value={disk.$SR}
|
||||
@@ -1184,7 +1165,7 @@ export default class NewVm extends BaseComponent {
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
onChange={this._getOnChange('existingDisks', index, 'name_label')}
|
||||
onChange={this._linkState(`existingDisks.${index}.name_label`)}
|
||||
value={disk.name_label}
|
||||
/>
|
||||
</Item>
|
||||
@@ -1192,14 +1173,14 @@ export default class NewVm extends BaseComponent {
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
onChange={this._getOnChange('existingDisks', index, 'name_description')}
|
||||
onChange={this._linkState(`existingDisks.${index}.name_description`)}
|
||||
value={disk.name_description}
|
||||
/>
|
||||
</Item>
|
||||
<Item label={_('newVmSizeLabel')}>
|
||||
<SizeInput
|
||||
className={styles.sizeInput}
|
||||
onChange={this._getOnChange('existingDisks', index, 'size')}
|
||||
onChange={this._linkState(`existingDisks.${index}.size`)}
|
||||
readOnly={!configDrive}
|
||||
value={firstDefined(disk.size, null)}
|
||||
/>
|
||||
@@ -1214,12 +1195,12 @@ export default class NewVm extends BaseComponent {
|
||||
<Item label={_('newVmSrLabel')}>
|
||||
<span className={styles.inlineSelect}>
|
||||
{pool ? <SelectSr
|
||||
onChange={this._getOnChange('VDIs', index, 'SR', 'id')}
|
||||
onChange={this._linkState(`VDIs.${index}.SR`, 'id')}
|
||||
predicate={this._getSrPredicate()}
|
||||
value={vdi.SR}
|
||||
/>
|
||||
: <SelectResourceSetsSr
|
||||
onChange={this._getOnChange('VDIs', index, 'SR', 'id')}
|
||||
onChange={this._linkState(`VDIs.${index}.SR`, 'id')}
|
||||
predicate={this._getSrPredicate()}
|
||||
resourceSet={resourceSet}
|
||||
value={vdi.SR}
|
||||
@@ -1230,7 +1211,7 @@ export default class NewVm extends BaseComponent {
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
onChange={this._getOnChange('VDIs', index, 'name_label')}
|
||||
onChange={this._linkState(`VDIs.${index}.name_label`)}
|
||||
value={vdi.name_label}
|
||||
/>
|
||||
</Item>
|
||||
@@ -1238,14 +1219,14 @@ export default class NewVm extends BaseComponent {
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
onChange={this._getOnChange('VDIs', index, 'name_description')}
|
||||
onChange={this._linkState(`VDIs.${index}.name_description`)}
|
||||
value={vdi.name_description}
|
||||
/>
|
||||
</Item>
|
||||
<Item label={_('newVmSizeLabel')}>
|
||||
<SizeInput
|
||||
className={styles.sizeInput}
|
||||
onChange={this._getOnChange('VDIs', index, 'size')}
|
||||
onChange={this._linkState(`VDIs.${index}.size`)}
|
||||
value={firstDefined(vdi.size, null)}
|
||||
/>
|
||||
</Item>
|
||||
@@ -1308,7 +1289,7 @@ export default class NewVm extends BaseComponent {
|
||||
<Item>
|
||||
<input
|
||||
checked={bootAfterCreate}
|
||||
onChange={this._getOnChangeCheckbox('bootAfterCreate')}
|
||||
onChange={this._linkState('bootAfterCreate')}
|
||||
type='checkbox'
|
||||
/>
|
||||
|
||||
@@ -1317,7 +1298,7 @@ export default class NewVm extends BaseComponent {
|
||||
<Item>
|
||||
<input
|
||||
checked={autoPoweron}
|
||||
onChange={this._getOnChangeCheckbox('autoPoweron')}
|
||||
onChange={this._linkState('autoPoweron')}
|
||||
type='checkbox'
|
||||
/>
|
||||
|
||||
@@ -1331,7 +1312,7 @@ export default class NewVm extends BaseComponent {
|
||||
<Item>
|
||||
<input
|
||||
checked={share}
|
||||
onChange={this._getOnChangeCheckbox('share')}
|
||||
onChange={this._linkState('share')}
|
||||
type='checkbox'
|
||||
/>
|
||||
|
||||
@@ -1345,7 +1326,7 @@ export default class NewVm extends BaseComponent {
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
min={0}
|
||||
max={65535}
|
||||
onChange={this._getOnChange('cpuWeight')}
|
||||
onChange={this._linkState('cpuWeight')}
|
||||
placeholder={formatMessage(messages.newVmDefaultCpuWeight, { value: XEN_DEFAULT_CPU_WEIGHT })}
|
||||
type='number'
|
||||
value={cpuWeight}
|
||||
@@ -1356,7 +1337,7 @@ export default class NewVm extends BaseComponent {
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
min={0}
|
||||
onChange={this._getOnChange('cpuCap')}
|
||||
onChange={this._linkState('cpuCap')}
|
||||
placeholder={formatMessage(messages.newVmDefaultCpuCap, { value: XEN_DEFAULT_CPU_CAP })}
|
||||
type='number'
|
||||
value={cpuCap}
|
||||
@@ -1376,14 +1357,14 @@ export default class NewVm extends BaseComponent {
|
||||
</SectionContent>,
|
||||
<SectionContent>
|
||||
<Item label={_('newVmMultipleVms')}>
|
||||
<Toggle value={multipleVms} onChange={this._getOnChange('multipleVms')} />
|
||||
<Toggle value={multipleVms} onChange={this._linkState('multipleVms')} />
|
||||
</Item>
|
||||
<Item label={_('newVmMultipleVmsPattern')}>
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
disabled={!multipleVms}
|
||||
onChange={this._getOnChange('namePattern')}
|
||||
onChange={this._linkState('namePattern')}
|
||||
placeholder={formatMessage(messages.newVmMultipleVmsPatternPlaceholder)}
|
||||
value={namePattern}
|
||||
/>
|
||||
@@ -1393,7 +1374,7 @@ export default class NewVm extends BaseComponent {
|
||||
className={'form-control'}
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
disabled={!multipleVms}
|
||||
onChange={this._getOnChange('seqStart')}
|
||||
onChange={this._linkState('seqStart')}
|
||||
type='number'
|
||||
value={seqStart}
|
||||
/>
|
||||
@@ -1405,7 +1386,7 @@ export default class NewVm extends BaseComponent {
|
||||
disabled={!multipleVms}
|
||||
max={NB_VMS_MAX}
|
||||
min={NB_VMS_MIN}
|
||||
onChange={this._getOnChange('nbVms')}
|
||||
onChange={this._linkState('nbVms')}
|
||||
type='number'
|
||||
value={nbVms}
|
||||
/>
|
||||
@@ -1425,7 +1406,7 @@ export default class NewVm extends BaseComponent {
|
||||
{multipleVms && <LineItem>
|
||||
{map(nameLabels, (nameLabel, index) =>
|
||||
<Item key={`nameLabel_${index}`}>
|
||||
<input type='text' className='form-control' value={nameLabel} onChange={this._getOnChange('nameLabels', index)} />
|
||||
<input type='text' className='form-control' value={nameLabel} onChange={this._linkState(`nameLabels.${index}`)} />
|
||||
</Item>
|
||||
)}
|
||||
</LineItem>}
|
||||
@@ -1527,7 +1508,7 @@ export default class NewVm extends BaseComponent {
|
||||
<span style={{margin: 'auto'}}>
|
||||
<input
|
||||
checked={fastClone}
|
||||
onChange={this._getOnChangeCheckbox('fastClone')}
|
||||
onChange={this._linkState('fastClone')}
|
||||
type='checkbox'
|
||||
/>
|
||||
{' '}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { routes } from 'utils'
|
||||
|
||||
import Sr from './sr'
|
||||
import VmGroup from './vm-group'
|
||||
|
||||
const New = routes('vm', {
|
||||
sr: Sr
|
||||
sr: Sr,
|
||||
'vm-group': VmGroup
|
||||
})(
|
||||
({ children }) => children
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import info, { error } from 'notification'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import Page from '../../page'
|
||||
import propTypes from 'prop-types'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import store from 'store'
|
||||
import trim from 'lodash/trim'
|
||||
|
||||
153
src/xo-app/new/vm-group/index.js
Normal file
153
src/xo-app/new/vm-group/index.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import BaseComponent from 'base-component'
|
||||
import classNames from 'classnames'
|
||||
import DebounceInput from 'react-debounce-input'
|
||||
import Icon from 'icon'
|
||||
import Page from '../../page'
|
||||
import React from 'react'
|
||||
import Wizard, { Section } from 'wizard'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
createVmGroup,
|
||||
subscribeCurrentUser,
|
||||
subscribePermissions,
|
||||
subscribeResourceSets
|
||||
} from 'xo'
|
||||
import {
|
||||
createSelector,
|
||||
createGetObjectsOfType,
|
||||
getUser
|
||||
} from 'selectors'
|
||||
import { SelectPool } from 'select-objects'
|
||||
import { addSubscriptions, connectStore } from 'utils'
|
||||
|
||||
import styles from '../../new-vm/index.css'
|
||||
|
||||
const SectionContent = ({ column, children }) => (
|
||||
<div className={classNames(
|
||||
'form-inline',
|
||||
styles.sectionContent,
|
||||
column && styles.sectionContentColumn
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
const Item = ({ label, children, className }) => (
|
||||
<span className={styles.item}>
|
||||
{label && <span>{label} </span>}
|
||||
<span className={classNames(styles.input, className)}>{children}</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
@addSubscriptions({
|
||||
resourceSets: subscribeResourceSets,
|
||||
permissions: subscribePermissions,
|
||||
user: subscribeCurrentUser
|
||||
})
|
||||
@connectStore(() => ({
|
||||
isAdmin: createSelector(
|
||||
getUser,
|
||||
user => user && user.permission === 'admin'
|
||||
),
|
||||
pools: createGetObjectsOfType('pool')
|
||||
}))
|
||||
export default class VmGroup extends BaseComponent {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {name_label: '', name_description: ''}
|
||||
}
|
||||
|
||||
_selectPool = pool => {
|
||||
this.setState({ pool })
|
||||
this._reset()
|
||||
}
|
||||
|
||||
_getCanOperate = createSelector(
|
||||
() => this.props.isAdmin,
|
||||
() => this.props.permissions,
|
||||
(isAdmin, permissions) => isAdmin
|
||||
? () => true
|
||||
: ({ id }) => permissions && permissions[id] && permissions[id].operate
|
||||
)
|
||||
|
||||
_renderHeader = () => {
|
||||
const { pool } = this.state
|
||||
return <Container>
|
||||
<Row>
|
||||
<Col mediumSize={12}>
|
||||
<h2><Icon icon='sr' /> {_('newVmGroupTitle')}
|
||||
<SelectPool
|
||||
onChange={this._selectPool}
|
||||
predicate={this._getCanOperate()}
|
||||
value={pool}
|
||||
/>
|
||||
</h2>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
}
|
||||
_reset = () => this.setState({poolId: '', name_label: '', name_description: ''})
|
||||
_create = () => {
|
||||
createVmGroup(this.state)
|
||||
this.context.router.push('home?s=&t=VmGroup')
|
||||
}
|
||||
_getOnChange = item => ({target}) => this.setState({[item]: target.value})
|
||||
|
||||
render () {
|
||||
const {pool, name_label: nameLabel, name_description: nameDescription} = this.state
|
||||
return (
|
||||
<Page header={this._renderHeader()}>
|
||||
<form id='vmGroupCreation'>
|
||||
<Wizard>
|
||||
<Section icon='new-vm-infos' title='newVmGroupInfoPanel'>
|
||||
<SectionContent>
|
||||
<Item label={_('newVmGroupNameLabel')}>
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
// debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
onChange={this._getOnChange('name_label')}
|
||||
value={nameLabel}
|
||||
/>
|
||||
</Item>
|
||||
<Item label={_('newVmGroupDescriptionLabel')}>
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
// debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
onChange={this._getOnChange('name_description')}
|
||||
value={nameDescription}
|
||||
/>
|
||||
</Item>
|
||||
</SectionContent>
|
||||
</Section>
|
||||
</Wizard>
|
||||
<div className={styles.submitSection}>
|
||||
<ActionButton
|
||||
className={styles.button}
|
||||
handler={this._reset}
|
||||
icon='new-vm-reset'
|
||||
>
|
||||
{_('newVmGroupReset')}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className={styles.button}
|
||||
disabled={nameLabel === '' || pool === undefined}
|
||||
form='vmGroupCreation'
|
||||
handler={this._create}
|
||||
icon='new-vm-create'
|
||||
redirectOnSuccess={this._getRedirectionUrl}
|
||||
>
|
||||
{_('newVmGroupCreate')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</form>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export default ({
|
||||
<Col smallOffset={1} mediumSize={10}>
|
||||
<Usage total={sumBy(hosts, 'memory.size')}>
|
||||
{map(hosts, host => <UsageElement
|
||||
tooltip={host.name_label}
|
||||
tooltip={`${host.name_label} (${formatSize(host.memory.usage)})`}
|
||||
key={host.id}
|
||||
value={host.memory.usage}
|
||||
href={`#/hosts/${host.id}`}
|
||||
|
||||
@@ -5,7 +5,7 @@ import includes from 'lodash/includes'
|
||||
import intersection from 'lodash/intersection'
|
||||
import keyBy from 'lodash/keyBy'
|
||||
import map from 'lodash/map'
|
||||
import propTypes from 'prop-types'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React, { Component } from 'react'
|
||||
import reduce from 'lodash/reduce'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
|
||||
@@ -14,7 +14,7 @@ import isEmpty from 'lodash/isEmpty'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import mapKeys from 'lodash/mapKeys'
|
||||
import propTypes from 'prop-types'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import remove from 'lodash/remove'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
@@ -357,6 +357,7 @@ export class Edit extends Component {
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<SelectSubject
|
||||
hasSelectAll
|
||||
multi
|
||||
onChange={this.linkState('subjects')}
|
||||
required
|
||||
@@ -365,6 +366,7 @@ export class Edit extends Component {
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<SelectPool
|
||||
hasSelectAll
|
||||
multi
|
||||
onChange={this._updateSelectedPools}
|
||||
required
|
||||
@@ -378,6 +380,7 @@ export class Edit extends Component {
|
||||
<Col mediumSize={4}>
|
||||
<SelectVmTemplate
|
||||
disabled={!state.nPools}
|
||||
hasSelectAll
|
||||
multi
|
||||
onChange={this.linkState('templates')}
|
||||
predicate={state.vmTemplatePredicate}
|
||||
@@ -388,6 +391,7 @@ export class Edit extends Component {
|
||||
<Col mediumSize={4}>
|
||||
<SelectSr
|
||||
disabled={!state.nPools}
|
||||
hasSelectAll
|
||||
multi
|
||||
onChange={this._updateSelectedSrs}
|
||||
predicate={state.srPredicate}
|
||||
@@ -398,6 +402,7 @@ export class Edit extends Component {
|
||||
<Col mediumSize={4}>
|
||||
<SelectNetwork
|
||||
disabled={!state.nSrs}
|
||||
hasSelectAll
|
||||
multi
|
||||
onChange={this._updateSelectedNetworks}
|
||||
predicate={state.networkPredicate}
|
||||
|
||||
@@ -5,7 +5,7 @@ import includes from 'lodash/includes'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import keyBy from 'lodash/keyBy'
|
||||
import map from 'lodash/map'
|
||||
import propTypes from 'prop-types'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import size from 'lodash/size'
|
||||
import SortedTable from 'sorted-table'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,16 +3,16 @@ import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import StateButton from 'state-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import { alert } from 'modal'
|
||||
import { alert, confirm } from 'modal'
|
||||
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 {
|
||||
addServer,
|
||||
editServer,
|
||||
@@ -36,9 +36,31 @@ export default class Servers extends Component {
|
||||
this.setState({ label: '', host: '', password: '', username: '' })
|
||||
}
|
||||
|
||||
_showError = error => alert(
|
||||
error.code === 'SESSION_AUTHENTICATION_FAILED' ? _('serverAuthFailed') : error.code || _('serverUnknownError'),
|
||||
error.message
|
||||
_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 () {
|
||||
@@ -60,6 +82,21 @@ export default class Servers extends Component {
|
||||
<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>
|
||||
@@ -112,9 +149,8 @@ export default class Servers extends Component {
|
||||
{server.error &&
|
||||
<Tooltip content={_('serverConnectionFailed')}>
|
||||
<a
|
||||
className='text-danger btn btn-link'
|
||||
style={{ padding: '0px' }}
|
||||
onClick={() => this._showError(server.error)}
|
||||
className='text-danger btn btn-link btn-sm'
|
||||
onClick={() => this._showError(server)}
|
||||
>
|
||||
<Icon
|
||||
icon='alarm'
|
||||
@@ -125,15 +161,18 @@ export default class Servers extends Component {
|
||||
}
|
||||
</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'
|
||||
style={{
|
||||
marginRight: '0.5em'
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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,67 +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 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)
|
||||
@@ -136,8 +92,8 @@ import TabXosan from './tab-xosan'
|
||||
pbds: getPbds(state, props),
|
||||
logs: getLogs(state, props),
|
||||
vdis: getVdis(state, props),
|
||||
unmanagedVdis: getUnmanagedVdis(state, props),
|
||||
vdiSnapshots: getVdiSnapshots(state, props),
|
||||
vdisToVmIds: getVdisToVmIds(state, props),
|
||||
sr
|
||||
}
|
||||
}
|
||||
@@ -216,8 +172,8 @@ export default class Sr extends Component {
|
||||
'pbds',
|
||||
'sr',
|
||||
'vdis',
|
||||
'vdiSnapshots',
|
||||
'vdisToVmIds'
|
||||
'unmanagedVdis',
|
||||
'vdiSnapshots'
|
||||
]))
|
||||
return <Page header={this.header()} title={`${sr.name_label}${container ? ` (${container.name_label})` : ''}`}>
|
||||
{cloneElement(this.props.children, childProps)}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
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 { createGetObject, createSelector } from 'selectors'
|
||||
import { deleteVdi, editVdi } from 'xo'
|
||||
import { renderXoItemFromId } from 'render-xo-item'
|
||||
import { Text } from 'editable'
|
||||
|
||||
// ===================================================================
|
||||
@@ -37,20 +39,38 @@ 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()
|
||||
}
|
||||
|
||||
return <Link to={`/vms/${
|
||||
vm.type === 'VM-snapshot'
|
||||
? `${vm.$snapshot_of}/snapshots`
|
||||
: vm.id
|
||||
}`}>
|
||||
{renderXoItem(vm)}
|
||||
</Link>
|
||||
})
|
||||
},
|
||||
{
|
||||
name: _('vdiTags'),
|
||||
@@ -75,21 +95,39 @@ const COLUMNS = [
|
||||
]
|
||||
|
||||
const FILTERS = {
|
||||
filterNoSnapshots: '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, vdiSnapshots, vdisToVmIds }) => (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
{!isEmpty(vdis)
|
||||
? <SortedTable collection={vdis.concat(vdiSnapshots)} 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}
|
||||
/>
|
||||
: <h4 className='text-xs-center'>{_('srNoVdis')}</h4>
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +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,
|
||||
vdisToVmIds
|
||||
vdiSnapshots,
|
||||
unmanagedVdis
|
||||
}) => <Container>
|
||||
<Row className='text-xs-center'>
|
||||
<Col mediumSize={4}>
|
||||
@@ -34,13 +46,21 @@ export default ({
|
||||
<Row>
|
||||
<Col smallOffset={1} mediumSize={10}>
|
||||
<Usage total={sr.size}>
|
||||
{map(unmanagedVdis, vdi => <UsageElement
|
||||
highlight
|
||||
key={vdi.id}
|
||||
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>
|
||||
|
||||
@@ -42,7 +42,7 @@ export const TaskItem = connectStore(() => ({
|
||||
host: createGetObject((_, props) => props.task.$host)
|
||||
}))(({ task, host }) => <SingleLineRow className='mb-1'>
|
||||
<Col mediumSize={6}>
|
||||
{task.name_label} (on {host
|
||||
{task.name_label} ({task.name_description && `${task.name_description} `}on {host
|
||||
? <Link to={`/hosts/${host.id}`}>{host.name_label}</Link>
|
||||
: `unknown host − ${task.$host}`
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import propTypes from 'prop-types'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import { Text } from 'editable'
|
||||
import { alert } from 'modal'
|
||||
|
||||
53
src/xo-app/vm-group/action-bar.js
Normal file
53
src/xo-app/vm-group/action-bar.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import ActionBar from 'action-bar'
|
||||
import React from 'react'
|
||||
import { connectStore } from 'utils'
|
||||
import { includes } from 'lodash'
|
||||
import { isAdmin } from 'selectors'
|
||||
import {
|
||||
rebootVmGroup,
|
||||
startVmGroup,
|
||||
shutdownVmGroup
|
||||
} from 'xo'
|
||||
|
||||
const vmGroupActionBarByState = ({ isAdmin, vmGroup }) => {
|
||||
const actions = []
|
||||
if (vmGroup.allowed_operations.includes('start') || vmGroup.allowed_operations.length === 0) {
|
||||
actions.push({
|
||||
icon: 'vm-start',
|
||||
label: 'startVmLabel',
|
||||
handler: startVmGroup,
|
||||
pending: includes(vmGroup.current_operations, 'start')
|
||||
})
|
||||
}
|
||||
if (vmGroup.allowed_operations.includes('shutdown') || vmGroup.allowed_operations.length === 0) {
|
||||
actions.push({
|
||||
icon: 'vm-stop',
|
||||
label: 'stopVmLabel',
|
||||
handler: shutdownVmGroup,
|
||||
pending: includes(vmGroup.current_operations, 'shutdown')
|
||||
})
|
||||
}
|
||||
actions.push({
|
||||
icon: 'vm-reboot',
|
||||
label: 'rebootVmLabel',
|
||||
handler: rebootVmGroup,
|
||||
pending: includes(vmGroup.current_operations, 'shutdown') || includes(vmGroup.current_operations, 'start')
|
||||
})
|
||||
return <ActionBar
|
||||
actions={actions}
|
||||
display='icon'
|
||||
param={vmGroup}
|
||||
/>
|
||||
}
|
||||
|
||||
const VmGroupActionBar = connectStore({
|
||||
isAdmin
|
||||
})(({ isAdmin, vmGroup }) => {
|
||||
const ActionBar = vmGroupActionBarByState
|
||||
if (!ActionBar) {
|
||||
return <p>No action bar for state</p>
|
||||
}
|
||||
|
||||
return <ActionBar isAdmin={isAdmin} vmGroup={vmGroup} />
|
||||
})
|
||||
export default VmGroupActionBar
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user