Compare commits
75 Commits
v5.1.3
...
xo-web/v5.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bcb65c518 | ||
|
|
25361fa7eb | ||
|
|
889a265000 | ||
|
|
3122f6dcd5 | ||
|
|
16aa2e8085 | ||
|
|
074d51a670 | ||
|
|
2122a79132 | ||
|
|
26dbc585ba | ||
|
|
4b3cfbd424 | ||
|
|
035191a2cc | ||
|
|
06a40180a1 | ||
|
|
aaf4c5dff7 | ||
|
|
0c83bc2b0e | ||
|
|
2d412fd8db | ||
|
|
443e2bec25 | ||
|
|
d5e1323d82 | ||
|
|
7f0b77cc89 | ||
|
|
0169cff66c | ||
|
|
0fd1424a41 | ||
|
|
6280d56f32 | ||
|
|
9f2a77872f | ||
|
|
b571c18e9a | ||
|
|
49863d6e4d | ||
|
|
48cc7bb647 | ||
|
|
442d42d8dc | ||
|
|
9501ebacfc | ||
|
|
23f9fa46f8 | ||
|
|
1bd0f37fd4 | ||
|
|
ed74ded923 | ||
|
|
b732410b74 | ||
|
|
a51f2b7fcf | ||
|
|
fe12bbb60d | ||
|
|
8882df7939 | ||
|
|
185a554cd9 | ||
|
|
230e0dc2a5 | ||
|
|
f5b69fdfdc | ||
|
|
01dc0d8f1e | ||
|
|
8035886a3c | ||
|
|
0ab5f4b13f | ||
|
|
a1bc98def8 | ||
|
|
868cf6140b | ||
|
|
4b3473f480 | ||
|
|
7bc782cc62 | ||
|
|
e625a53e4a | ||
|
|
b31185d96d | ||
|
|
09d75e972f | ||
|
|
f33568951b | ||
|
|
8d8c442be5 | ||
|
|
f890b8ea7a | ||
|
|
1b80b3929c | ||
|
|
4f946293f6 | ||
|
|
36788cde2b | ||
|
|
1547c99e5a | ||
|
|
5c9606dad8 | ||
|
|
fdcb1dccf5 | ||
|
|
12812b8c23 | ||
|
|
0098497255 | ||
|
|
6562d2de7f | ||
|
|
1f0e88cdb0 | ||
|
|
197da91ef3 | ||
|
|
cbd59789e2 | ||
|
|
190ecf3d74 | ||
|
|
15b8f6bca2 | ||
|
|
5b406d731b | ||
|
|
4be9e67ac4 | ||
|
|
d047421685 | ||
|
|
f6f415a421 | ||
|
|
edfaaebac0 | ||
|
|
67df22a1bf | ||
|
|
7dc59a00f6 | ||
|
|
6214fe4c2e | ||
|
|
21610c3e0a | ||
|
|
87550b0189 | ||
|
|
b7c42d0a08 | ||
|
|
c15ad299ac |
73
CHANGELOG.md
73
CHANGELOG.md
@@ -1,5 +1,78 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.2.0** (2016-09-09)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- IP management [\#1350](https://github.com/vatesfr/xo-web/issues/1350), [\#988](https://github.com/vatesfr/xo-web/issues/988), [\#1427](https://github.com/vatesfr/xo-web/issues/1427) and [\#240](https://github.com/vatesfr/xo-web/issues/240)
|
||||
- Update reverse proxy example [\#1474](https://github.com/vatesfr/xo-web/issues/1474)
|
||||
- Improve log view [\#1467](https://github.com/vatesfr/xo-web/issues/1467)
|
||||
- Backup Reports: e-mail subject [\#1463](https://github.com/vatesfr/xo-web/issues/1463)
|
||||
- Backup Reports: report the error [\#1462](https://github.com/vatesfr/xo-web/issues/1462)
|
||||
- Vif selector: select management network by default [\#1425](https://github.com/vatesfr/xo-web/issues/1425)
|
||||
- Display when browser disconnected to server [\#1417](https://github.com/vatesfr/xo-web/issues/1417)
|
||||
- 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)
|
||||
- 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)
|
||||
- Pool name modification [\#1390](https://github.com/vatesfr/xo-web/issues/1390)
|
||||
- Confirmation dialog before destroying VDIs [\#1388](https://github.com/vatesfr/xo-web/issues/1388)
|
||||
- Tooltips for meter object [\#1387](https://github.com/vatesfr/xo-web/issues/1387)
|
||||
- New Host assistant [\#1374](https://github.com/vatesfr/xo-web/issues/1374)
|
||||
- New VM assistant [\#1373](https://github.com/vatesfr/xo-web/issues/1373)
|
||||
- New SR assistant [\#1372](https://github.com/vatesfr/xo-web/issues/1372)
|
||||
- Direct access to VDI listing from dashboard's SR usage breakdown [\#1371](https://github.com/vatesfr/xo-web/issues/1371)
|
||||
- Can't set a network name at pool level [\#1368](https://github.com/vatesfr/xo-web/issues/1368)
|
||||
- Change a few mouse over descriptions [\#1363](https://github.com/vatesfr/xo-web/issues/1363)
|
||||
- Hide network install in VM create if template is HVM [\#1362](https://github.com/vatesfr/xo-web/issues/1362)
|
||||
- SR space left during VM creation [\#1358](https://github.com/vatesfr/xo-web/issues/1358)
|
||||
- Add destination SR on migration modal in VM view [\#1357](https://github.com/vatesfr/xo-web/issues/1357)
|
||||
- Ability to create a new VM from a snapshot [\#1353](https://github.com/vatesfr/xo-web/issues/1353)
|
||||
- Missing explanation/confirmation on Snapshot Page [\#1349](https://github.com/vatesfr/xo-web/issues/1349)
|
||||
- Log view: expose API errors in the web UI [\#1344](https://github.com/vatesfr/xo-web/issues/1344)
|
||||
- Registration on update page [\#1341](https://github.com/vatesfr/xo-web/issues/1341)
|
||||
- Add export snapshot button [\#1336](https://github.com/vatesfr/xo-web/issues/1336)
|
||||
- Use saved SSH keys in VM create CloudConfig [\#1319](https://github.com/vatesfr/xo-web/issues/1319)
|
||||
- Collapse header in console view [\#1268](https://github.com/vatesfr/xo-web/issues/1268)
|
||||
- Two max concurrent jobs in parallel [\#915](https://github.com/vatesfr/xo-web/issues/915)
|
||||
- Handle OVA import via the web UI [\#709](https://github.com/vatesfr/xo-web/issues/709)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Bug on VM console when header is hidden [\#1485](https://github.com/vatesfr/xo-web/issues/1485)
|
||||
- Disks not removed when deleting multiple VMs [\#1484](https://github.com/vatesfr/xo-web/issues/1484)
|
||||
- Do not display VDI disconnect button when a VM is not running [\#1470](https://github.com/vatesfr/xo-web/issues/1470)
|
||||
- Do not display VIF disconnect button when a VM is not running [\#1468](https://github.com/vatesfr/xo-web/issues/1468)
|
||||
- Error on migration if no default SR \(even when not used\) [\#1466](https://github.com/vatesfr/xo-web/issues/1466)
|
||||
- DR issue while rotating old backup [\#1464](https://github.com/vatesfr/xo-web/issues/1464)
|
||||
- Giving resource set to end-user ends with error [\#1448](https://github.com/vatesfr/xo-web/issues/1448)
|
||||
- Error thrown when cancelling out of Delete User confirmation dialog [\#1439](https://github.com/vatesfr/xo-web/issues/1439)
|
||||
- Wrong month label shown in Backup and Job scheduler [\#1438](https://github.com/vatesfr/xo-web/issues/1438)
|
||||
- Bug on Self service creation/edition [\#1438](https://github.com/vatesfr/xo-web/issues/1428)
|
||||
- ISO selection during VM create is not mounted after [\#1415](https://github.com/vatesfr/xo-web/issues/1415)
|
||||
- Hosts general view: bad link for storage [\#1408](https://github.com/vatesfr/xo-web/issues/1408)
|
||||
- Backup Schedule - "Month" and "Day of Week" display error [\#1404](https://github.com/vatesfr/xo-web/issues/1404)
|
||||
- Migrate dialog doesn't present all available VIF's in new UI interface [\#1403](https://github.com/vatesfr/xo-web/issues/1403)
|
||||
- NFS mount issues [\#1396](https://github.com/vatesfr/xo-web/issues/1396)
|
||||
- Select component color [\#1391](https://github.com/vatesfr/xo-web/issues/1391)
|
||||
- SR created with local path shouldn't be shared [\#1389](https://github.com/vatesfr/xo-web/issues/1389)
|
||||
- Disks (VBD) are attached to VM in RO mode instead of RW even if RO is unchecked [\#1386](https://github.com/vatesfr/xo-web/issues/1386)
|
||||
- Re-connection issues between server and XS hosts [\#1384](https://github.com/vatesfr/xo-web/issues/1384)
|
||||
- Meter object style with Chrome 52 [\#1383](https://github.com/vatesfr/xo-web/issues/1383)
|
||||
- Editing a rolling snapshot job seems to fail [\#1376](https://github.com/vatesfr/xo-web/issues/1376)
|
||||
- Dashboard SR usage and total inverted [\#1370](https://github.com/vatesfr/xo-web/issues/1370)
|
||||
- XenServer connection issue with host while using VGPUs [\#1369](https://github.com/vatesfr/xo-web/issues/1369)
|
||||
- Job created with v4 are not correctly displayed in v5 [\#1366](https://github.com/vatesfr/xo-web/issues/1366)
|
||||
- CPU accounting in resource set [\#1365](https://github.com/vatesfr/xo-web/issues/1365)
|
||||
- Tooltip stay displayed when a button change state [\#1360](https://github.com/vatesfr/xo-web/issues/1360)
|
||||
- Failure on host reboot [\#1351](https://github.com/vatesfr/xo-web/issues/1351)
|
||||
- Editing Backup Jobs Without Compression, Slider Always Set To On [\#1339](https://github.com/vatesfr/xo-web/issues/1339)
|
||||
- Month Selection on Backup Screen Wrong [\#1338](https://github.com/vatesfr/xo-web/issues/1338)
|
||||
- Delta backup fail when removed VDIs [\#1333](https://github.com/vatesfr/xo-web/issues/1333)
|
||||
|
||||
## **5.1.0** (2016-07-26)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -330,7 +330,6 @@ gulp.task(function server (done) {
|
||||
address = '[' + address + ']'
|
||||
}
|
||||
|
||||
/* jshint devel: true*/
|
||||
console.log('Listening on http://' + address + ':' + port)
|
||||
})
|
||||
.on('error', done)
|
||||
|
||||
16
package.json
16
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.1.3",
|
||||
"version": "5.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -33,7 +33,7 @@
|
||||
"devDependencies": {
|
||||
"ansi_up": "^1.3.0",
|
||||
"asap": "^2.0.4",
|
||||
"ava": "^0.15.0",
|
||||
"ava": "^0.16.0",
|
||||
"babel-eslint": "^6.0.0",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-react-constant-elements": "^6.5.0",
|
||||
@@ -48,7 +48,7 @@
|
||||
"bootstrap": "github:twbs/bootstrap#v4-dev",
|
||||
"browserify": "^13.0.0",
|
||||
"bundle-collapser": "^1.2.1",
|
||||
"chartist-plugin-legend": "^0.3.1",
|
||||
"chartist-plugin-legend": "^0.5.0",
|
||||
"chartist-plugin-tooltip": "0.0.11",
|
||||
"classnames": "^2.2.3",
|
||||
"connect": "^3.4.1",
|
||||
@@ -57,6 +57,7 @@
|
||||
"dependency-check": "^2.5.1",
|
||||
"font-awesome": "^4.5.0",
|
||||
"font-mfizz": "github:fizzed/font-mfizz",
|
||||
"get-stream": "^2.3.0",
|
||||
"ghooks": "^1.1.1",
|
||||
"globby": "^6.0.0",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
@@ -70,17 +71,18 @@
|
||||
"gulp-uglify": "^2.0.0",
|
||||
"gulp-watch": "^4.3.5",
|
||||
"human-format": "^0.6.0",
|
||||
"is-ip": "^1.0.0",
|
||||
"jsonrpc-websocket-client": "0.0.1-5",
|
||||
"later": "^1.2.0",
|
||||
"lodash": "^4.6.1",
|
||||
"loose-envify": "^1.1.0",
|
||||
"marked": "^0.3.5",
|
||||
"modular-css": "^0.25.0",
|
||||
"modular-css": "^0.27.1",
|
||||
"moment": "^2.13.0",
|
||||
"moment-timezone": "^0.5.4",
|
||||
"notifyjs": "^2.0.1",
|
||||
"novnc-node": "^0.5.3",
|
||||
"promise-toolbox": "^0.4.0",
|
||||
"promise-toolbox": "^0.5.0",
|
||||
"random-password": "^0.1.2",
|
||||
"react": "^15.0.0",
|
||||
"react-addons-shallow-compare": "^15.1.0",
|
||||
@@ -109,10 +111,12 @@
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.2.1",
|
||||
"serve-static": "^1.10.2",
|
||||
"standard": "^7.0.0",
|
||||
"standard": "^8.0.0",
|
||||
"superagent": "^2.0.0",
|
||||
"tar-stream": "^1.5.2",
|
||||
"vinyl": "^1.1.1",
|
||||
"watchify": "^3.7.0",
|
||||
"xml2js": "^0.4.17",
|
||||
"xo-acl-resolver": "^0.2.1",
|
||||
"xo-lib": "^0.8.0",
|
||||
"xo-remote-parser": "^0.3"
|
||||
|
||||
@@ -6,17 +6,21 @@ import Tooltip from 'tooltip'
|
||||
import {
|
||||
ButtonGroup
|
||||
} from 'react-bootstrap-4/lib'
|
||||
import {
|
||||
noop
|
||||
} from 'utils'
|
||||
|
||||
const ActionBar = ({ actions, param }) => (
|
||||
<ButtonGroup>
|
||||
{map(actions, ({ handler, handlerParam = param, label, icon }, index) => (
|
||||
{map(actions, ({ handler, handlerParam = param, label, icon, redirectOnSuccess }, index) => (
|
||||
<Tooltip key={index} content={_(label)}>
|
||||
<ActionButton
|
||||
key={index}
|
||||
btnStyle='secondary'
|
||||
handler={handler}
|
||||
handler={handler || noop}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
redirectOnSuccess={redirectOnSuccess}
|
||||
size='large'
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -28,7 +32,8 @@ ActionBar.propTypes = {
|
||||
React.PropTypes.shape({
|
||||
label: React.PropTypes.string.isRequired,
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
handler: React.PropTypes.func
|
||||
handler: React.PropTypes.func,
|
||||
redirectOnSuccess: React.PropTypes.string
|
||||
})
|
||||
).isRequired,
|
||||
display: React.PropTypes.oneOf(['icon', 'text', 'both'])
|
||||
|
||||
@@ -1,10 +1,36 @@
|
||||
import clone from 'lodash/clone'
|
||||
import includes from 'lodash/includes'
|
||||
import isArray from 'lodash/isArray'
|
||||
import forEach from 'lodash/forEach'
|
||||
import map from 'lodash/map'
|
||||
import { Component } from 'react'
|
||||
|
||||
import getEventValue from './get-event-value'
|
||||
import invoke from './invoke'
|
||||
import shallowEqual from './shallow-equal'
|
||||
|
||||
const cowSet = (object, path, value, depth) => {
|
||||
if (depth >= path.length) {
|
||||
return value
|
||||
}
|
||||
|
||||
object = clone(object)
|
||||
const prop = path[depth]
|
||||
object[prop] = cowSet(object[prop], path, value, depth + 1)
|
||||
return object
|
||||
}
|
||||
|
||||
const get = (object, path, depth) => {
|
||||
if (depth >= path.length) {
|
||||
return object
|
||||
}
|
||||
|
||||
const prop = path[depth++]
|
||||
return isArray(object) && prop === '*'
|
||||
? map(object, value => get(value, path, depth))
|
||||
: get(object[prop], path, depth)
|
||||
}
|
||||
|
||||
export default class BaseComponent extends Component {
|
||||
constructor (props, context) {
|
||||
super(props, context)
|
||||
@@ -24,7 +50,42 @@ export default class BaseComponent extends Component {
|
||||
}
|
||||
|
||||
// See https://preactjs.com/guide/linked-state
|
||||
linkState (name) {
|
||||
linkState (name, targetPath) {
|
||||
const key = targetPath
|
||||
? `${name}##${targetPath}`
|
||||
: name
|
||||
|
||||
let linkedState = this._linkedState
|
||||
let cb
|
||||
if (!linkedState) {
|
||||
linkedState = this._linkedState = {}
|
||||
} else if ((cb = linkedState[key])) {
|
||||
return cb
|
||||
}
|
||||
|
||||
let getValue
|
||||
if (targetPath) {
|
||||
const path = targetPath.split('.')
|
||||
getValue = event => get(getEventValue(event), path, 0)
|
||||
} else {
|
||||
getValue = getEventValue
|
||||
}
|
||||
|
||||
if (includes(name, '.')) {
|
||||
const path = name.split('.')
|
||||
return (linkedState[key] = event => {
|
||||
this.setState(cowSet(this.state, path, getValue(event), 0))
|
||||
})
|
||||
}
|
||||
|
||||
return (linkedState[key] = event => {
|
||||
this.setState({
|
||||
[name]: getValue(event)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
toggleState (name) {
|
||||
let linkedState = this._linkedState
|
||||
let cb
|
||||
if (!linkedState) {
|
||||
@@ -33,9 +94,16 @@ export default class BaseComponent extends Component {
|
||||
return cb
|
||||
}
|
||||
|
||||
return (linkedState[name] = event => {
|
||||
if (includes(name, '.')) {
|
||||
const path = name.split('.')
|
||||
return (linkedState[path] = event => {
|
||||
this.setState(cowSet(this.state, path, !get(this.state, path), 0))
|
||||
})
|
||||
}
|
||||
|
||||
return (linkedState[name] = () => {
|
||||
this.setState({
|
||||
[name]: getEventValue(event)
|
||||
[name]: !this.state[name]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import _ from 'intl'
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import classNames from 'classnames'
|
||||
import Tooltip from 'tooltip'
|
||||
import React, { createElement } from 'react'
|
||||
|
||||
import Icon from '../icon'
|
||||
@@ -18,10 +20,12 @@ const Copiable = propTypes({
|
||||
},
|
||||
props.children,
|
||||
' ',
|
||||
<CopyToClipboard text={props.data || props.children}>
|
||||
<button className={classNames('btn btn-sm btn-secondary', styles.button)}>
|
||||
<Icon icon='clipboard' />
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
<Tooltip content={_('copyToClipboard')}>
|
||||
<CopyToClipboard text={props.data || props.children}>
|
||||
<button className={classNames('btn btn-sm btn-secondary', styles.button)}>
|
||||
<Icon icon='clipboard' />
|
||||
</button>
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
))
|
||||
export { Copiable as default }
|
||||
|
||||
@@ -15,6 +15,7 @@ import { formatSize } from './utils'
|
||||
import { SizeInput } from './form'
|
||||
import {
|
||||
SelectHost,
|
||||
SelectIp,
|
||||
SelectNetwork,
|
||||
SelectPool,
|
||||
SelectRemote,
|
||||
@@ -373,6 +374,7 @@ export class Select extends Editable {
|
||||
|
||||
const MAP_TYPE_SELECT = {
|
||||
host: SelectHost,
|
||||
ip: SelectIp,
|
||||
network: SelectNetwork,
|
||||
pool: SelectPool,
|
||||
remote: SelectRemote,
|
||||
|
||||
@@ -99,12 +99,9 @@ export class Range extends Component {
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
const { onChange } = this.props
|
||||
this.state.value = +value
|
||||
|
||||
if (onChange) {
|
||||
onChange(value)
|
||||
}
|
||||
this.setState({
|
||||
value: +value
|
||||
})
|
||||
}
|
||||
|
||||
_handleChange = event => {
|
||||
|
||||
@@ -102,6 +102,7 @@ export default class Select extends Component {
|
||||
return (
|
||||
<ReactSelect
|
||||
{...this.props}
|
||||
backspaceToRemoveMessage=''
|
||||
menuRenderer={this._renderMenu}
|
||||
menuStyle={SELECT_MENU_STYLE}
|
||||
style={SELECT_STYLE}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import { Portal } from 'react-overlays'
|
||||
@@ -19,7 +20,8 @@ import {
|
||||
} from './selectors'
|
||||
import {
|
||||
getHostMissingPatches,
|
||||
installAllHostPatches
|
||||
installAllHostPatches,
|
||||
installAllPatchesOnPool
|
||||
} from './xo'
|
||||
|
||||
// ===================================================================
|
||||
@@ -84,9 +86,17 @@ class HostsPatchesTable extends Component {
|
||||
)
|
||||
)
|
||||
|
||||
_installAllMissingPatches = () => (
|
||||
Promise.all(map(this._getHosts(), this._installAllHostPatches))
|
||||
)
|
||||
_installAllMissingPatches = () => {
|
||||
const pools = {}
|
||||
forEach(this._getHosts(), host => {
|
||||
pools[host.$pool] = true
|
||||
})
|
||||
|
||||
return Promise.all(map(
|
||||
keys(pools),
|
||||
installAllPatchesOnPool
|
||||
)).then(this._refreshMissingPatches)
|
||||
}
|
||||
|
||||
_refreshHostMissingPatches = host => (
|
||||
getHostMissingPatches(host).then(patches => {
|
||||
|
||||
@@ -8,6 +8,9 @@ addLocaleData(reactIntlData)
|
||||
// ===================================================================
|
||||
|
||||
export default {
|
||||
statusConnecting: 'Connexion…',
|
||||
statusDisconnected: 'Déconnecté',
|
||||
|
||||
editableLongClickPlaceholder: 'Clic long pour éditer',
|
||||
editableClickPlaceholder: 'Cliquer pour éditer',
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// See http://momentjs.com/docs/#/use-it/browserify/
|
||||
// See http://momentjs.com/docs/#/use-it/browserify/
|
||||
import 'moment/locale/pt'
|
||||
|
||||
import reactIntlData from 'react-intl/locale-data/pt'
|
||||
|
||||
@@ -5,6 +5,9 @@ var forEach = require('lodash/forEach')
|
||||
var isString = require('lodash/isString')
|
||||
|
||||
var messages = {
|
||||
statusConnecting: 'Connecting',
|
||||
statusDisconnected: 'Disconnected',
|
||||
|
||||
editableLongClickPlaceholder: 'Long click to edit',
|
||||
editableClickPlaceholder: 'Click to edit',
|
||||
|
||||
@@ -17,6 +20,12 @@ var messages = {
|
||||
onError: 'On error',
|
||||
successful: 'Successful',
|
||||
|
||||
// ----- Copiable component -----
|
||||
copyToClipboard: 'Copy to clipboard',
|
||||
|
||||
// ----- Pills -----
|
||||
pillMaster: 'Master',
|
||||
|
||||
// ----- Titles -----
|
||||
homePage: 'Home',
|
||||
homeVmPage: 'VMs',
|
||||
@@ -39,6 +48,8 @@ var messages = {
|
||||
settingsGroupsPage: 'Groups',
|
||||
settingsAclsPage: 'ACLs',
|
||||
settingsPluginsPage: 'Plugins',
|
||||
settingsLogsPage: 'Logs',
|
||||
settingsIpsPage: 'IPs',
|
||||
aboutPage: 'About',
|
||||
newMenu: 'New',
|
||||
taskMenu: 'Tasks',
|
||||
@@ -112,6 +123,7 @@ var messages = {
|
||||
homeMore: 'More',
|
||||
homeMigrateTo: 'Migrate to…',
|
||||
homeMissingPaths: 'Missing patches',
|
||||
homePoolMaster: 'Master:',
|
||||
highAvailability: 'High Availability',
|
||||
|
||||
// ----- Forms -----
|
||||
@@ -134,12 +146,14 @@ var messages = {
|
||||
selectResourceSetsSr: 'Select SR(s)…',
|
||||
selectResourceSetsNetwork: 'Select network(s)…',
|
||||
selectResourceSetsVdi: 'Select disk(s)…',
|
||||
selectSshKey: 'Select SSH key(s)…',
|
||||
selectSrs: 'Select SR(s)…',
|
||||
selectVms: 'Select VM(s)…',
|
||||
selectVmTemplates: 'Select VM template(s)…',
|
||||
selectTags: 'Select tag(s)…',
|
||||
selectVdis: 'Select disk(s)…',
|
||||
selectTimezone: 'Select timezone…',
|
||||
selectIp: 'Select IP(s)...',
|
||||
fillRequiredInformations: 'Fill required informations.',
|
||||
fillOptionalInformations: 'Fill informations (optional)',
|
||||
selectTableReset: 'Reset',
|
||||
@@ -329,6 +343,9 @@ var messages = {
|
||||
srForget: 'Forget this SR',
|
||||
srRemoveButton: 'Remove this SR',
|
||||
srNoVdis: 'No VDIs in this storage',
|
||||
// ----- Pool general -----
|
||||
poolRamUsage: '{used} used on {total}',
|
||||
poolMaster: 'Master:',
|
||||
// ----- Pool tabs -----
|
||||
hostsTabName: 'Hosts',
|
||||
// ----- Pool advanced tab -----
|
||||
@@ -340,6 +357,7 @@ var messages = {
|
||||
hostDescription: 'Description',
|
||||
hostMemory: 'Memory',
|
||||
noHost: 'No hosts',
|
||||
memoryLeftTooltip: '{used}% used ({free} free)',
|
||||
// ----- Pool network tab -----
|
||||
poolNetworkNameLabel: 'Name',
|
||||
poolNetworkDescription: 'Description',
|
||||
@@ -400,6 +418,8 @@ var messages = {
|
||||
pifStatusConnected: 'Connected',
|
||||
pifStatusDisconnected: 'Disconnected',
|
||||
pifNoInterface: 'No physical interface detected',
|
||||
pifInUse: 'This interface is currently in use',
|
||||
defaultLockingMode: 'Default locking mode',
|
||||
// ----- Host storage tabs -----
|
||||
addSrDeviceButton: 'Add a storage',
|
||||
srNameLabel: 'Name',
|
||||
@@ -476,6 +496,8 @@ var messages = {
|
||||
ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
|
||||
tipLabel: 'Tip:',
|
||||
tipConsoleLabel: 'non-US keyboard could have issues with console: switch your own layout to US.',
|
||||
hideHeaderTooltip: 'Hide infos',
|
||||
showHeaderTooltip: 'Show infos',
|
||||
|
||||
// ----- VM disk tab -----
|
||||
vdiAction: 'Action',
|
||||
@@ -506,6 +528,10 @@ var messages = {
|
||||
vifStatusDisconnected: 'Disconnected',
|
||||
vifIpAddresses: 'IP addresses',
|
||||
vifMacAutoGenerate: 'Auto-generated if empty',
|
||||
vifAllowedIps: 'Allowed IPs',
|
||||
vifNoIps: 'No IPs',
|
||||
vifLockedNetwork: 'Network is locked',
|
||||
vifLockedNetworkNoIps: 'Network is locked and no IPs are allowed for this interface',
|
||||
|
||||
// ----- VM snapshot tab -----
|
||||
noSnapshots: 'No snapshots',
|
||||
@@ -513,6 +539,8 @@ var messages = {
|
||||
tipCreateSnapshotLabel: 'Just click on the snapshot button to create one!',
|
||||
revertSnapshot: 'Revert VM to this snapshot',
|
||||
deleteSnapshot: 'Remove this snapshot',
|
||||
copySnapshot: 'Create a VM from this snapshot',
|
||||
exportSnapshot: 'Export this snapshot',
|
||||
snapshotDate: 'Creation date',
|
||||
snapshotName: 'Name',
|
||||
snapshotAction: 'Action',
|
||||
@@ -618,6 +646,7 @@ var messages = {
|
||||
alarmObject: 'Issue on',
|
||||
alarmPool: 'Pool',
|
||||
alarmRemoveAll: 'Remove all alarms',
|
||||
spaceLeftTooltip: '{used}% used ({free} left)',
|
||||
|
||||
// ----- New VM -----
|
||||
newVmCreateNewVmOn: 'Create a new VM on {select}',
|
||||
@@ -664,6 +693,8 @@ var messages = {
|
||||
newVmMultipleVmsPattern: 'Name pattern:',
|
||||
newVmMultipleVmsPatternPlaceholder: 'e.g.: \\{name\\}_%',
|
||||
newVmFirstIndex: 'First index:',
|
||||
newVmNumberRecalculate: 'Recalculate VMs number',
|
||||
newVmNameRefresh: 'Refresh VMs name',
|
||||
|
||||
// ----- Self -----
|
||||
resourceSets: 'Resource sets',
|
||||
@@ -695,7 +726,7 @@ var messages = {
|
||||
usedResource: 'Used:',
|
||||
|
||||
// ---- VM import ---
|
||||
importVmsList: 'Try dropping some backups here, or click to select backups to upload. Accept only .xva files.',
|
||||
importVmsList: 'Try dropping some VMs files here, or click to select VMs to upload. Accept only .xva/.ova files.',
|
||||
noSelectedVms: 'No selected VMs.',
|
||||
vmImportToPool: 'To Pool:',
|
||||
vmImportToSr: 'To SR:',
|
||||
@@ -705,6 +736,17 @@ var messages = {
|
||||
vmImportFailed: 'VM import failed',
|
||||
startVmImport: 'Import starting…',
|
||||
startVmExport: 'Export starting…',
|
||||
nCpus: 'N CPUs',
|
||||
vmMemory: 'Memory',
|
||||
diskInfo: 'Disk {position} ({capacity})',
|
||||
diskDescription: 'Disk description',
|
||||
noDisks: 'No disks.',
|
||||
noNetworks: 'No networks.',
|
||||
networkInfo: 'Network {name}',
|
||||
noVmImportErrorDescription: 'No description available',
|
||||
vmImportError: 'Error:',
|
||||
vmImportFileType: '{type} file:',
|
||||
vmImportConfigAlert: 'Please to check and/or modify the VM configuration.',
|
||||
|
||||
// ---- Tasks ---
|
||||
noTasks: 'No pending tasks',
|
||||
@@ -724,7 +766,8 @@ var messages = {
|
||||
lastBackupColumn: 'Last Backup',
|
||||
availableBackupsColumn: 'Available Backups',
|
||||
restoreColumn: 'Restore',
|
||||
restoreTip: 'Restore VM',
|
||||
restoreTip: 'View restore options',
|
||||
displayBackup: 'Display backups',
|
||||
importBackupTitle: 'Import VM',
|
||||
importBackupMessage: 'Starting your backup import',
|
||||
vmsToBackup: 'VMs to backup',
|
||||
@@ -734,6 +777,8 @@ var messages = {
|
||||
emergencyShutdownHostsModalMessage: 'Are you sure you want to shutdown {nHosts} Host{nHosts, plural, one {} other {s}}?',
|
||||
stopHostModalTitle: 'Shutdown host',
|
||||
stopHostModalMessage: 'This will shutdown your host. Do you want to continue?',
|
||||
addHostModalTitle: 'Add host',
|
||||
addHostModalMessage: 'Are you sure you want to add {host} to {pool}?',
|
||||
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}}',
|
||||
@@ -754,10 +799,10 @@ var messages = {
|
||||
restartVmsModalMessage: 'Are you sure you want to restart {vms} 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}}?',
|
||||
deleteVmModalTitle: 'Delete VM',
|
||||
deleteVmsModalTitle: 'Delete VM{vms, plural, one {} other {s}}',
|
||||
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
|
||||
deleteVmsModalMessage: 'Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
|
||||
deleteVmModalTitle: 'Delete VM',
|
||||
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
|
||||
migrateVmModalTitle: 'Migrate VM',
|
||||
migrateVmSelectHost: 'Select a destination host:',
|
||||
migrateVmSelectMigrationNetwork: 'Select a migration network:',
|
||||
@@ -773,6 +818,8 @@ var messages = {
|
||||
migrateVmNetwork: 'Network',
|
||||
migrateVmNoTargetHost: 'No target host',
|
||||
migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
|
||||
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',
|
||||
revertVmModalMessage: 'You are about to revert your VM to the snapshot state. This operation is irreversible',
|
||||
importBackupModalTitle: 'Import a {name} Backup',
|
||||
@@ -797,6 +844,8 @@ var messages = {
|
||||
serverPassword: 'Password',
|
||||
serverAction: 'Action',
|
||||
serverReadOnly: 'Read Only',
|
||||
serverConnect: 'Connect server',
|
||||
serverDisconnect: 'Disconnect server',
|
||||
|
||||
// ----- Copy VM -----
|
||||
copyVm: 'Copy VM',
|
||||
@@ -810,6 +859,11 @@ var messages = {
|
||||
copyVmsNoTargetSr: 'No target SR',
|
||||
copyVmsNoTargetSrMessage: 'A target SR is required to copy a VM',
|
||||
|
||||
// ----- Detach host -----
|
||||
detachHostModalTitle: 'Detach host',
|
||||
detachHostModalMessage: 'Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST.',
|
||||
detachHost: 'Detach',
|
||||
|
||||
// ----- Network -----
|
||||
newNetworkCreate: 'Create network',
|
||||
newNetworkInterface: 'Interface',
|
||||
@@ -821,6 +875,12 @@ var messages = {
|
||||
newNetworkDefaultMtu: 'Default: 1500',
|
||||
deleteNetwork: 'Delete network',
|
||||
deleteNetworkConfirm: 'Are you sure you want to delete this network?',
|
||||
networkInUse: 'This network is currently in use',
|
||||
|
||||
// ----- Add host -----
|
||||
addHostSelectHost: 'Host',
|
||||
addHostNoHost: 'No host',
|
||||
addHostNoHostMessage: 'No host selected to be added',
|
||||
|
||||
// ----- About View -----
|
||||
xenOrchestra: 'Xen Orchestra',
|
||||
@@ -856,13 +916,16 @@ var messages = {
|
||||
registration: 'Registration',
|
||||
trial: 'Trial',
|
||||
settings: 'Settings',
|
||||
proxySettings: 'Proxy settings',
|
||||
update: 'Update',
|
||||
refresh: 'Refresh',
|
||||
upgrade: 'Upgrade',
|
||||
noUpdaterCommunity: 'No updater available for Community Edition',
|
||||
noUpdaterSubscribe: 'Please consider subscribe and try it with all features for free during 15 days on',
|
||||
noUpdaterWarning: 'Manual update could break your current installation due to dependencies issues, do it with caution',
|
||||
currentVersion: 'Current version:',
|
||||
register: 'Register',
|
||||
editRegistration: 'Edit registration',
|
||||
trialRegistration: 'Please, take time to register in order to enjoy your trial.',
|
||||
trialStartButton: 'Start trial',
|
||||
trialAvailableUntil: 'You can use a trial version until {date, date, medium}. Upgrade your appliance to get it.',
|
||||
@@ -918,7 +981,36 @@ var messages = {
|
||||
deleteSshKeyConfirmMessage: 'Are you sure you want to delete the SSH key {title}?',
|
||||
|
||||
// ----- Usage -----
|
||||
others: 'Others'
|
||||
others: 'Others',
|
||||
|
||||
// ----- Logs -----
|
||||
loadingLogs: 'Loading logs...',
|
||||
logUser: 'User',
|
||||
logMethod: 'Method',
|
||||
logParams: 'Params',
|
||||
logMessage: 'Message',
|
||||
logStack: 'Stack trace',
|
||||
logDisplayDetails: 'Display details',
|
||||
logShowParams: 'ShowParams',
|
||||
logTime: 'Date',
|
||||
logNoStackTrace: 'No stack trace',
|
||||
logNoParams: 'No params',
|
||||
logDeleteAll: 'Delete all logs',
|
||||
logDeleteAllTitle: 'Delete all logs',
|
||||
logDeleteAllMessage: 'Are you sure you want to delete all the logs?',
|
||||
|
||||
// ----- IPs ------
|
||||
ipPoolName: 'Name',
|
||||
ipPoolIps: 'IPs',
|
||||
ipPoolIpsPlaceholder: 'IPs (e.g.: 1.0.0.12-1.0.0.17;1.0.0.23)',
|
||||
ipPoolNetworks: 'Networks',
|
||||
ipsNoIpPool: 'No IP pools',
|
||||
ipsCreate: 'Create',
|
||||
ipsDeleteAllTitle: 'Delete all IP pools',
|
||||
ipsDeleteAllMessage: 'Are you sure you want to delete all the IP pools?',
|
||||
ipsVifs: 'VIFs',
|
||||
ipsNotUsed: 'Not used'
|
||||
|
||||
}
|
||||
forEach(messages, function (message, id) {
|
||||
if (isString(message)) {
|
||||
|
||||
126
src/common/ip.js
Normal file
126
src/common/ip.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import forEachRight from 'lodash/forEachRight'
|
||||
import forEach from 'lodash/forEach'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isIp from 'is-ip'
|
||||
import some from 'lodash/some'
|
||||
|
||||
export { isIp }
|
||||
export const isIpV4 = isIp.v4
|
||||
export const isIpV6 = isIp.v6
|
||||
|
||||
// Source: https://github.com/ezpaarse-project/ip-range-generator/blob/master/index.js
|
||||
|
||||
const ipv4 = /^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(?:\.(?!$)|$)){4}$/
|
||||
|
||||
function ip2hex (ip) {
|
||||
let parts = ip.split('.').map(str => parseInt(str, 10))
|
||||
let n = 0
|
||||
|
||||
n += parts[3]
|
||||
n += parts[2] * 256 // 2^8
|
||||
n += parts[1] * 65536 // 2^16
|
||||
n += parts[0] * 16777216 // 2^24
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
function assertIpv4 (str, msg) {
|
||||
if (!ipv4.test(str)) { throw new Error(msg) }
|
||||
}
|
||||
|
||||
function *range (ip1, ip2) {
|
||||
assertIpv4(ip1, 'argument "ip1" must be a valid IPv4 address')
|
||||
assertIpv4(ip2, 'argument "ip2" must be a valid IPv4 address')
|
||||
|
||||
let hex = ip2hex(ip1)
|
||||
let hex2 = ip2hex(ip2)
|
||||
|
||||
if (hex > hex2) {
|
||||
let tmp = hex
|
||||
hex = hex2
|
||||
hex2 = tmp
|
||||
}
|
||||
|
||||
for (let i = hex; i <= hex2; i++) {
|
||||
yield `${(i >> 24) & 0xff}.${(i >> 16) & 0xff}.${(i >> 8) & 0xff}.${i & 0xff}`
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const getNextIpV4 = ip => {
|
||||
const splitIp = ip.split('.')
|
||||
if (splitIp.length !== 4 || some(splitIp, value => value < 0 || value > 255)) {
|
||||
return
|
||||
}
|
||||
let index
|
||||
forEachRight(splitIp, (value, i) => {
|
||||
if (value < 255) {
|
||||
index = i
|
||||
return false
|
||||
}
|
||||
splitIp[i] = 1
|
||||
})
|
||||
if (index === 0 && +splitIp[0] === 255) {
|
||||
return 0
|
||||
}
|
||||
splitIp[index]++
|
||||
|
||||
return splitIp.join('.')
|
||||
}
|
||||
|
||||
export const formatIps = ips => {
|
||||
if (!isArray(ips)) {
|
||||
throw new Error('ips must be an array')
|
||||
}
|
||||
if (ips.length === 0) {
|
||||
return []
|
||||
}
|
||||
const sortedIps = ips.sort((ip1, ip2) => {
|
||||
const splitIp1 = ip1.split('.')
|
||||
const splitIp2 = ip2.split('.')
|
||||
if (splitIp1.length !== 4) {
|
||||
return 1
|
||||
}
|
||||
if (splitIp2.length !== 4) {
|
||||
return -1
|
||||
}
|
||||
return splitIp1[3] - splitIp2[3] +
|
||||
(splitIp1[2] - splitIp2[2]) * 256 +
|
||||
(splitIp1[1] - splitIp2[1]) * 256 * 256 +
|
||||
(splitIp1[0] - splitIp2[0]) * 256 * 256 * 256
|
||||
})
|
||||
const range = { first: '', last: '' }
|
||||
const formattedIps = []
|
||||
let index = 0
|
||||
forEach(sortedIps, ip => {
|
||||
if (ip !== getNextIpV4(range.last)) {
|
||||
if (range.first) {
|
||||
formattedIps[index] = range.first === range.last ? range.first : { ...range }
|
||||
index++
|
||||
}
|
||||
range.first = range.last = ip
|
||||
} else {
|
||||
range.last = ip
|
||||
}
|
||||
})
|
||||
formattedIps[index] = range.first === range.last ? range.first : range
|
||||
|
||||
return formattedIps
|
||||
}
|
||||
|
||||
export const parseIpPattern = pattern => {
|
||||
const ips = []
|
||||
forEach(pattern.split(';'), rawIpRange => {
|
||||
const ipRange = rawIpRange.split('-')
|
||||
if (ipRange.length < 2) {
|
||||
ips.push(ipRange[0])
|
||||
} else if (!isIpV4(ipRange[0]) || !isIpV4(ipRange[1])) {
|
||||
ips.push(rawIpRange)
|
||||
} else {
|
||||
ips.push(...range(ipRange[0], ipRange[1]))
|
||||
}
|
||||
})
|
||||
|
||||
return ips
|
||||
}
|
||||
8
src/common/react-novnc.js
vendored
8
src/common/react-novnc.js
vendored
@@ -102,6 +102,14 @@ export default class NoVnc extends Component {
|
||||
this._clean()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
const rfb = this._rfb
|
||||
if (rfb && this.props.scale !== props.scale) {
|
||||
rfb.get_display().set_scale(props.scale || 1)
|
||||
rfb.get_mouse().set_scale(props.scale || 1)
|
||||
}
|
||||
}
|
||||
|
||||
_focus = () => {
|
||||
const rfb = this._rfb
|
||||
if (rfb) {
|
||||
|
||||
@@ -55,13 +55,12 @@ export const SrItem = propTypes({
|
||||
let label = `${sr.name_label || sr.id}`
|
||||
|
||||
if (isSrWritable(sr)) {
|
||||
label += ` (${formatSize(sr.size)})`
|
||||
label += ` (${formatSize(sr.size - sr.physical_usage)} free)`
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Icon icon='sr' /> {label}
|
||||
{container && ` (${container.name_label || container.id})`}
|
||||
</span>
|
||||
)
|
||||
}))
|
||||
@@ -113,6 +112,16 @@ const xoItemToRender = {
|
||||
<Icon icon='resource-set' /> {resourceSet.name}
|
||||
</span>
|
||||
),
|
||||
sshKey: key => (
|
||||
<span>
|
||||
<Icon icon='ssh-key' /> {key.label}
|
||||
</span>
|
||||
),
|
||||
ipPool: ipPool => (
|
||||
<span>
|
||||
<Icon icon='ip' /> {ipPool.name}
|
||||
</span>
|
||||
),
|
||||
|
||||
// XO objects.
|
||||
pool: pool => (
|
||||
|
||||
@@ -4,7 +4,7 @@ import later from 'later'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import sortedIndex from 'lodash/sortedIndex'
|
||||
import { FormattedTime } from 'react-intl'
|
||||
import { FormattedDate, FormattedTime } from 'react-intl'
|
||||
import {
|
||||
Tab,
|
||||
Tabs
|
||||
@@ -111,12 +111,12 @@ const TIME_FORMAT = {
|
||||
|
||||
// monthNum: [ 0 : 11 ]
|
||||
const getMonthName = (monthNum) =>
|
||||
<FormattedTime value={new Date(1970, monthNum)} month='long' />
|
||||
<FormattedDate value={Date.UTC(1970, monthNum)} month='long' timeZone='UTC' />
|
||||
|
||||
// dayNum: [ 0 : 6 ]
|
||||
const getDayName = (dayNum) =>
|
||||
// January, 1970, 5th => Monday
|
||||
<FormattedTime value={new Date(1970, 0, 4 + dayNum)} weekday='long' />
|
||||
<FormattedDate value={Date.UTC(1970, 0, 4 + dayNum)} weekday='long' timeZone='UTC' />
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -280,14 +280,12 @@ class TimePicker extends Component {
|
||||
}
|
||||
|
||||
_update (props) {
|
||||
const { refs } = this
|
||||
const { value, valueRenderer } = props
|
||||
|
||||
if (value.indexOf('/') === 1) {
|
||||
this.setState({
|
||||
activeKey: NAV_EVERY_N
|
||||
})
|
||||
refs.range.value = value.split('/')[1]
|
||||
}, () => { this.refs.range.value = value.split('/')[1] })
|
||||
} else {
|
||||
this.setState({
|
||||
activeKey: NAV_EACH_SELECTED,
|
||||
|
||||
@@ -8,6 +8,8 @@ import groupBy from 'lodash/groupBy'
|
||||
import keyBy from 'lodash/keyBy'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import pick from 'lodash/pick'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
import store from 'store'
|
||||
import { parse as parseRemote } from 'xo-remote-parser'
|
||||
@@ -31,7 +33,9 @@ import {
|
||||
} from './utils'
|
||||
import {
|
||||
isSrWritable,
|
||||
subscribeCurrentUser,
|
||||
subscribeGroups,
|
||||
subscribeIpPools,
|
||||
subscribeRemotes,
|
||||
subscribeResourceSets,
|
||||
subscribeRoles,
|
||||
@@ -50,6 +54,32 @@ const getLabel = object =>
|
||||
|
||||
// ===================================================================
|
||||
|
||||
/*
|
||||
* WITHOUT xoContainers :
|
||||
*
|
||||
* xoObjects: [
|
||||
* { type: 'myType', id: 'abc', label: 'First object' },
|
||||
* { type: 'myType', id: 'def', label: 'Second object' }
|
||||
* ]
|
||||
*
|
||||
*
|
||||
* WITH xoContainers :
|
||||
*
|
||||
* xoContainers: [
|
||||
* { type: 'containerType', id: 'ghi', label: 'First container' },
|
||||
* { type: 'containerType', id: 'jkl', label: 'Second container' }
|
||||
* ]
|
||||
*
|
||||
* xoObjects: {
|
||||
* ghi: [
|
||||
* { type: 'objectType', id: 'mno', label: 'First object' }
|
||||
* { type: 'objectType', id: 'pqr', label: 'Second object' }
|
||||
* ],
|
||||
* jkl: [
|
||||
* { type: 'objectType', id: 'stu', label: 'Third object' }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
@propTypes({
|
||||
autoFocus: propTypes.bool,
|
||||
clearable: propTypes.bool,
|
||||
@@ -82,7 +112,7 @@ export class GenericSelect extends Component {
|
||||
// Returns the values of the selected objects
|
||||
// if they are contained in xoObjectsById.
|
||||
return mapPlus(value, (value, push) => {
|
||||
const o = xoObjectsById[value.value || value]
|
||||
const o = xoObjectsById[value.value !== undefined ? value.value : value]
|
||||
|
||||
if (o) {
|
||||
push(o)
|
||||
@@ -96,11 +126,11 @@ export class GenericSelect extends Component {
|
||||
// Supports id strings and objects.
|
||||
_setValue (value, props = this.props) {
|
||||
if (props.multi) {
|
||||
return map(value, object => object.id || object)
|
||||
return map(value, object => object.id !== undefined ? object.id : object)
|
||||
}
|
||||
|
||||
return (value != null)
|
||||
? value.id || value
|
||||
? value.id !== undefined ? value.id : value
|
||||
: ''
|
||||
}
|
||||
|
||||
@@ -202,7 +232,7 @@ export class GenericSelect extends Component {
|
||||
|
||||
this.setState({
|
||||
value: this._setValue(value)
|
||||
}, onChange && (() => { onChange(this.value) }))
|
||||
}, onChange && (() => onChange(this.value)))
|
||||
}
|
||||
|
||||
// GroupBy: Display option with margin if not disabled and containers exists.
|
||||
@@ -267,13 +297,28 @@ const makeSubscriptionSelect = (subscribe, props) => (
|
||||
class extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
xoObjects: []
|
||||
}
|
||||
|
||||
this._getFilteredXoObjects = createFilter(
|
||||
this._getFilteredXoContainers = createFilter(
|
||||
() => this.state.xoContainers,
|
||||
() => this.props.containerPredicate
|
||||
)
|
||||
|
||||
this._getFilteredXoObjects = createSelector(
|
||||
() => this.state.xoObjects,
|
||||
() => this.props.predicate
|
||||
() => this.state.xoContainers && this._getFilteredXoContainers(),
|
||||
() => this.props.predicate,
|
||||
(xoObjects, xoContainers, predicate) => {
|
||||
if (xoContainers == null) {
|
||||
return filter(xoObjects, predicate)
|
||||
} else {
|
||||
// Filter xoObjects with `predicate`...
|
||||
const filteredObjects = mapValues(xoObjects, xoObjectsGroup =>
|
||||
filter(xoObjectsGroup, predicate)
|
||||
)
|
||||
// ...and keep only those whose xoContainer hasn't been filtered out
|
||||
return pick(filteredObjects, map(xoContainers, container => container.id))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -296,7 +341,7 @@ const makeSubscriptionSelect = (subscribe, props) => (
|
||||
{...props}
|
||||
{...this.props}
|
||||
xoObjects={this._getFilteredXoObjects()}
|
||||
xoContainers={this.state.xoContainers}
|
||||
xoContainers={this.state.xoContainers && this._getFilteredXoContainers()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -766,3 +811,61 @@ export class SelectResourceSetsNetwork extends Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectSshKey extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeCurrentUser(user => {
|
||||
this.setState({
|
||||
sshKeys: user && user.preferences && map(user.preferences.sshKeys, (key, id) => ({
|
||||
id,
|
||||
label: key.title,
|
||||
type: 'sshKey'
|
||||
}))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectSshKey')}
|
||||
{...this.props}
|
||||
xoObjects={this.state.sshKeys || []}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectIp = makeSubscriptionSelect(subscriber => {
|
||||
const unsubscribeIpPools = subscribeIpPools(ipPools => {
|
||||
const sortedIpPools = sortBy(ipPools, 'name')
|
||||
const xoObjects = mapValues(
|
||||
groupBy(sortedIpPools, 'id'),
|
||||
ipPools => map(ipPools[0].addresses, (address, ip) => ({
|
||||
...address,
|
||||
id: ip,
|
||||
label: ip
|
||||
}))
|
||||
)
|
||||
const xoContainers = map(sortedIpPools, ipPool => ({
|
||||
...ipPool,
|
||||
type: 'ipPool'
|
||||
}))
|
||||
subscriber({ xoObjects, xoContainers })
|
||||
})
|
||||
|
||||
return unsubscribeIpPools
|
||||
}, { placeholder: _('selectIp') })
|
||||
|
||||
@@ -212,6 +212,8 @@ const _getId = (state, { routeParams, id }) => routeParams
|
||||
|
||||
export const getLang = state => state.lang
|
||||
|
||||
export const getStatus = state => state.status
|
||||
|
||||
export const getUser = state => state.user
|
||||
|
||||
const _getPermissionsPredicate = invoke(() => {
|
||||
|
||||
@@ -300,7 +300,7 @@ export default class SortedTable extends Component {
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(this._getVisibleItems(), (item, i) => {
|
||||
const colums = map(props.columns, (column, key) => (
|
||||
const columns = map(props.columns, (column, key) => (
|
||||
<td key={key}>
|
||||
{column.itemRenderer(item, userData)}
|
||||
</td>
|
||||
@@ -313,8 +313,8 @@ export default class SortedTable extends Component {
|
||||
key={id}
|
||||
tagName='tr'
|
||||
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
|
||||
>{colums}</BlockLink>
|
||||
: <tr key={id}>{colums}</tr>
|
||||
>{columns}</BlockLink>
|
||||
: <tr key={id}>{columns}</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -58,7 +58,7 @@ export default class Tags extends Component {
|
||||
}
|
||||
}}
|
||||
onBlur={this._stopEdit}
|
||||
></input>
|
||||
/>
|
||||
</span>
|
||||
: []
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as actions from 'store/actions'
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
import every from 'lodash/every'
|
||||
import forEach from 'lodash/forEach'
|
||||
import getStream from 'get-stream'
|
||||
import humanFormat from 'human-format'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
@@ -13,6 +14,7 @@ import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import React from 'react'
|
||||
import ReadableStream from 'readable-stream'
|
||||
import replace from 'lodash/replace'
|
||||
import store from 'store'
|
||||
import { connect } from 'react-redux'
|
||||
@@ -209,7 +211,7 @@ export const getXoaPlan = plan => {
|
||||
export const mapPlus = (collection, cb) => {
|
||||
const result = []
|
||||
const push = ::result.push
|
||||
forEach(collection, value => cb(value, push))
|
||||
forEach(collection, (value, index) => cb(value, push, index))
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -417,3 +419,36 @@ export function buildTemplate (pattern, rules) {
|
||||
return isFunction(rule) ? rule(...params) : rule
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const streamToString = getStream
|
||||
|
||||
// ===================================================================
|
||||
|
||||
/* global FileReader */
|
||||
|
||||
// Creates a readable stream from a HTML file.
|
||||
export const htmlFileToStream = file => {
|
||||
const reader = new FileReader()
|
||||
const stream = new ReadableStream()
|
||||
let offset = 0
|
||||
|
||||
reader.onloadend = evt => {
|
||||
stream.push(evt.target.result)
|
||||
}
|
||||
reader.onerror = error => {
|
||||
stream.emit('error', error)
|
||||
}
|
||||
|
||||
stream._read = function (size) {
|
||||
if (offset >= file.size) {
|
||||
stream.push(null)
|
||||
} else {
|
||||
reader.readAsBinaryString(file.slice(offset, offset + size))
|
||||
offset += size
|
||||
}
|
||||
}
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Sparklines,
|
||||
SparklinesLine,
|
||||
SparklinesSpots
|
||||
SparklinesLine
|
||||
} from 'react-sparklines'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
const STYLE = {}
|
||||
|
||||
const WIDTH = 120
|
||||
const HEIGHT = 40
|
||||
const HEIGHT = 20
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -36,8 +35,7 @@ export const CpuSparkLines = propTypes({
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={computeArraysAvg(cpus)} max={100} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 1, stroke: '#366e98', fill: '#366e98', fillOpacity: 0.5 }} color='#2598d9' />
|
||||
<SparklinesSpots />
|
||||
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#366e98', fill: '#366e98', fillOpacity: 0.5 }} color='#2598d9' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
@@ -53,8 +51,7 @@ export const MemorySparkLines = propTypes({
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={memoryUsed} max={memory[memory.length - 1]} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 1, stroke: '#990822', fill: '#990822', fillOpacity: 0.5 }} color='#cc0066' />
|
||||
<SparklinesSpots />
|
||||
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#990822', fill: '#990822', fillOpacity: 0.5 }} color='#cc0066' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
@@ -70,8 +67,7 @@ export const XvdSparkLines = propTypes({
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(xvds)} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 1, stroke: '#089944', fill: '#089944', fillOpacity: 0.5 }} color='#33cc33' />
|
||||
<SparklinesSpots />
|
||||
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#089944', fill: '#089944', fillOpacity: 0.5 }} color='#33cc33' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
@@ -87,8 +83,7 @@ export const VifSparkLines = propTypes({
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(vifs)} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 1, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
|
||||
<SparklinesSpots />
|
||||
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
@@ -104,8 +99,7 @@ export const PifSparkLines = propTypes({
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(pifs)} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 1, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
|
||||
<SparklinesSpots />
|
||||
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
@@ -121,8 +115,7 @@ export const LoadSparkLines = propTypes({
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={load} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 1, stroke: '#33cc33', fill: '#33cc33', fillOpacity: 0.5 }} color='#33cc33' />
|
||||
<SparklinesSpots />
|
||||
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#33cc33', fill: '#33cc33', fillOpacity: 0.5 }} color='#33cc33' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
|
||||
37
src/common/xo/add-host-modal/index.js
Normal file
37
src/common/xo/add-host-modal/index.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import _ from 'intl'
|
||||
import BaseComponent from 'base-component'
|
||||
import every from 'lodash/every'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import { SelectHost } from 'select-objects'
|
||||
import { Col } from 'grid'
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
|
||||
@connectStore(() => ({
|
||||
hosts: createGetObjectsOfType('host')
|
||||
}), { withRef: true })
|
||||
export default class AddHostModal extends BaseComponent {
|
||||
get value () {
|
||||
return this.state
|
||||
}
|
||||
|
||||
_hostPredicate = host =>
|
||||
host.$pool !== this.props.pool.id &&
|
||||
every(this.props.hosts, h => h.$pool !== host.$pool || h.id === host.id)
|
||||
|
||||
render () {
|
||||
return <div>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('addHostSelectHost')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectHost
|
||||
onChange={this.linkState('host')}
|
||||
predicate={this._hostPredicate}
|
||||
value={this.state.host}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -39,8 +39,9 @@ export const XEN_DEFAULT_CPU_CAP = 0
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const isSrWritable = sr => sr.content_type !== 'iso' && sr.size > 0
|
||||
export const isSrShared = sr => sr.$PBDs.length > 1
|
||||
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'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -49,6 +50,12 @@ export const signOut = () => {
|
||||
window.location.reload(true)
|
||||
}
|
||||
|
||||
export const connect = () => {
|
||||
xo.open(createBackoff()).catch(error => {
|
||||
logError(error, 'failed to connect to xo-server')
|
||||
})
|
||||
}
|
||||
|
||||
const xo = invoke(() => {
|
||||
const token = cookies.get('token')
|
||||
if (!token) {
|
||||
@@ -60,13 +67,6 @@ const xo = invoke(() => {
|
||||
credentials: { token }
|
||||
})
|
||||
|
||||
const connect = () => {
|
||||
xo.open(createBackoff()).catch(error => {
|
||||
logError(error, 'failed to connect to xo-server')
|
||||
})
|
||||
}
|
||||
connect()
|
||||
|
||||
xo.on('scheduledAttempt', ({ delay }) => {
|
||||
console.warn('next attempt in %s ms', delay)
|
||||
})
|
||||
@@ -75,6 +75,7 @@ const xo = invoke(() => {
|
||||
|
||||
return xo
|
||||
})
|
||||
connect()
|
||||
|
||||
const _signIn = new Promise(resolve => xo.once('authenticated', resolve))
|
||||
|
||||
@@ -159,6 +160,12 @@ const createSubscription = cb => {
|
||||
if (!isEqual(result, cache)) {
|
||||
cache = result
|
||||
|
||||
/* FIXME: Edge case:
|
||||
* 1) MyComponent has a subscription with subscribers[1]
|
||||
* 2) subscribers[0] causes the MyComponent unmounting (and thus its unsubscription)
|
||||
* When subscribers[1] will be executed, it will no longer exist,
|
||||
* which will throw an error (Uncaught (in promise) TypeError: subscriber is not a function)
|
||||
*/
|
||||
forEach(subscribers, subscriber => {
|
||||
subscriber(result)
|
||||
})
|
||||
@@ -206,6 +213,8 @@ export const subscribeJobs = createSubscription(() => _call('job.getAll'))
|
||||
|
||||
export const subscribeJobsLogs = createSubscription(() => _call('log.get', {namespace: 'jobs'}))
|
||||
|
||||
export const subscribeApiLogs = createSubscription(() => _call('log.get', {namespace: 'api'}))
|
||||
|
||||
export const subscribePermissions = createSubscription(() => _call('acl.getCurrentPermissions'))
|
||||
|
||||
export const subscribePlugins = createSubscription(() => _call('plugin.get'))
|
||||
@@ -244,6 +253,8 @@ export const subscribeRoles = createSubscription(invoke(
|
||||
sort => () => _call('role.getAll').then(sort)
|
||||
))
|
||||
|
||||
export const subscribeIpPools = createSubscription(() => _call('ipPool.getAll'))
|
||||
|
||||
// System ============================================================
|
||||
|
||||
export const apiMethods = _call('system.getMethodsInfo')
|
||||
@@ -277,26 +288,26 @@ export const addServer = (host, username, password) => (
|
||||
)
|
||||
)
|
||||
|
||||
export const editServer = ({ id }, { host, username, password, readOnly }) => (
|
||||
_call('server.set', { id, host, username, password, readOnly })::tap(
|
||||
export const editServer = (server, { host, username, password, readOnly }) => (
|
||||
_call('server.set', { id: resolveId(server), host, username, password, readOnly })::tap(
|
||||
subscribeServers.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const connectServer = ({ id }) => (
|
||||
_call('server.connect', { id })::tap(
|
||||
export const connectServer = server => (
|
||||
_call('server.connect', { id: resolveId(server) })::tap(
|
||||
subscribeServers.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const disconnectServer = ({ id }) => (
|
||||
_call('server.disconnect', { id })::tap(
|
||||
export const disconnectServer = server => (
|
||||
_call('server.disconnect', { id: resolveId(server) })::tap(
|
||||
subscribeServers.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const removeServer = ({ id }) => (
|
||||
_call('server.remove', { id })::tap(
|
||||
export const removeServer = server => (
|
||||
_call('server.remove', { id: resolveId(server) })::tap(
|
||||
subscribeServers.forceRefresh
|
||||
)
|
||||
)
|
||||
@@ -307,6 +318,42 @@ export const editPool = (pool, props) => (
|
||||
_call('pool.set', { id: resolveId(pool), ...props })
|
||||
)
|
||||
|
||||
import AddHostModalBody from './add-host-modal'
|
||||
export const addHostToPool = (pool, host) => {
|
||||
if (host) {
|
||||
return confirm({
|
||||
title: _('addHostModalTitle'),
|
||||
body: _('addHostModalMessage', { pool: pool.name_label, host: host.name_label })
|
||||
}).then(() =>
|
||||
_call('pool.mergeInto', { source: host.$pool, target: pool.id, force: true })
|
||||
)
|
||||
}
|
||||
|
||||
return confirm({
|
||||
title: _('addHostModalTitle'),
|
||||
body: <AddHostModalBody pool={pool} />
|
||||
}).then(
|
||||
params => {
|
||||
if (!params.host) {
|
||||
error(_('addHostNoHost'), _('addHostNoHostMessage'))
|
||||
return
|
||||
}
|
||||
_call('pool.mergeInto', { source: params.host.$pool, target: pool.id, force: true })
|
||||
},
|
||||
noop
|
||||
)
|
||||
}
|
||||
|
||||
export const detachHost = host => (
|
||||
confirm({
|
||||
icon: 'host-eject',
|
||||
title: _('detachHostModalTitle'),
|
||||
body: _('detachHostModalMessage', {host: <strong>{host.name_label}</strong>})
|
||||
}).then(
|
||||
() => _call('host.detach', { host: host.id })
|
||||
)
|
||||
)
|
||||
|
||||
// Host --------------------------------------------------------------
|
||||
|
||||
export const editHost = (host, props) => (
|
||||
@@ -348,7 +395,7 @@ export const restartHostsAgents = hosts => {
|
||||
title: _('restartHostsAgentsModalTitle', { nHosts }),
|
||||
body: _('restartHostsAgentsModalMessage', { nHosts })
|
||||
}).then(
|
||||
() => map(hosts, host => restartHostAgent(host)),
|
||||
() => map(hosts, restartHostAgent),
|
||||
noop
|
||||
)
|
||||
}
|
||||
@@ -413,6 +460,10 @@ export const installAllHostPatches = host => (
|
||||
_call('host.installAllPatches', { host: resolveId(host) })
|
||||
)
|
||||
|
||||
export const installAllPatchesOnPool = pool => (
|
||||
_call('pool.installAllPatches', { pool: resolveId(pool) })
|
||||
)
|
||||
|
||||
// VM ----------------------------------------------------------------
|
||||
|
||||
export const startVm = vm => (
|
||||
@@ -651,7 +702,7 @@ export const deleteVms = vms => (
|
||||
title: _('deleteVmsModalTitle', { vms: vms.length }),
|
||||
body: _('deleteVmsModalMessage', { vms: vms.length })
|
||||
}).then(
|
||||
() => map(vms, vmId => _call('vm.delete', { id: vmId })),
|
||||
() => map(vms, vmId => _call('vm.delete', { id: vmId, delete_disks: true })),
|
||||
noop
|
||||
)
|
||||
)
|
||||
@@ -682,12 +733,12 @@ export const fetchVmStats = (vm, granularity) => (
|
||||
_call('vm.stats', { id: resolveId(vm), granularity })
|
||||
)
|
||||
|
||||
export const importVm = (file, sr) => {
|
||||
export const importVm = (file, type = 'xva', data = undefined, sr) => {
|
||||
const { name } = file
|
||||
|
||||
info(_('startVmImport'), name)
|
||||
|
||||
return _call('vm.import', { sr }).then(({ $sendTo: url }) => {
|
||||
return _call('vm.import', { type, data, sr: resolveId(sr) }).then(({ $sendTo: url }) => {
|
||||
const req = request.post(url)
|
||||
|
||||
req.send(file)
|
||||
@@ -701,9 +752,9 @@ export const importVm = (file, sr) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const importVms = (files, sr) => (
|
||||
Promise.all(map(files, file =>
|
||||
importVm(file, sr).catch(noop)
|
||||
export const importVms = (vms, sr) => (
|
||||
Promise.all(map(vms, ({ file, type, data }) =>
|
||||
importVm(file, type, data, sr).catch(noop)
|
||||
))
|
||||
)
|
||||
|
||||
@@ -759,7 +810,13 @@ export const editVdi = (vdi, props) => (
|
||||
)
|
||||
|
||||
export const deleteVdi = vdi => (
|
||||
_call('vdi.delete', { id: resolveId(vdi) })
|
||||
confirm({
|
||||
title: _('deleteVdiModalTitle'),
|
||||
body: _('deleteVdiModalMessage')
|
||||
}).then(
|
||||
() => _call('vdi.delete', { id: resolveId(vdi) }),
|
||||
noop
|
||||
)
|
||||
)
|
||||
|
||||
export const migrateVdi = (vdi, sr) => (
|
||||
@@ -802,6 +859,10 @@ export const deleteVif = vif => (
|
||||
_call('vif.delete', { id: resolveId(vif) })
|
||||
)
|
||||
|
||||
export const setVif = (vif, { allowedIpv4Addresses, allowedIpv6Addresses }) => (
|
||||
_call('vif.set', { id: resolveId(vif), allowedIpv4Addresses, allowedIpv6Addresses })
|
||||
)
|
||||
|
||||
// Network -----------------------------------------------------------
|
||||
|
||||
export const editNetwork = (network, props) => (
|
||||
@@ -1233,6 +1294,14 @@ export const deleteJobsLog = id => (
|
||||
)
|
||||
)
|
||||
|
||||
// Logs
|
||||
|
||||
export const deleteApiLog = id => (
|
||||
_call('log.delete', {namespace: 'api', id})::tap(
|
||||
subscribeApiLogs.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
// Acls, users, groups ----------------------------------------------------------
|
||||
|
||||
export const addAcl = ({subject, object, action}) => (
|
||||
@@ -1316,9 +1385,11 @@ export const deleteUser = user => (
|
||||
confirm({
|
||||
title: _('deleteUser'),
|
||||
body: <p>{_('deleteUserConfirm')}</p>
|
||||
}).then(() => _call('user.delete', resolveIds({id: user})))
|
||||
::tap(subscribeUsers.forceRefresh)
|
||||
::rethrow(err => error(_('deleteUser'), err.message || String(err)))
|
||||
}).then(() =>
|
||||
_call('user.delete', { id: resolveId(user) })
|
||||
::tap(subscribeUsers.forceRefresh)
|
||||
::rethrow(err => error(_('deleteUser'), err.message || String(err)))
|
||||
)
|
||||
)
|
||||
|
||||
export const editUser = (user, { email, password, permission }) => (
|
||||
@@ -1347,8 +1418,16 @@ const _setUserPreferences = preferences => (
|
||||
)
|
||||
|
||||
import NewSshKeyModalBody from './new-ssh-key-modal'
|
||||
export const addSshKey = () => (
|
||||
confirm({
|
||||
export const addSshKey = key => {
|
||||
const { preferences } = xo.user
|
||||
const otherKeys = preferences && preferences.sshKeys || []
|
||||
if (key) {
|
||||
return _setUserPreferences({ sshKeys: [
|
||||
...otherKeys,
|
||||
key
|
||||
]})
|
||||
}
|
||||
return confirm({
|
||||
icon: 'ssh-key',
|
||||
title: _('newSshKeyModalTitle'),
|
||||
body: <NewSshKeyModalBody />
|
||||
@@ -1358,8 +1437,6 @@ export const addSshKey = () => (
|
||||
error(_('sshKeyErrorTitle'), _('sshKeyErrorMessage'))
|
||||
return
|
||||
}
|
||||
const { preferences } = xo.user
|
||||
const otherKeys = preferences && preferences.sshKeys || []
|
||||
return _setUserPreferences({ sshKeys: [
|
||||
...otherKeys,
|
||||
newKey
|
||||
@@ -1367,7 +1444,7 @@ export const addSshKey = () => (
|
||||
},
|
||||
noop
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export const deleteSshKey = key => (
|
||||
confirm({
|
||||
@@ -1462,13 +1539,13 @@ export const setDefaultHomeFilter = (type, name) => {
|
||||
// Jobs ----------------------------------------------------------
|
||||
|
||||
export const deleteJob = job => (
|
||||
_call('job.delete', resolveIds({id: job}))::tap(
|
||||
_call('job.delete', { id: resolveId(job) })::tap(
|
||||
subscribeJobs.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const deleteSchedule = schedule => (
|
||||
_call('schedule.delete', resolveIds({id: schedule}))::tap(
|
||||
_call('schedule.delete', { id: resolveIds(schedule) })::tap(
|
||||
subscribeSchedules.forceRefresh
|
||||
)
|
||||
)
|
||||
@@ -1484,3 +1561,25 @@ export const updateSchedule = ({ id, job: jobId, cron, enabled, name, timezone }
|
||||
subscribeSchedules.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
// IP pools --------------------------------------------------------------------
|
||||
|
||||
export const createIpPool = ({ name, ips, networks }) => {
|
||||
const addresses = {}
|
||||
forEach(ips, ip => { addresses[ip] = {} })
|
||||
return _call('ipPool.create', { name, addresses, networks: resolveIds(networks) })::tap(
|
||||
subscribeIpPools.forceRefresh
|
||||
)
|
||||
}
|
||||
|
||||
export const deleteIpPool = ipPool => (
|
||||
_call('ipPool.delete', { id: resolveId(ipPool) })::tap(
|
||||
subscribeIpPools.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const setIpPool = (ipPool, { name, addresses, networks }) => (
|
||||
_call('ipPool.set', { id: resolveId(ipPool), name, addresses, networks: resolveIds(networks) })::tap(
|
||||
subscribeIpPools.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import every from 'lodash/every'
|
||||
import forEach from 'lodash/forEach'
|
||||
import find from 'lodash/find'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import React from 'react'
|
||||
import store from 'store'
|
||||
|
||||
import _ from '../../intl'
|
||||
import invoke from '../../invoke'
|
||||
@@ -22,8 +24,12 @@ import {
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
createPicker,
|
||||
createSelector
|
||||
createSelector,
|
||||
getObject
|
||||
} from '../../selectors'
|
||||
import {
|
||||
isSrShared
|
||||
} from 'xo'
|
||||
|
||||
import { isSrWritable } from '../'
|
||||
|
||||
@@ -59,6 +65,7 @@ import styles from './index.css'
|
||||
networks: getNetworks,
|
||||
pifs: getPifs,
|
||||
pools: getPools,
|
||||
vbds: getVbds,
|
||||
vdis: getVdis,
|
||||
vifs: getVifs
|
||||
}
|
||||
@@ -85,7 +92,26 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
)
|
||||
)
|
||||
|
||||
this._getNetworkPredicate = createSelector(
|
||||
this._getTargetNetworkPredicate = createSelector(
|
||||
createPicker(
|
||||
() => this.props.pifs,
|
||||
() => this.state.host.$PIFs
|
||||
),
|
||||
pifs => {
|
||||
if (!pifs) {
|
||||
return false
|
||||
}
|
||||
|
||||
const networks = {}
|
||||
forEach(pifs, pif => {
|
||||
networks[pif.$network] = true
|
||||
})
|
||||
|
||||
return network => networks[network.id]
|
||||
}
|
||||
)
|
||||
|
||||
this._getMigrationNetworkPredicate = createSelector(
|
||||
createPicker(
|
||||
() => this.props.pifs,
|
||||
() => this.state.host.$PIFs
|
||||
@@ -118,7 +144,12 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
_getObject (id) {
|
||||
return getObject(store.getState(), id)
|
||||
}
|
||||
|
||||
_selectHost = host => {
|
||||
// No host selected
|
||||
if (!host) {
|
||||
this.setState({
|
||||
host: undefined,
|
||||
@@ -126,20 +157,40 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
})
|
||||
return
|
||||
}
|
||||
const intraPool = this.props.vm.$pool === host.$pool
|
||||
|
||||
const { pools, vbds, vdis, vm } = this.props
|
||||
const intraPool = vm.$pool === host.$pool
|
||||
|
||||
// Intra-pool
|
||||
const defaultSr = pools[host.$pool].default_SR
|
||||
if (intraPool) {
|
||||
let doNotMigrateVdis
|
||||
if (vm.$container === host.id) {
|
||||
doNotMigrateVdis = true
|
||||
} else {
|
||||
const _doNotMigrateVdi = {}
|
||||
forEach(vbds, vbd => {
|
||||
if (vbd.VDI != null) {
|
||||
_doNotMigrateVdi[vbd.VDI] = isSrShared(this._getObject(this._getObject(vbd.VDI).$SR))
|
||||
}
|
||||
})
|
||||
doNotMigrateVdis = every(_doNotMigrateVdi)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
doNotMigrateVdis,
|
||||
host,
|
||||
intraPool,
|
||||
mapVdisSrs: undefined,
|
||||
mapVdisSrs: doNotMigrateVdis ? undefined : mapValues(vdis, vdi => defaultSr),
|
||||
mapVifsNetworks: undefined,
|
||||
migrationNetwork: undefined
|
||||
})
|
||||
return
|
||||
}
|
||||
const { networks, pools, pifs, vdis, vifs } = this.props
|
||||
|
||||
// Inter-pool
|
||||
const { networks, pifs, vifs } = this.props
|
||||
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
|
||||
const defaultSr = pools[host.$pool].default_SR
|
||||
|
||||
const defaultNetwork = invoke(() => {
|
||||
// First PIF with an IP.
|
||||
@@ -158,6 +209,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
})
|
||||
|
||||
this.setState({
|
||||
doNotMigrateVdis: false,
|
||||
host,
|
||||
intraPool,
|
||||
mapVdisSrs: mapValues(vdis, vdi => defaultSr),
|
||||
@@ -171,6 +223,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
render () {
|
||||
const { vdis, vifs, networks } = this.props
|
||||
const {
|
||||
doNotMigrateVdis,
|
||||
host,
|
||||
intraPool,
|
||||
mapVdisSrs,
|
||||
@@ -190,6 +243,28 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
{host && !doNotMigrateVdis && <div className={styles.groupBlock}>
|
||||
<SingleLineRow>
|
||||
<Col>{_('migrateVmSelectSrs')}</Col>
|
||||
</SingleLineRow>
|
||||
<br />
|
||||
<SingleLineRow>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmName')}</span></Col>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmSr')}</span></Col>
|
||||
</SingleLineRow>
|
||||
{map(vdis, vdi => <div className={styles.listItem} key={vdi.id}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{vdi.name_label}</Col>
|
||||
<Col size={6}>
|
||||
<SelectSr
|
||||
onChange={sr => this.setState({ mapVdisSrs: { ...mapVdisSrs, [vdi.id]: sr.id } })}
|
||||
predicate={this._getSrPredicate()}
|
||||
value={mapVdisSrs[vdi.id]}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>)}
|
||||
</div>}
|
||||
{intraPool !== undefined &&
|
||||
(!intraPool &&
|
||||
<div>
|
||||
@@ -199,34 +274,12 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
<Col size={6}>
|
||||
<SelectNetwork
|
||||
onChange={this._selectMigrationNetwork}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
predicate={this._getMigrationNetworkPredicate()}
|
||||
value={migrationNetworkId}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
<div className={styles.groupBlock}>
|
||||
<SingleLineRow>
|
||||
<Col>{_('migrateVmSelectSrs')}</Col>
|
||||
</SingleLineRow>
|
||||
<br />
|
||||
<SingleLineRow>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmName')}</span></Col>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmSr')}</span></Col>
|
||||
</SingleLineRow>
|
||||
{map(vdis, vdi => <div className={styles.listItem} key={vdi.id}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{vdi.name_label}</Col>
|
||||
<Col size={6}>
|
||||
<SelectSr
|
||||
onChange={sr => this.setState({ mapVdisSrs: { ...mapVdisSrs, [vdi.id]: sr.id } })}
|
||||
predicate={this._getSrPredicate()}
|
||||
value={mapVdisSrs[vdi.id]}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>)}
|
||||
</div>
|
||||
<div className={styles.groupBlock}>
|
||||
<SingleLineRow>
|
||||
<Col>{_('migrateVmSelectNetworks')}</Col>
|
||||
@@ -242,7 +295,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
<Col size={6}>
|
||||
<SelectNetwork
|
||||
onChange={network => this.setState({ mapVifsNetworks: { ...mapVifsNetworks, [vif.id]: network.id } })}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
predicate={this._getTargetNetworkPredicate()}
|
||||
value={mapVifsNetworks[vif.id]}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -87,7 +87,26 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
)
|
||||
)
|
||||
|
||||
this._getNetworkPredicate = createSelector(
|
||||
this._getTargetNetworkPredicate = createSelector(
|
||||
createPicker(
|
||||
() => this.props.pifs,
|
||||
() => this.state.host.$PIFs
|
||||
),
|
||||
pifs => {
|
||||
if (!pifs) {
|
||||
return false
|
||||
}
|
||||
|
||||
const networks = {}
|
||||
forEach(pifs, pif => {
|
||||
networks[pif.$network] = true
|
||||
})
|
||||
|
||||
return network => networks[network.id]
|
||||
}
|
||||
)
|
||||
|
||||
this._getMigrationNetworkPredicate = createSelector(
|
||||
createPicker(
|
||||
() => this.props.pifs,
|
||||
() => this.state.host.$PIFs
|
||||
@@ -261,7 +280,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
<Col size={6}>
|
||||
<SelectNetwork
|
||||
onChange={this._selectMigrationNetwork}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
predicate={this._getMigrationNetworkPredicate()}
|
||||
value={migrationNetworkId}
|
||||
/>
|
||||
</Col>
|
||||
@@ -290,7 +309,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
<SelectNetwork
|
||||
disabled={smartVifMapping}
|
||||
onChange={this._selectNetwork}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
predicate={this._getTargetNetworkPredicate()}
|
||||
value={networkId}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -116,6 +116,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-key;
|
||||
}
|
||||
&-ip {
|
||||
@extend .fa;
|
||||
@extend .fa-map-marker;
|
||||
}
|
||||
|
||||
&-shown {
|
||||
@extend .fa;
|
||||
@@ -163,12 +167,20 @@
|
||||
@extend .fa;
|
||||
@extend .fa-link;
|
||||
}
|
||||
|
||||
&-disconnect {
|
||||
@extend .fa;
|
||||
@extend .fa-chain-broken;
|
||||
}
|
||||
|
||||
&-lock {
|
||||
@extend .fa;
|
||||
@extend .fa-lock;
|
||||
}
|
||||
&-unlock {
|
||||
@extend .fa;
|
||||
@extend .fa-unlock;
|
||||
}
|
||||
|
||||
&-cpu {
|
||||
@extend .fa;
|
||||
@extend .fa-dashboard;
|
||||
@@ -676,6 +688,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-puzzle-piece;
|
||||
}
|
||||
&-logs {
|
||||
@extend .fa;
|
||||
@extend .fa-list;
|
||||
}
|
||||
}
|
||||
&-menu-about {
|
||||
@extend .fa;
|
||||
|
||||
@@ -72,17 +72,27 @@ $select-input-height: 40px; // Bootstrap input height
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.Select-value-label {
|
||||
color: #373a3c;
|
||||
}
|
||||
|
||||
.Select-control {
|
||||
border-radius: unset;
|
||||
}
|
||||
|
||||
// Disabled option style.
|
||||
.Select-menu-outer {
|
||||
.Select-option.is-disabled {
|
||||
cursor: default;
|
||||
font-weight: bold;
|
||||
color: #777;
|
||||
}
|
||||
.Select-menu-outer .Select-option.is-disabled {
|
||||
cursor: default;
|
||||
font-weight: bold;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.Select-placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.Select--single > .Select-control .Select-value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
// COLORS ======================================================================
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
// error for usage > 90%
|
||||
|
||||
meter {
|
||||
/* Reset the default appearance */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
/* For Firefox */
|
||||
background: #EEE;
|
||||
box-shadow: 0 2px 3px rgba(0,0,0,0.2) inset;
|
||||
|
||||
@@ -137,7 +137,7 @@ export default class Restore extends Component {
|
||||
{r.enabled && <span className='tag tag-success'>{_('remoteEnabled')}</span>}
|
||||
{r.error && <span className='tag tag-danger'>{_('remoteError')}</span>}
|
||||
<span className='pull-right'>
|
||||
<ActionButton disabled={!r.enabled} icon='refresh' btnStyle='default' handler={this._list} handlerParam={r.id} />
|
||||
<Tooltip content={_('displayBackup')}><ActionButton disabled={!r.enabled} icon='refresh' btnStyle='default' handler={this._list} handlerParam={r.id} /></Tooltip>
|
||||
</span>
|
||||
{r.backupInfoByVm && <div>
|
||||
<br />
|
||||
|
||||
@@ -2,9 +2,11 @@ import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import React, { Component } from 'react'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
@@ -25,7 +27,7 @@ import {
|
||||
|
||||
const SrColContainer = connectStore(() => ({
|
||||
container: createGetObject()
|
||||
}))(({ container }) => <span>{container.name_label}</span>)
|
||||
}))(({ container }) => <Link to={`pools/${container.id}`}>{container.name_label}</Link>)
|
||||
|
||||
const VdiColSr = connectStore(() => ({
|
||||
sr: createGetObject()
|
||||
@@ -65,7 +67,10 @@ const SR_COLUMNS = [
|
||||
},
|
||||
{
|
||||
name: _('srUsage'),
|
||||
itemRenderer: sr => sr.size > 1 && <meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90'></meter>,
|
||||
itemRenderer: sr => sr.size > 1 &&
|
||||
<Tooltip content={_('spaceLeftTooltip', {used: Math.round((sr.physical_usage / sr.size) * 100), free: formatSize(sr.size - sr.physical_usage)})}>
|
||||
<meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90' />
|
||||
</Tooltip>,
|
||||
sortCriteria: sr => sr.physical_usage / sr.size,
|
||||
sortOrder: 'desc'
|
||||
}
|
||||
@@ -230,6 +235,8 @@ export default class Health extends Component {
|
||||
)
|
||||
)
|
||||
|
||||
_getSrUrl = sr => `srs/${sr.id}`
|
||||
|
||||
render () {
|
||||
return process.env.XOA_PLAN > 3
|
||||
? <Container>
|
||||
@@ -244,7 +251,12 @@ export default class Health extends Component {
|
||||
? <p className='text-xs-center'>{_('noSrs')}</p>
|
||||
: <Row>
|
||||
<Col>
|
||||
<SortedTable collection={this.props.userSrs} columns={SR_COLUMNS} defaultColumn={4} />
|
||||
<SortedTable
|
||||
collection={this.props.userSrs}
|
||||
columns={SR_COLUMNS}
|
||||
defaultColumn={4}
|
||||
rowLink={this._getSrUrl}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
|
||||
@@ -71,6 +71,8 @@ export default class HostItem extends Component {
|
||||
<Ellipsis>
|
||||
<Text value={host.name_label} onChange={this._setNameLabel} useLongClick />
|
||||
</Ellipsis>
|
||||
|
||||
{container && host.id === container.master && <span className='tag tag-pill tag-info'>{_('pillMaster')}</span>}
|
||||
</EllipsisContainer>
|
||||
</Col>
|
||||
<Col mediumSize={4} className='hidden-md-down'>
|
||||
@@ -110,10 +112,10 @@ export default class HostItem extends Component {
|
||||
<Col largeSize={2} className='hidden-lg-down'>
|
||||
<span className='tag tag-info tag-ip'>{host.address}</span>
|
||||
</Col>
|
||||
<Col mediumSize={2} className='hidden-sm-down'>
|
||||
{container && <Col mediumSize={2} className='hidden-sm-down'>
|
||||
<Link to={`/${container.type}s/${container.id}`}>{container.name_label}</Link>
|
||||
</Col>
|
||||
<Col mediumSize={1} className={styles.itemExpandRow}>
|
||||
</Col>}
|
||||
<Col mediumSize={1} offset={container ? undefined : 2} className={styles.itemExpandRow}>
|
||||
<a className={styles.itemExpandButton}
|
||||
onClick={this._toggleExpanded}>
|
||||
<Icon icon='nav' fixedWidth />
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
}
|
||||
|
||||
.itemExpanded {
|
||||
padding-top: 0.4em;
|
||||
color: #999;
|
||||
font-size: 1em;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -9,8 +9,8 @@ import SingleLineRow from 'single-line-row'
|
||||
import size from 'lodash/size'
|
||||
import Tags from 'tags'
|
||||
import Tooltip from 'tooltip'
|
||||
import { BlockLink } from 'link'
|
||||
import { Row, Col } from 'grid'
|
||||
import Link, { BlockLink } from 'link'
|
||||
import { Col } from 'grid'
|
||||
import { Text } from 'editable'
|
||||
import {
|
||||
addTag,
|
||||
@@ -48,7 +48,8 @@ import styles from './index.css'
|
||||
|
||||
return {
|
||||
hostMetrics: getHostMetrics,
|
||||
missingPaths: getMissingPatches
|
||||
missingPaths: getMissingPatches,
|
||||
poolHosts: getPoolHosts
|
||||
}
|
||||
})
|
||||
export default class PoolItem extends Component {
|
||||
@@ -64,7 +65,7 @@ export default class PoolItem extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { item: pool, expandAll, selected, hostMetrics } = this.props
|
||||
const { item: pool, expandAll, selected, hostMetrics, poolHosts } = this.props
|
||||
const { missingPatchCount } = this.state
|
||||
return <div className={styles.item}>
|
||||
<BlockLink to={`/pools/${pool.id}`}>
|
||||
@@ -122,8 +123,8 @@ export default class PoolItem extends Component {
|
||||
</SingleLineRow>
|
||||
</BlockLink>
|
||||
{(this.state.expanded || expandAll) &&
|
||||
<Row>
|
||||
<Col mediumSize={6} className={styles.itemExpanded}>
|
||||
<SingleLineRow>
|
||||
<Col mediumSize={3} className={styles.itemExpanded}>
|
||||
<span>
|
||||
{hostMetrics.count}x <Icon icon='host' />
|
||||
{' '}
|
||||
@@ -132,12 +133,17 @@ export default class PoolItem extends Component {
|
||||
{formatSize(hostMetrics.memoryTotal)}
|
||||
</span>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<Col mediumSize={4} className={styles.itemExpanded}>
|
||||
<span>
|
||||
{_('homePoolMaster')} <Link to={`/hosts/${pool.master}`}>{poolHosts && poolHosts[pool.master].name_label}</Link>
|
||||
</span>
|
||||
</Col>
|
||||
<Col mediumSize={5}>
|
||||
<span style={{fontSize: '1.4em'}}>
|
||||
<Tags labels={pool.tags} onDelete={this._removeTag} onAdd={this._addTag} />
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</SingleLineRow>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export default class VmItem extends Component {
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
<Icon className='text-info' icon={vm.os_version && osFamily(vm.os_version.distro)} fixedWidth />
|
||||
<Tooltip content={vm.os_version ? vm.os_version.name : _('unknownOsName')}><Icon className='text-info' icon={vm.os_version && osFamily(vm.os_version.distro)} fixedWidth /></Tooltip>
|
||||
{' '}
|
||||
<Ellipsis>
|
||||
<Text value={vm.name_description} onChange={this._setNameDescription} placeholder={_('vmHomeDescriptionPlaceholder')} useLongClick />
|
||||
|
||||
@@ -129,6 +129,10 @@ const isRunning = host => host && host.power_state === 'Running'
|
||||
}
|
||||
})
|
||||
export default class Host extends Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
|
||||
loop (host = this.props.host) {
|
||||
if (this.cancel) {
|
||||
this.cancel()
|
||||
@@ -182,6 +186,10 @@ export default class Host extends Component {
|
||||
}
|
||||
|
||||
const hostCur = this.props.host
|
||||
if (hostCur && !hostNext) {
|
||||
this.context.router.push('/')
|
||||
}
|
||||
|
||||
if (!hostCur) {
|
||||
this._getMissingPatches(hostNext)
|
||||
}
|
||||
@@ -234,7 +242,7 @@ export default class Host extends Component {
|
||||
value={host.name_description}
|
||||
onChange={this._setNameDescription}
|
||||
/>
|
||||
<span className='text-muted'> - <Link to={`/pools/${pool.id}`}>{pool.name_label}</Link></span>
|
||||
{pool && <span className='text-muted'> - <Link to={`/pools/${pool.id}`}>{pool.name_label}</Link></span>}
|
||||
</span>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import Copiable from 'copiable'
|
||||
import React from 'react'
|
||||
import TabButton from 'tab-button'
|
||||
import { Toggle } from 'form'
|
||||
import { enableHost, disableHost, restartHost } from 'xo'
|
||||
import { enableHost, detachHost, disableHost, restartHost } from 'xo'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
|
||||
@@ -39,6 +39,13 @@ export default ({
|
||||
labelId='enableHostLabel'
|
||||
/>
|
||||
}
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={detachHost}
|
||||
handlerParam={host}
|
||||
icon='host-eject'
|
||||
labelId='detachHost'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
|
||||
@@ -3,6 +3,7 @@ import Copiable from 'copiable'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import store from 'store'
|
||||
import Tags from 'tags'
|
||||
import { addTag, removeTag } from 'xo'
|
||||
import { BlockLink } from 'link'
|
||||
@@ -10,6 +11,7 @@ import { Container, Row, Col } from 'grid'
|
||||
import { FormattedRelative } from 'react-intl'
|
||||
import { formatSize } from 'utils'
|
||||
import Usage, { UsageElement } from 'usage'
|
||||
import { getObject } from 'selectors'
|
||||
import {
|
||||
CpuSparkLines,
|
||||
MemorySparkLines,
|
||||
@@ -22,70 +24,78 @@ export default ({
|
||||
host,
|
||||
vmController,
|
||||
vms
|
||||
}) => <Container>
|
||||
<br />
|
||||
<Row className='text-xs-center'>
|
||||
<Col mediumSize={3}>
|
||||
<h2>{host.CPUs.cpu_count}x <Icon icon='cpu' size='lg' /></h2>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <CpuSparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<h2>{formatSize(host.memory.size)} <Icon icon='memory' size='lg' /></h2>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <MemorySparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<BlockLink to={`/hosts/${host.id}/network`}><h2>{host.$PIFs.length}x <Icon icon='network' size='lg' /></h2></BlockLink>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <PifSparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<BlockLink to={`/hosts/${host.id}/disks`}><h2>{host.$PBDs.length}x <Icon icon='disk' size='lg' /></h2></BlockLink>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <LoadSparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row className='text-xs-center'>
|
||||
<Col mediumSize={3}>
|
||||
<p className='text-xs-center'>{_('started', { ago: <FormattedRelative value={host.startTime * 1000} /> })}</p>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<p>{host.license_params.sku_marketing_name} {host.version} ({host.license_params.sku_type})</p>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<Copiable tagName='p'>
|
||||
{host.address}
|
||||
</Copiable>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<p>{host.bios_strings['system-manufacturer']} {host.bios_strings['system-product-name']}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col className='text-xs-center'>
|
||||
<h5>RAM usage:</h5>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col smallOffset={1} mediumSize={10}>
|
||||
<Usage total={host.memory.size}>
|
||||
<UsageElement
|
||||
highlight
|
||||
tooltip='XenServer'
|
||||
value={vmController.memory.size}
|
||||
/>
|
||||
{map(vms, vm => <UsageElement
|
||||
tooltip={vm.name_label}
|
||||
key={vm.id}
|
||||
value={vm.memory.size}
|
||||
href={`#/vms/${vm.id}`}
|
||||
/>)}
|
||||
</Usage>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<h2 className='text-xs-center'>
|
||||
<Tags labels={host.tags} onDelete={tag => removeTag(host.id, tag)} onAdd={tag => addTag(host.id, tag)} />
|
||||
</h2>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
}) => {
|
||||
const pool = getObject(store.getState(), host.$pool)
|
||||
return <Container>
|
||||
<br />
|
||||
<Row className='text-xs-center'>
|
||||
<Col mediumSize={3}>
|
||||
<h2>{host.CPUs.cpu_count}x <Icon icon='cpu' size='lg' /></h2>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <CpuSparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<h2>{formatSize(host.memory.size)} <Icon icon='memory' size='lg' /></h2>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <MemorySparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<BlockLink to={`/hosts/${host.id}/network`}><h2>{host.$PIFs.length}x <Icon icon='network' size='lg' /></h2></BlockLink>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <PifSparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<BlockLink to={`/hosts/${host.id}/storage`}><h2>{host.$PBDs.length}x <Icon icon='disk' size='lg' /></h2></BlockLink>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <LoadSparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row className='text-xs-center'>
|
||||
<Col mediumSize={3}>
|
||||
<p className='text-xs-center'>{_('started', { ago: <FormattedRelative value={host.startTime * 1000} /> })}</p>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<p>{host.license_params.sku_marketing_name} {host.version} ({host.license_params.sku_type})</p>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<Copiable tagName='p'>
|
||||
{host.address}
|
||||
</Copiable>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<p>{host.bios_strings['system-manufacturer']} {host.bios_strings['system-product-name']}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col className='text-xs-center'>
|
||||
<h5>RAM usage:</h5>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col smallOffset={1} mediumSize={10}>
|
||||
<Usage total={host.memory.size}>
|
||||
<UsageElement
|
||||
highlight
|
||||
tooltip='XenServer'
|
||||
value={vmController.memory.size}
|
||||
/>
|
||||
{map(vms, vm => <UsageElement
|
||||
tooltip={vm.name_label}
|
||||
key={vm.id}
|
||||
value={vm.memory.size}
|
||||
href={`#/vms/${vm.id}`}
|
||||
/>)}
|
||||
</Usage>
|
||||
</Col>
|
||||
</Row>
|
||||
{pool && host.id === pool.master && <Row className='text-xs-center'>
|
||||
<Col>
|
||||
<h3><span className='tag tag-pill tag-info'>{_('pillMaster')}</span></h3>
|
||||
</Col>
|
||||
</Row>}
|
||||
<Row>
|
||||
<Col>
|
||||
<h2 className='text-xs-center'>
|
||||
<Tags labels={host.tags} onDelete={tag => removeTag(host.id, tag)} onAdd={tag => addTag(host.id, tag)} />
|
||||
</h2>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
}
|
||||
|
||||
@@ -3,15 +3,35 @@ import ActionRowButton from 'action-row-button'
|
||||
import React from 'react'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import some from 'lodash/some'
|
||||
import TabButton from 'tab-button'
|
||||
import { connectPif, createNetwork, deletePif, disconnectPif } from 'xo'
|
||||
import Tooltip from 'tooltip'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { Toggle } from 'form'
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import {
|
||||
connectPif,
|
||||
createNetwork,
|
||||
deletePif,
|
||||
disconnectPif,
|
||||
editNetwork
|
||||
} from 'xo'
|
||||
|
||||
export default ({
|
||||
const _toggleDefaultLockingMode = (component, tooltip) => tooltip
|
||||
? <Tooltip content={tooltip}>
|
||||
{component}
|
||||
</Tooltip>
|
||||
: component
|
||||
|
||||
export default connectStore(() => ({
|
||||
vifsByNetwork: createGetObjectsOfType('VIF').groupBy('$network')
|
||||
}))(({
|
||||
host,
|
||||
networks,
|
||||
pifs
|
||||
pifs,
|
||||
vifsByNetwork
|
||||
}) => <Container>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
@@ -37,13 +57,16 @@ export default ({
|
||||
<th>{_('pifAddressLabel')}</th>
|
||||
<th>{_('pifMacLabel')}</th>
|
||||
<th>{_('pifMtuLabel')}</th>
|
||||
<th>{_('defaultLockingMode')}</th>
|
||||
<th>{_('pifStatusLabel')}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(pifs, pif =>
|
||||
<tr key={pif.id}>
|
||||
{map(pifs, pif => {
|
||||
const pifInUse = some(vifsByNetwork[pif.$network], vif => vif.attached)
|
||||
|
||||
return <tr key={pif.id}>
|
||||
<td>{pif.device}</td>
|
||||
<td>{networks[pif.$network].name_label}</td>
|
||||
<td>{pif.vlan === -1
|
||||
@@ -53,6 +76,16 @@ export default ({
|
||||
<td>{pif.ip} ({pif.mode})</td>
|
||||
<td><pre>{pif.mac}</pre></td>
|
||||
<td>{pif.mtu}</td>
|
||||
<td className='text-xs-center'>
|
||||
{_toggleDefaultLockingMode(
|
||||
<Toggle
|
||||
disabled={pifInUse}
|
||||
onChange={() => editNetwork(pif.$network, { defaultIsLocked: !networks[pif.$network].defaultIsLocked })}
|
||||
value={networks[pif.$network].defaultIsLocked}
|
||||
/>,
|
||||
pifInUse && _('pifInUse')
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{pif.attached
|
||||
? <span className='tag tag-success'>
|
||||
@@ -82,7 +115,7 @@ export default ({
|
||||
</ButtonGroup>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
@@ -90,4 +123,4 @@ export default ({
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Container>)
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react'
|
||||
import _ from 'intl'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import Tooltip from 'tooltip'
|
||||
import { BlockLink } from 'link'
|
||||
import { TabButtonLink } from 'tab-button'
|
||||
import { formatSize } from 'utils'
|
||||
@@ -51,7 +52,9 @@ export default ({
|
||||
<td>{formatSize(sr.size)}</td>
|
||||
<td>
|
||||
{sr.size > 1 &&
|
||||
<meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90'></meter>
|
||||
<Tooltip content={_('spaceLeftTooltip', {used: Math.round((sr.physical_usage / sr.size) * 100), free: formatSize(sr.size - sr.physical_usage)})}>
|
||||
<meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90' />
|
||||
</Tooltip>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -114,9 +114,9 @@ export default class XoApp extends Component {
|
||||
{blocked ? <XoaUpdates /> : this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
<TooltipViewer />
|
||||
<Modal />
|
||||
<Notification />
|
||||
<TooltipViewer />
|
||||
</div>
|
||||
</IntlProvider>
|
||||
}
|
||||
|
||||
@@ -335,8 +335,8 @@ export default class Jobs extends Component {
|
||||
<tr>
|
||||
<th>{_('jobName')}</th>
|
||||
<th>{_('jobAction')}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -181,7 +181,7 @@ export default class Schedules extends Component {
|
||||
<th>{_('job')}</th>
|
||||
<th className='hidden-xs-down'>{_('jobScheduling')}</th>
|
||||
<th className='hidden-xs-down'>{_('jobTimezone')}</th>
|
||||
<th></th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -11,6 +11,7 @@ import propTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
import SortedTable from 'sorted-table'
|
||||
import Tooltip from 'tooltip'
|
||||
import { alert, confirm } from 'modal'
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObject } from 'selectors'
|
||||
@@ -89,7 +90,7 @@ const showCalls = log => alert(<span>{_('job')} {log.jobId}</span>, <Log log={lo
|
||||
const LOG_COLUMNS = [
|
||||
{
|
||||
name: '',
|
||||
itemRenderer: log => <ActionRowButton icon='preview' handler={showCalls} handlerParam={log} />
|
||||
itemRenderer: log => <Tooltip content={_('logDisplayDetails')}><ActionRowButton icon='preview' handler={showCalls} handlerParam={log} /></Tooltip>
|
||||
},
|
||||
{
|
||||
name: _('jobId'),
|
||||
|
||||
@@ -9,13 +9,19 @@ import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Button } from 'react-bootstrap-4/lib'
|
||||
import { connectStore, noop, getXoaPlan } from 'utils'
|
||||
import { signOut, subscribePermissions, subscribeResourceSets } from 'xo'
|
||||
import { UpdateTag } from '../xoa-updates'
|
||||
import {
|
||||
connect,
|
||||
signOut,
|
||||
subscribePermissions,
|
||||
subscribeResourceSets
|
||||
} from 'xo'
|
||||
import {
|
||||
createFilter,
|
||||
createGetObjectsOfType,
|
||||
createSelector,
|
||||
getLang,
|
||||
getStatus,
|
||||
getUser
|
||||
} from 'selectors'
|
||||
|
||||
@@ -31,6 +37,7 @@ import styles from './index.css'
|
||||
[ task => task.status === 'pending' ]
|
||||
),
|
||||
pools: createGetObjectsOfType('pool'),
|
||||
status: getStatus,
|
||||
user: getUser
|
||||
}), {
|
||||
withRef: true
|
||||
@@ -88,11 +95,12 @@ export default class Menu extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { nTasks, user } = this.props
|
||||
const { nTasks, status, user } = this.props
|
||||
const isAdmin = user && user.permission === 'admin'
|
||||
const noOperatablePools = this._getNoOperatablePools()
|
||||
const noResourceSets = isEmpty(this.state.resourceSets)
|
||||
|
||||
/* eslint-disable object-property-newline */
|
||||
const items = [
|
||||
{ to: '/home', icon: 'menu-home', label: 'homePage', subMenu: [
|
||||
{ to: '/home?t=VM', icon: 'vm', label: 'homeVmPage' },
|
||||
@@ -121,7 +129,9 @@ export default class Menu extends Component {
|
||||
{ to: '/settings/groups', icon: 'menu-settings-groups', label: 'settingsGroupsPage' },
|
||||
{ to: '/settings/acls', icon: 'menu-settings-acls', label: 'settingsAclsPage' },
|
||||
{ to: '/settings/remotes', icon: 'menu-backup-remotes', label: 'backupRemotesPage' },
|
||||
{ to: '/settings/plugins', icon: 'menu-settings-plugins', label: 'settingsPluginsPage' }
|
||||
{ to: '/settings/plugins', icon: 'menu-settings-plugins', label: 'settingsPluginsPage' },
|
||||
{ to: '/settings/logs', icon: 'menu-settings-logs', label: 'settingsLogsPage' },
|
||||
{ to: '/settings/ips', icon: 'ip', label: 'settingsIpsPage' }
|
||||
]},
|
||||
{ to: '/jobs/overview', icon: 'menu-jobs', label: 'jobsPage', subMenu: [
|
||||
{ to: '/jobs/overview', icon: 'menu-jobs-overview', label: 'jobsOverviewPage' },
|
||||
@@ -137,6 +147,7 @@ export default class Menu extends Component {
|
||||
!noOperatablePools && { to: '/vms/import', icon: 'menu-new-import', label: 'newImport' }
|
||||
]}
|
||||
]
|
||||
/* eslint-enable object-property-newline */
|
||||
|
||||
return <div className={classNames(
|
||||
'xo-menu',
|
||||
@@ -200,15 +211,24 @@ export default class Menu extends Component {
|
||||
<span className={styles.hiddenCollapsed}>{' '}{_('signOut')}</span>
|
||||
</Button>
|
||||
</li>
|
||||
<li className='nav-item'>
|
||||
<Link className='nav-link' style={{display: 'flex'}} to={'/user'}>
|
||||
<div style={{margin: 'auto'}}>
|
||||
<Tooltip content={user ? user.email : ''}>
|
||||
<Icon icon='user' size='lg' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<li className='nav-item xo-menu-item'>
|
||||
<Link className='nav-link text-xs-center' to={'/user'}>
|
||||
<Tooltip content={user ? user.email : ''}>
|
||||
<Icon icon='user' size='lg' />
|
||||
</Tooltip>
|
||||
</Link>
|
||||
</li>
|
||||
<li> </li>
|
||||
<li> </li>
|
||||
{status === 'connecting'
|
||||
? <li className='nav-item text-xs-center'>{_('statusConnecting')}</li>
|
||||
: status === 'disconnected' &&
|
||||
<li className='nav-item text-xs-center xo-menu-item'>
|
||||
<Button className='nav-link' onClick={connect}>
|
||||
<Icon icon='alarm' size='lg' fixedWidth /> {_('statusDisconnected')}
|
||||
</Button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -54,14 +54,13 @@
|
||||
|
||||
.configDrive {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #eee;
|
||||
padding: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.configDriveToggle {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.refreshNames {
|
||||
@@ -71,3 +70,7 @@
|
||||
.customConfig {
|
||||
resize: both;
|
||||
}
|
||||
|
||||
.fixedWidth {
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
@@ -13,27 +13,33 @@ import Icon from 'icon'
|
||||
import includes from 'lodash/includes'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isIp from 'is-ip'
|
||||
import isObject from 'lodash/isObject'
|
||||
import join from 'lodash/join'
|
||||
import map from 'lodash/map'
|
||||
import Page from '../page'
|
||||
import React from 'react'
|
||||
import size from 'lodash/size'
|
||||
import slice from 'lodash/slice'
|
||||
import store from 'store'
|
||||
import Tooltip from 'tooltip'
|
||||
import Wizard, { Section } from 'wizard'
|
||||
import { Button } from 'react-bootstrap-4/lib'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import {
|
||||
addSshKey,
|
||||
createVm,
|
||||
createVms,
|
||||
getCloudInitConfig,
|
||||
subscribeCurrentUser,
|
||||
subscribePermissions,
|
||||
subscribeResourceSets,
|
||||
XEN_DEFAULT_CPU_CAP,
|
||||
XEN_DEFAULT_CPU_WEIGHT
|
||||
} from 'xo'
|
||||
import {
|
||||
SelectIp,
|
||||
SelectNetwork,
|
||||
SelectPool,
|
||||
SelectResourceSet,
|
||||
@@ -42,6 +48,7 @@ import {
|
||||
SelectResourceSetsVdi,
|
||||
SelectResourceSetsVmTemplate,
|
||||
SelectSr,
|
||||
SelectSshKey,
|
||||
SelectVdi,
|
||||
SelectVmTemplate
|
||||
} from 'select-objects'
|
||||
@@ -50,6 +57,7 @@ import {
|
||||
Toggle
|
||||
} from 'form'
|
||||
import {
|
||||
addSubscriptions,
|
||||
buildTemplate,
|
||||
connectStore,
|
||||
formatSize,
|
||||
@@ -72,7 +80,9 @@ const NB_VMS_MAX = 100
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
// Sub-components --------------------------------------------------------------
|
||||
const getObject = createGetObject((_, id) => id)
|
||||
|
||||
// Sub-components
|
||||
|
||||
const SectionContent = ({ summary, column, children }) => (
|
||||
<div className={classNames(
|
||||
@@ -83,11 +93,13 @@ const SectionContent = ({ summary, column, children }) => (
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
const LineItem = ({ children }) => (
|
||||
<div className={styles.lineItem}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
const Item = ({ label, children, className }) => (
|
||||
<span className={styles.item}>
|
||||
{label && <span>{_(label)} </span>}
|
||||
@@ -95,21 +107,32 @@ const Item = ({ label, children, className }) => (
|
||||
</span>
|
||||
)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const getObject = createGetObject((_, id) => id)
|
||||
|
||||
@addSubscriptions({
|
||||
user: subscribeCurrentUser
|
||||
})
|
||||
@connectStore(() => ({
|
||||
isAdmin: createSelector(
|
||||
getUser,
|
||||
user => user && user.permission === 'admin'
|
||||
),
|
||||
networks: createGetObjectsOfType('network').sort(),
|
||||
pool: createGetObject((_, props) => props.location.query.pool),
|
||||
pools: createGetObjectsOfType('pool'),
|
||||
templates: createGetObjectsOfType('VM-template').sort()
|
||||
templates: createGetObjectsOfType('VM-template').sort(),
|
||||
userSshKeys: createSelector(
|
||||
(_, props) => {
|
||||
const user = props.user
|
||||
return user && user.preferences && user.preferences.sshKeys
|
||||
},
|
||||
keys => keys
|
||||
)
|
||||
}))
|
||||
@injectIntl
|
||||
export default class NewVm extends BaseComponent {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
@@ -159,10 +182,16 @@ export default class NewVm extends BaseComponent {
|
||||
}
|
||||
_replaceState = (state, callback) =>
|
||||
this.setState({ state }, callback)
|
||||
_linkState = (path, targetPath) =>
|
||||
this.linkState(`state.${path}`, targetPath)
|
||||
|
||||
// Actions ---------------------------------------------------------------------
|
||||
|
||||
_reset = ({ pool, resourceSet } = { pool: this.state.pool, resourceSet: this.state.resourceSet }) => {
|
||||
if (!pool) {
|
||||
pool = this.props.pool
|
||||
}
|
||||
|
||||
this.setState({ pool, resourceSet })
|
||||
this._replaceState({
|
||||
bootAfterCreate: true,
|
||||
@@ -215,7 +244,11 @@ export default class NewVm extends BaseComponent {
|
||||
if (state.configDrive) {
|
||||
const hostname = state.name_label.replace(/^\s+|\s+$/g, '').replace(/\s+/g, '-')
|
||||
if (state.installMethod === 'SSH') {
|
||||
cloudConfig = '#cloud-config\nhostname: ' + hostname + '\nssh_authorized_keys:\n - ' + state.sshKey + '\n'
|
||||
cloudConfig = `#cloud-config\nhostname: ${hostname}\nssh_authorized_keys:\n${
|
||||
join(map(state.sshKeys, keyId => {
|
||||
return this.props.userSshKeys[keyId] ? ` - ${this.props.userSshKeys[keyId].key}\n` : ''
|
||||
}), '')
|
||||
}`
|
||||
} else {
|
||||
cloudConfig = state.customConfig
|
||||
}
|
||||
@@ -223,6 +256,26 @@ export default class NewVm extends BaseComponent {
|
||||
cloudConfig = state.cloudConfig
|
||||
}
|
||||
|
||||
// Split allowed IPs into IPv4 and IPv6
|
||||
const { VIFs } = state
|
||||
const _VIFs = map(VIFs, vif => {
|
||||
const _vif = { ...vif }
|
||||
delete _vif.addresses
|
||||
_vif.allowedIpv4Addresses = []
|
||||
_vif.allowedIpv6Addresses = []
|
||||
forEach(vif.addresses, ip => {
|
||||
if (!isIp(ip)) {
|
||||
return
|
||||
}
|
||||
if (isIp.v4(ip)) {
|
||||
_vif.allowedIpv4Addresses.push(ip)
|
||||
} else {
|
||||
_vif.allowedIpv6Addresses.push(ip)
|
||||
}
|
||||
})
|
||||
return _vif
|
||||
})
|
||||
|
||||
const data = {
|
||||
clone: !this.isDiskTemplate && state.fastClone,
|
||||
existingDisks: state.existingDisks,
|
||||
@@ -230,9 +283,8 @@ export default class NewVm extends BaseComponent {
|
||||
name_label: state.name_label,
|
||||
template: state.template.id,
|
||||
VDIs: state.VDIs,
|
||||
VIFs: state.VIFs,
|
||||
VIFs: _VIFs,
|
||||
resourceSet: resourceSet && resourceSet.id,
|
||||
// TODO: To be added in xo-server
|
||||
// vm.set parameters
|
||||
CPUs: state.CPUs,
|
||||
cpuWeight: state.cpuWeight === '' ? null : state.cpuWeight,
|
||||
@@ -310,7 +362,8 @@ export default class NewVm extends BaseComponent {
|
||||
cpuCap: '',
|
||||
cpuWeight: '',
|
||||
// installation
|
||||
installMethod: template.install_methods && template.install_methods[0] || state.installMethod,
|
||||
installMethod: template.install_methods && template.install_methods[0] || 'SSH',
|
||||
sshKeys: this.props.userSshKeys && this.props.userSshKeys.length && [ 0 ],
|
||||
customConfig: '#cloud-config\n#hostname: myhostname\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n',
|
||||
// interfaces
|
||||
VIFs,
|
||||
@@ -375,8 +428,10 @@ export default class NewVm extends BaseComponent {
|
||||
(isInPool, isInResourceSet) => disk =>
|
||||
(isInResourceSet(disk) || isInPool(disk)) && disk.content_type !== 'iso' && disk.size > 0
|
||||
)
|
||||
_getIsoPredicate = () => disk =>
|
||||
disk.content_type === 'iso'
|
||||
_getIsoPredicate = createSelector(
|
||||
() => this.state.pool && this.state.pool.id,
|
||||
poolId => sr => (poolId == null || poolId === sr.$pool) && sr.SR_type === 'iso'
|
||||
)
|
||||
_getNetworkPredicate = createSelector(
|
||||
this._getIsInPool,
|
||||
this._getIsInResourceSet,
|
||||
@@ -467,11 +522,13 @@ export default class NewVm extends BaseComponent {
|
||||
this._setState({ [prop]: value })
|
||||
}
|
||||
}
|
||||
_onChangeSshKeys = keys => this._setState({ sshKeys: map(keys, key => key.id) })
|
||||
|
||||
_updateNbVms = () => {
|
||||
const { nbVms, nameLabels, seqStart } = this.state.state
|
||||
const nbVmsClamped = clamp(nbVms, NB_VMS_MIN, NB_VMS_MAX)
|
||||
const newNameLabels = [ ...nameLabels ]
|
||||
|
||||
if (nbVmsClamped < nameLabels.length) {
|
||||
this._setState({ nameLabels: slice(newNameLabels, 0, nbVmsClamped) })
|
||||
} else {
|
||||
@@ -487,6 +544,7 @@ export default class NewVm extends BaseComponent {
|
||||
const nbVms = nameLabels.length
|
||||
const newNameLabels = []
|
||||
const replacer = this._buildTemplate()
|
||||
|
||||
for (let i = +seqStart; i <= +seqStart + nbVms - 1; i++) {
|
||||
newNameLabels.push(replacer(this.state.state, i))
|
||||
}
|
||||
@@ -494,11 +552,19 @@ export default class NewVm extends BaseComponent {
|
||||
}
|
||||
_selectResourceSet = resourceSet =>
|
||||
this._reset({ pool: undefined, resourceSet })
|
||||
_selectPool = pool =>
|
||||
_selectPool = pool => {
|
||||
const { pathname, query } = this.props.location
|
||||
|
||||
this.context.router.push({
|
||||
pathname,
|
||||
query: { ...query, pool: pool.id }
|
||||
})
|
||||
this._reset({ pool, resourceSet: undefined })
|
||||
}
|
||||
_addVdi = () => {
|
||||
const { pool, state } = this.state
|
||||
const device = String(this.getUniqueId())
|
||||
|
||||
this._setState({ VDIs: [ ...state.VDIs, {
|
||||
device,
|
||||
name_description: 'Created by XO',
|
||||
@@ -509,10 +575,12 @@ export default class NewVm extends BaseComponent {
|
||||
}
|
||||
_removeVdi = index => {
|
||||
const { VDIs } = this.state.state
|
||||
|
||||
this._setState({ VDIs: [ ...VDIs.slice(0, index), ...VDIs.slice(index + 1) ] })
|
||||
}
|
||||
_addInterface = () => {
|
||||
const networkId = this._getDefaultNetworkId()
|
||||
|
||||
this._setState({ VIFs: [ ...this.state.state.VIFs, {
|
||||
id: this.getUniqueId(),
|
||||
network: networkId
|
||||
@@ -520,9 +588,29 @@ export default class NewVm extends BaseComponent {
|
||||
}
|
||||
_removeInterface = index => {
|
||||
const { VIFs } = this.state.state
|
||||
|
||||
this._setState({ VIFs: [ ...VIFs.slice(0, index), ...VIFs.slice(index + 1) ] })
|
||||
}
|
||||
|
||||
_addNewSshKey = () => {
|
||||
const { newSshKey, sshKeys } = this.state.state
|
||||
const { userSshKeys } = this.props
|
||||
const splitKey = newSshKey.split(' ')
|
||||
const title = splitKey.length === 3 ? splitKey[2].split('\n')[0] : newSshKey.substring(newSshKey.length - 10, newSshKey.length)
|
||||
|
||||
// save key
|
||||
addSshKey({
|
||||
title,
|
||||
key: newSshKey
|
||||
}).then(() => {
|
||||
// select key
|
||||
this._setState({
|
||||
sshKeys: [ ...(sshKeys || []), userSshKeys ? userSshKeys.length : 0 ],
|
||||
newSshKey: ''
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getRedirectionUrl = id =>
|
||||
this.state.state.multipleVms ? '/home' : `/vms/${id}`
|
||||
|
||||
@@ -700,11 +788,11 @@ export default class NewVm extends BaseComponent {
|
||||
value={nbVms}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<Button bsStyle='secondary' disabled={!multipleVms} onClick={this._updateNbVms}><Icon icon='arrow-right' /></Button>
|
||||
<Tooltip content={_('newVmNumberRecalculate')}><Button bsStyle='secondary' disabled={!multipleVms} onClick={this._updateNbVms}><Icon icon='arrow-right' /></Button></Tooltip>
|
||||
</span>
|
||||
</Item>
|
||||
<Item>
|
||||
<a className={styles.refreshNames} onClick={this._updateNameLabels}><Icon icon='refresh' /></a>
|
||||
<Tooltip content={_('newVmNameRefresh')}><a className={styles.refreshNames} onClick={this._updateNameLabels}><Icon icon='refresh' /></a></Tooltip>
|
||||
</Item>
|
||||
{multipleVms && <LineItem>
|
||||
{map(nameLabels, (nameLabel, index) =>
|
||||
@@ -784,44 +872,64 @@ export default class NewVm extends BaseComponent {
|
||||
installIso,
|
||||
installMethod,
|
||||
installNetwork,
|
||||
newSshKey,
|
||||
pv_args,
|
||||
sshKey
|
||||
sshKeys
|
||||
} = this.state.state
|
||||
return <Section icon='new-vm-install-settings' title='newVmInstallSettingsPanel' done={this._isInstallSettingsDone()}>
|
||||
{this._isDiskTemplate ? <SectionContent key='diskTemplate'>
|
||||
<div className={styles.configDrive}>
|
||||
<span className={styles.configDriveToggle}>
|
||||
{_('newVmConfigDrive')}
|
||||
</span>
|
||||
<span className={styles.configDriveToggle}>
|
||||
<Toggle
|
||||
value={configDrive}
|
||||
onChange={this._getOnChange('configDrive')}
|
||||
{this._isDiskTemplate ? <SectionContent key='diskTemplate' column>
|
||||
<LineItem>
|
||||
<div className={styles.configDrive}>
|
||||
<span className={styles.configDriveToggle}>
|
||||
{_('newVmConfigDrive')}
|
||||
</span>
|
||||
|
||||
<span className={styles.configDriveToggle}>
|
||||
<Toggle
|
||||
value={configDrive}
|
||||
onChange={this._getOnChange('configDrive')}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</LineItem>
|
||||
<LineItem>
|
||||
<span>
|
||||
<input
|
||||
checked={installMethod === 'SSH'}
|
||||
disabled={!configDrive}
|
||||
name='installMethod'
|
||||
onChange={this._getOnChange('installMethod')}
|
||||
type='radio'
|
||||
value='SSH'
|
||||
/>
|
||||
{' '}
|
||||
<span>{_('newVmSshKey')}</span>
|
||||
</span>
|
||||
</div>
|
||||
<Item>
|
||||
<input
|
||||
checked={installMethod === 'SSH'}
|
||||
disabled={!configDrive}
|
||||
name='installMethod'
|
||||
onChange={this._getOnChange('installMethod')}
|
||||
type='radio'
|
||||
value='SSH'
|
||||
/>
|
||||
{' '}
|
||||
<span>{_('newVmSshKey')}</span>
|
||||
{' '}
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
disabled={!configDrive || installMethod !== 'SSH'}
|
||||
onChange={this._getOnChange('sshKey')}
|
||||
type='text'
|
||||
value={sshKey}
|
||||
/>
|
||||
</Item>
|
||||
<Item>
|
||||
|
||||
<span className={classNames('input-group', styles.fixedWidth)}>
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
disabled={!configDrive || installMethod !== 'SSH'}
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
onChange={this._getOnChange('newSshKey')}
|
||||
value={newSshKey}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<Button className='btn btn-secondary' onClick={this._addNewSshKey} disabled={!newSshKey}>
|
||||
<Icon icon='add' />
|
||||
</Button>
|
||||
</span>
|
||||
</span>
|
||||
{this.props.userSshKeys && this.props.userSshKeys.length > 0 && <span className={styles.fixedWidth}>
|
||||
<SelectSshKey
|
||||
disabled={!configDrive || installMethod !== 'SSH'}
|
||||
onChange={this._onChangeSshKeys}
|
||||
multi
|
||||
value={sshKeys || []}
|
||||
/>
|
||||
</span>}
|
||||
</LineItem>
|
||||
<LineItem>
|
||||
<input
|
||||
checked={installMethod === 'customConfig'}
|
||||
disabled={!configDrive}
|
||||
@@ -830,9 +938,9 @@ export default class NewVm extends BaseComponent {
|
||||
type='radio'
|
||||
value='customConfig'
|
||||
/>
|
||||
{' '}
|
||||
|
||||
<span>{_('newVmCustomConfig')}</span>
|
||||
{' '}
|
||||
|
||||
<DebounceInput
|
||||
className={classNames('form-control', styles.customConfig)}
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
@@ -841,7 +949,7 @@ export default class NewVm extends BaseComponent {
|
||||
onChange={this._getOnChange('customConfig')}
|
||||
value={customConfig}
|
||||
/>
|
||||
</Item>
|
||||
</LineItem>
|
||||
</SectionContent>
|
||||
: <SectionContent>
|
||||
<Item>
|
||||
@@ -938,7 +1046,7 @@ export default class NewVm extends BaseComponent {
|
||||
installIso,
|
||||
installMethod,
|
||||
installNetwork,
|
||||
sshKey,
|
||||
sshKeys,
|
||||
template
|
||||
} = this.state.state
|
||||
switch (installMethod) {
|
||||
@@ -946,7 +1054,7 @@ export default class NewVm extends BaseComponent {
|
||||
case 'ISO': return installIso
|
||||
case 'network': return /^(http|ftp|nfs)/i.exec(installNetwork)
|
||||
case 'PXE': return true
|
||||
case 'SSH': return sshKey || !configDrive
|
||||
case 'SSH': return !isEmpty(sshKeys) || !configDrive
|
||||
default: return template && this._isDiskTemplate && !configDrive
|
||||
}
|
||||
}
|
||||
@@ -954,11 +1062,12 @@ export default class NewVm extends BaseComponent {
|
||||
// INTERFACES ------------------------------------------------------------------
|
||||
|
||||
_renderInterfaces = () => {
|
||||
const { formatMessage } = this.props.intl
|
||||
const {
|
||||
state: { VIFs },
|
||||
pool
|
||||
} = this.state
|
||||
const { formatMessage } = this.props.intl
|
||||
|
||||
return <Section icon='new-vm-interfaces' title='newVmInterfacesPanel' done={this._isInterfacesDone()}>
|
||||
<SectionContent column>
|
||||
{map(VIFs, (vif, index) => <div key={index}>
|
||||
@@ -967,7 +1076,7 @@ export default class NewVm extends BaseComponent {
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
onChange={this._getOnChange('VIFs', index, 'mac')}
|
||||
onChange={this._linkState(`VIFs.${index}.mac`)}
|
||||
placeholder={formatMessage(messages.newVmMacPlaceholder)}
|
||||
rows={7}
|
||||
value={vif.mac}
|
||||
@@ -976,17 +1085,27 @@ export default class NewVm extends BaseComponent {
|
||||
<Item label='newVmNetworkLabel'>
|
||||
<span className={styles.inlineSelect}>
|
||||
{pool ? <SelectNetwork
|
||||
onChange={this._getOnChange('VIFs', index, 'network', 'id')}
|
||||
onChange={this._linkState(`VIFs.${index}.network`, 'id')}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
value={vif.network}
|
||||
/>
|
||||
: <SelectResourceSetsNetwork
|
||||
onChange={this._getOnChange('VIFs', index, 'network', 'id')}
|
||||
onChange={this._linkState(`VIFs.${index}.network`, 'id')}
|
||||
resourceSet={this.state.resourceSet}
|
||||
value={vif.network}
|
||||
/>}
|
||||
</span>
|
||||
</Item>
|
||||
<LineItem>
|
||||
<span className={styles.inlineSelect}>
|
||||
<SelectIp
|
||||
containerPredicate={pool => find(pool.networks, poolNetwork => poolNetwork === vif.network)}
|
||||
multi
|
||||
onChange={this._linkState(`VIFs.${index}.addresses`, '*.id')}
|
||||
value={vif.addresses}
|
||||
/>
|
||||
</span>
|
||||
</LineItem>
|
||||
<Item>
|
||||
<Button onClick={() => this._removeInterface(index)} bsStyle='secondary'>
|
||||
<Icon icon='new-vm-remove' />
|
||||
@@ -994,8 +1113,7 @@ export default class NewVm extends BaseComponent {
|
||||
</Item>
|
||||
</LineItem>
|
||||
{index < VIFs.length - 1 && <hr />}
|
||||
</div>
|
||||
)}
|
||||
</div>)}
|
||||
<Item>
|
||||
<Button onClick={this._addInterface} bsStyle='secondary'>
|
||||
<Icon icon='new-vm-add' />
|
||||
@@ -1207,4 +1325,4 @@ export default class NewVm extends BaseComponent {
|
||||
</Section>
|
||||
}
|
||||
}
|
||||
/* eslint-enable camelcase*/
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
@@ -7,6 +7,7 @@ import styles from './index.css'
|
||||
|
||||
const Page = ({
|
||||
children,
|
||||
collapsedHeader,
|
||||
formatTitle,
|
||||
header,
|
||||
intl,
|
||||
@@ -16,9 +17,9 @@ const Page = ({
|
||||
return (
|
||||
<DocumentTitle title={formatTitle ? formatMessage(messages[title]) : title}>
|
||||
<div className={styles.container}>
|
||||
<nav className={'page-header ' + styles.header}>
|
||||
{!collapsedHeader && <nav className={'page-header ' + styles.header}>
|
||||
{header}
|
||||
</nav>
|
||||
</nav>}
|
||||
<div className={styles.content}>
|
||||
{children}
|
||||
</div>
|
||||
@@ -29,6 +30,7 @@ const Page = ({
|
||||
|
||||
Page.propTypes = {
|
||||
children: React.PropTypes.node,
|
||||
collapsedHeader: React.PropTypes.bool,
|
||||
formatTitle: React.PropTypes.bool,
|
||||
header: React.PropTypes.node,
|
||||
title: React.PropTypes.string
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import ActionBar from 'action-bar'
|
||||
import React from 'react'
|
||||
import {
|
||||
addHostToPool
|
||||
} from 'xo'
|
||||
|
||||
const NOT_IMPLEMENTED = () => {
|
||||
throw new Error('not implemented')
|
||||
@@ -11,17 +14,17 @@ const PoolActionBar = ({ pool }) => (
|
||||
{
|
||||
icon: 'add-sr',
|
||||
label: 'addSrLabel',
|
||||
handler: NOT_IMPLEMENTED // TODO add sr
|
||||
redirectOnSuccess: `new/sr?host=${pool.master}`
|
||||
},
|
||||
{
|
||||
icon: 'add-vm',
|
||||
label: 'addVmLabel',
|
||||
handler: NOT_IMPLEMENTED // TODO add VM
|
||||
redirectOnSuccess: `vms/new?pool=${pool.id}`
|
||||
},
|
||||
{
|
||||
icon: 'add-host',
|
||||
label: 'addHostLabel',
|
||||
handler: NOT_IMPLEMENTED // TODO add host
|
||||
handler: addHostToPool
|
||||
},
|
||||
{
|
||||
icon: 'disconnect',
|
||||
|
||||
@@ -95,6 +95,9 @@ import TabStorage from './tab-storage'
|
||||
})
|
||||
export default class Pool extends Component {
|
||||
|
||||
_setNameDescription = nameDescription => editPool(this.props.pool, { name_description: nameDescription })
|
||||
_setNameLabel = nameLabel => editPool(this.props.pool, { name_label: nameLabel })
|
||||
|
||||
header () {
|
||||
const { pool } = this.props
|
||||
if (!pool) {
|
||||
@@ -108,13 +111,13 @@ export default class Pool extends Component {
|
||||
{' '}
|
||||
<Text
|
||||
value={pool.name_label}
|
||||
onChange={nameLabel => editPool(pool, { nameLabel })}
|
||||
onChange={this._setNameLabel}
|
||||
/>
|
||||
</h2>
|
||||
<span>
|
||||
<Text
|
||||
value={pool.name_description}
|
||||
onChange={nameDescription => editPool(pool, { nameDescription })}
|
||||
onChange={this._setNameDescription}
|
||||
/>
|
||||
</span>
|
||||
</Col>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import _ from 'intl'
|
||||
import find from 'lodash/find'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import sumBy from 'lodash/sumBy'
|
||||
import Tags from 'tags'
|
||||
import { addTag, removeTag } from 'xo'
|
||||
import Link, { BlockLink } from 'link'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import Usage, { UsageElement } from 'usage'
|
||||
import { formatSize } from 'utils'
|
||||
|
||||
export default ({
|
||||
hosts,
|
||||
@@ -10,20 +17,49 @@ export default ({
|
||||
pool,
|
||||
srs
|
||||
}) => <Container>
|
||||
<br />
|
||||
<Row className='text-xs-center'>
|
||||
<Col mediumSize={4}>
|
||||
<h2>{hosts.length}x <Icon icon='host' size='lg' /></h2>
|
||||
<BlockLink to={`/pools/${pool.id}/hosts`}><h2>{hosts.length}x <Icon icon='host' size='lg' /></h2></BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<h2>{srs.length}x <Icon icon='sr' size='lg' /></h2>
|
||||
<BlockLink to={`/pools/${pool.id}/storage`}><h2>{srs.length}x <Icon icon='sr' size='lg' /></h2></BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<h2>{nVms}x <Icon icon='vm' size='lg' /></h2>
|
||||
<BlockLink to={`/home?s=$pool:${pool.id}`}><h2>{nVms}x <Icon icon='vm' size='lg' /></h2></BlockLink>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
<Col className='text-xs-center'>
|
||||
<h5>Pool RAM usage:</h5>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col smallOffset={1} mediumSize={10}>
|
||||
<Usage total={sumBy(hosts, 'memory.size')}>
|
||||
{map(hosts, host => <UsageElement
|
||||
tooltip={host.name_label}
|
||||
key={host.id}
|
||||
value={host.memory.usage}
|
||||
href={`#/hosts/${host.id}`}
|
||||
/>)}
|
||||
</Usage>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col className='text-xs-center'>
|
||||
<h5>{_('poolRamUsage', {used: formatSize(sumBy(hosts, 'memory.usage')), total: formatSize(sumBy(hosts, 'memory.size'))})}</h5>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='text-xs-center'>
|
||||
<Col>
|
||||
<h2 className='text-xs-center'>
|
||||
{_('poolMaster')} <Link to={`/hosts/${pool.master}`}>{find(hosts, host => host.id === pool.master).name_label}</Link>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='text-xs-center'>
|
||||
<Col>
|
||||
<h2>
|
||||
<Tags labels={pool.tags} onDelete={tag => removeTag(pool.id, tag)} onAdd={tag => addTag(pool.id, tag)} />
|
||||
</h2>
|
||||
</Col>
|
||||
|
||||
@@ -3,17 +3,24 @@ import isEmpty from 'lodash/isEmpty'
|
||||
import Link from 'link'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import store from 'store'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { editHost } from 'xo'
|
||||
import { Text } from 'editable'
|
||||
import { formatSize } from 'utils'
|
||||
import { getObject } from 'selectors'
|
||||
|
||||
const HOST_COLUMNS = [
|
||||
{
|
||||
name: _('hostNameLabel'),
|
||||
itemRenderer: host => (
|
||||
<Link to={`/hosts/${host.id}`}>
|
||||
<Text value={host.name_label} onChange={value => editHost(host, { name_label: value })} useLongClick />
|
||||
</Link>
|
||||
<span>
|
||||
<Link to={`/hosts/${host.id}`}>
|
||||
<Text value={host.name_label} onChange={value => editHost(host, { name_label: value })} useLongClick />
|
||||
</Link>
|
||||
{host.id === getObject(store.getState(), host.$pool).master && <span className='tag tag-pill tag-info'>{_('pillMaster')}</span>}
|
||||
</span>
|
||||
),
|
||||
sortCriteria: 'name_label'
|
||||
},
|
||||
@@ -24,7 +31,10 @@ const HOST_COLUMNS = [
|
||||
},
|
||||
{
|
||||
name: _('hostMemory'),
|
||||
itemRenderer: ({ memory }) => <meter value={memory.usage} min='0' max={memory.size}></meter>,
|
||||
itemRenderer: ({ memory }) =>
|
||||
<Tooltip content={_('memoryLeftTooltip', {used: Math.round((memory.usage / memory.size) * 100), free: formatSize(memory.size - memory.usage)})}>
|
||||
<meter value={memory.usage} min='0' max={memory.size} />
|
||||
</Tooltip>,
|
||||
sortCriteria: ({ memory }) => memory.usage / memory.size,
|
||||
sortOrder: 'desc'
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ import React, { Component } from 'react'
|
||||
import some from 'lodash/some'
|
||||
import store from 'store'
|
||||
import TabButton from 'tab-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { Text } from 'editable'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObject, createSelector } from 'selectors'
|
||||
import { createGetObject, createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { Toggle } from 'form'
|
||||
import {
|
||||
connectPif,
|
||||
createNetwork,
|
||||
@@ -24,6 +26,12 @@ const getObject = createGetObject((_, id) => id)
|
||||
const disableUnplug = pif =>
|
||||
pif.attached && (pif.management || pif.disallowUnplug)
|
||||
|
||||
const _toggleDefaultLockingMode = (component, tooltip) => tooltip
|
||||
? <Tooltip content={tooltip}>
|
||||
{component}
|
||||
</Tooltip>
|
||||
: component
|
||||
|
||||
@connectStore(() => {
|
||||
const pif = createGetObject()
|
||||
const host = createGetObject(
|
||||
@@ -69,6 +77,9 @@ class PifItem extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
@connectStore(() => ({
|
||||
vifsByNetwork: createGetObjectsOfType('VIF').groupBy('$network')
|
||||
}))
|
||||
export default class TabNetworks extends Component {
|
||||
_disableDelete = network => {
|
||||
const state = store.getState()
|
||||
@@ -76,7 +87,7 @@ export default class TabNetworks extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { networks } = this.props
|
||||
const { networks, vifsByNetwork } = this.props
|
||||
|
||||
return <Container>
|
||||
<Row>
|
||||
@@ -99,20 +110,33 @@ export default class TabNetworks extends Component {
|
||||
<th>{_('poolNetworkNameLabel')}</th>
|
||||
<th>{_('poolNetworkDescription')}</th>
|
||||
<th>{_('poolNetworkMTU')}</th>
|
||||
<th>{_('defaultLockingMode')}</th>
|
||||
<th>{_('poolNetworkPif')}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(networks, network => {
|
||||
const networkInUse = some(vifsByNetwork[network.id], vif => vif.attached)
|
||||
|
||||
return <tr key={network.id}>
|
||||
<td>
|
||||
<Text value={network.name_label} onChange={value => editNetwork(network, { name_label: value })} />
|
||||
</td>
|
||||
<td>
|
||||
<Text value={network.name_description} onChange={value => editNetwork(network, { name_label: value })} />
|
||||
<Text value={network.name_description} onChange={value => editNetwork(network, { name_description: value })} />
|
||||
</td>
|
||||
<td>{network.MTU}</td>
|
||||
<td className='text-xs-center'>
|
||||
{_toggleDefaultLockingMode(
|
||||
<Toggle
|
||||
disabled={networkInUse}
|
||||
onChange={() => editNetwork(network, { defaultIsLocked: !network.defaultIsLocked })}
|
||||
value={network.defaultIsLocked}
|
||||
/>,
|
||||
networkInUse && _('networkInUse')
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{!isEmpty(network.PIFs) && <table className='table'>
|
||||
<thead className='thead-default'>
|
||||
|
||||
@@ -3,6 +3,7 @@ import isEmpty from 'lodash/isEmpty'
|
||||
import Link from 'link'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { editSr, isSrShared } from 'xo'
|
||||
import { formatSize } from 'utils'
|
||||
@@ -30,7 +31,10 @@ const SR_COLUMNS = [
|
||||
},
|
||||
{
|
||||
name: _('srUsage'),
|
||||
itemRenderer: sr => sr.size > 1 && <meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90'></meter>,
|
||||
itemRenderer: sr => sr.size > 1 &&
|
||||
<Tooltip content={_('spaceLeftTooltip', {used: Math.round((sr.physical_usage / sr.size) * 100), free: formatSize(sr.size - sr.physical_usage)})}>
|
||||
<meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90' />
|
||||
</Tooltip>,
|
||||
sortCriteria: sr => sr.physical_usage / sr.size,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
|
||||
@@ -8,6 +8,8 @@ import { NavLink, NavTabs } from 'nav'
|
||||
|
||||
import Acls from './acls'
|
||||
import Groups from './groups'
|
||||
import Ips from './ips'
|
||||
import Logs from './logs'
|
||||
import Plugins from './plugins'
|
||||
import Remotes from './remotes'
|
||||
import Servers from './servers'
|
||||
@@ -26,6 +28,8 @@ const HEADER = <Container>
|
||||
<NavLink to={'/settings/acls'}><Icon icon='menu-settings-acls' /> {_('settingsAclsPage')}</NavLink>
|
||||
<NavLink to={'/settings/remotes'}><Icon icon='menu-backup-remotes' /> {_('backupRemotesPage')}</NavLink>
|
||||
<NavLink to={'/settings/plugins'}><Icon icon='menu-settings-plugins' /> {_('settingsPluginsPage')}</NavLink>
|
||||
<NavLink to={'/settings/logs'}><Icon icon='menu-settings-logs' /> {_('settingsLogsPage')}</NavLink>
|
||||
<NavLink to={'/settings/ips'}><Icon icon='ip' /> {_('settingsIpsPage')}</NavLink>
|
||||
</NavTabs>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -34,6 +38,8 @@ const HEADER = <Container>
|
||||
const Settings = routes('servers', {
|
||||
acls: Acls,
|
||||
groups: Groups,
|
||||
ips: Ips,
|
||||
logs: Logs,
|
||||
plugins: Plugins,
|
||||
remotes: Remotes,
|
||||
servers: Servers,
|
||||
|
||||
324
src/xo-app/settings/ips/index.js
Normal file
324
src/xo-app/settings/ips/index.js
Normal file
@@ -0,0 +1,324 @@
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import BaseComponent from 'base-component'
|
||||
import DebounceInput from 'react-debounce-input'
|
||||
import findIndex from 'lodash/findIndex'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import includes from 'lodash/includes'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isObject from 'lodash/isObject'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import SortedTable from 'sorted-table'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { addSubscriptions, connectStore } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { formatIps, getNextIpV4, parseIpPattern } from 'ip'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { SelectNetwork } from 'select-objects'
|
||||
import { Text } from 'editable'
|
||||
import {
|
||||
createIpPool,
|
||||
deleteIpPool,
|
||||
setIpPool,
|
||||
subscribeIpPools
|
||||
} from 'xo'
|
||||
|
||||
const FULL_WIDTH = { width: '100%' }
|
||||
const NETWORK_FORM_STYLE = { maxWidth: '40em' }
|
||||
|
||||
@connectStore(() => ({
|
||||
networks: createGetObjectsOfType('network').groupBy('id'),
|
||||
vifs: createGetObjectsOfType('VIF').groupBy('id')
|
||||
}))
|
||||
class IpsCell extends BaseComponent {
|
||||
_addIps = () => {
|
||||
const addresses = {}
|
||||
forEach(parseIpPattern(this.state.newIps), ip => {
|
||||
addresses[ip] = {}
|
||||
})
|
||||
setIpPool(this.props.ipPool.id, { addresses })
|
||||
this.setState({ newIps: '' })
|
||||
}
|
||||
|
||||
_deleteIp = ip => {
|
||||
const toBeRemoved = {}
|
||||
if (isObject(ip)) {
|
||||
let currentIp = ip.first
|
||||
while (currentIp !== ip.last) {
|
||||
toBeRemoved[currentIp] = null
|
||||
currentIp = getNextIpV4(currentIp)
|
||||
}
|
||||
toBeRemoved[currentIp] = null
|
||||
} else {
|
||||
toBeRemoved[ip] = null
|
||||
}
|
||||
setIpPool(this.props.ipPool.id, { addresses: toBeRemoved })
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
ipPool,
|
||||
networks,
|
||||
vifs
|
||||
} = this.props
|
||||
const {
|
||||
newIps,
|
||||
showNewIpForm
|
||||
} = this.state
|
||||
|
||||
return <Container>
|
||||
<Row>
|
||||
<Col mediumSize={6} offset={5}><strong>{_('ipsVifs')}</strong></Col>
|
||||
</Row>
|
||||
{ipPool.addresses && map(formatIps(keys(ipPool.addresses)), ip => {
|
||||
if (isObject(ip)) { // Range of IPs
|
||||
return <Row>
|
||||
<Col mediumSize={5}>
|
||||
<strong>{ip.first} <Icon icon='arrow-right' /> {ip.last}</strong>
|
||||
</Col>
|
||||
<Col mediumSize={1} offset={6}>
|
||||
<ActionRowButton
|
||||
handler={this._deleteIp}
|
||||
handlerParam={ip}
|
||||
icon='delete'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
const addressVifs = ipPool.addresses[ip].vifs
|
||||
return <Row>
|
||||
<Col mediumSize={5}>
|
||||
<strong>{ip}</strong>
|
||||
</Col>
|
||||
<Col mediumSize={6}>{!isEmpty(addressVifs)
|
||||
? map(addressVifs, vifId => {
|
||||
const vif = vifs[vifId][0]
|
||||
const network = networks[vif.$network][0]
|
||||
|
||||
return `${network.name_label} #${vif.device}`
|
||||
}).join(', ')
|
||||
: <em>{_('ipsNotUsed')}</em>
|
||||
}</Col>
|
||||
<Col mediumSize={1}>
|
||||
<ActionRowButton
|
||||
handler={this._deleteIp}
|
||||
handlerParam={ip}
|
||||
icon='delete'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
})}
|
||||
<Row>
|
||||
<Col>
|
||||
{showNewIpForm
|
||||
? <form id='newIpForm' className='form-inline'>
|
||||
<ActionButton btnStyle='danger' handler={this.toggleState('showNewIpForm')} icon='remove' />
|
||||
{' '}
|
||||
<DebounceInput
|
||||
autoFocus
|
||||
onChange={this.linkState('newIps')}
|
||||
type='text'
|
||||
className='form-control'
|
||||
required
|
||||
value={newIps || ''}
|
||||
/>
|
||||
{' '}
|
||||
<ActionButton form={`newIpForm`} icon='save' btnStyle='primary' handler={this._addIps} />
|
||||
</form>
|
||||
: <ActionButton btnStyle='success' size='small' handler={this.toggleState('showNewIpForm')} icon='add' />}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
}
|
||||
}
|
||||
|
||||
@connectStore(() => {
|
||||
const getNetworks = createGetObjectsOfType('network')
|
||||
|
||||
return (state, props) => ({
|
||||
networks: getNetworks(state, props)
|
||||
})
|
||||
})
|
||||
class NetworksCell extends BaseComponent {
|
||||
state = { newNetworks: [] }
|
||||
|
||||
_addNetworks = () => {
|
||||
if (isEmpty(this.state.newNetworks)) {
|
||||
return this._toggleNewNetworks()
|
||||
}
|
||||
const { ipPool } = this.props
|
||||
setIpPool(ipPool.id, {
|
||||
networks: [ ...ipPool.networks, ...this.state.newNetworks ]
|
||||
})
|
||||
this._toggleNewNetworks()
|
||||
this.setState({ newNetworks: [] })
|
||||
}
|
||||
|
||||
_deleteNetwork = networkId => {
|
||||
const _networks = [ ...this.props.ipPool.networks ]
|
||||
const index = findIndex(_networks, network => network === networkId)
|
||||
if (index !== -1) {
|
||||
_networks.splice(index, 1)
|
||||
setIpPool(this.props.ipPool.id, { networks: _networks })
|
||||
}
|
||||
}
|
||||
|
||||
_toggleNewNetworks = () =>
|
||||
this.setState({ showNewNetworkForm: !this.state.showNewNetworkForm })
|
||||
_getNetworkPredicate = createSelector(
|
||||
() => this.props.ipPool && this.props.ipPool.networks,
|
||||
networks => network =>
|
||||
!includes(networks, network.id)
|
||||
)
|
||||
|
||||
render () {
|
||||
const { ipPool, networks } = this.props
|
||||
const { newNetworks, showNewNetworkForm } = this.state
|
||||
|
||||
return <Container>
|
||||
{map(ipPool.networks, networkId => <Row>
|
||||
<Col mediumSize={11}>
|
||||
{networks[networkId].name_label}
|
||||
</Col>
|
||||
<Col mediumSize={1}>
|
||||
<ActionButton btnStyle='default' size='small' handler={this._deleteNetwork} handlerParam={networkId} icon='delete' />
|
||||
</Col>
|
||||
</Row>)}
|
||||
<Row>
|
||||
{showNewNetworkForm
|
||||
? <form id='newNetworkForm' style={NETWORK_FORM_STYLE}>
|
||||
<Col mediumSize={10}>
|
||||
<SelectNetwork
|
||||
autoFocus
|
||||
multi
|
||||
onChange={this.linkState('newNetworks', '*.id')}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
value={newNetworks}
|
||||
/>
|
||||
</Col>
|
||||
<Col mediumSize={2}>
|
||||
<ActionButton form='newNetworkForm' icon='save' btnStyle='primary' handler={this._addNetworks} />
|
||||
</Col>
|
||||
</form>
|
||||
: <Col><ActionButton btnStyle='success' size='small' handler={this._toggleNewNetworks} icon='add' /></Col>
|
||||
}
|
||||
</Row>
|
||||
</Container>
|
||||
}
|
||||
}
|
||||
|
||||
@addSubscriptions({
|
||||
ipPools: subscribeIpPools
|
||||
})
|
||||
@injectIntl
|
||||
export default class Ips extends BaseComponent {
|
||||
_create = () => {
|
||||
const { name, ips: { value: pattern }, networks } = this.refs
|
||||
|
||||
this.setState({ creatingIpPool: true })
|
||||
return createIpPool({
|
||||
ips: parseIpPattern(pattern),
|
||||
name: name.value,
|
||||
networks: map(networks.value, network => network.id)
|
||||
}).then(() => {
|
||||
name.value = this.refs.ips.value = networks.value = ''
|
||||
this.setState({ creatingIpPool: false })
|
||||
})
|
||||
}
|
||||
|
||||
_ipColumns = [
|
||||
{
|
||||
name: _('ipPoolName'),
|
||||
itemRenderer: ipPool => <Text onChange={name => setIpPool(ipPool, { name })} value={ipPool.name} />,
|
||||
sortCriteria: ipPool => ipPool.name
|
||||
},
|
||||
{
|
||||
name: _('ipPoolIps'),
|
||||
itemRenderer: ipPool => <IpsCell ipPool={ipPool} />
|
||||
},
|
||||
{
|
||||
name: _('ipPoolNetworks'),
|
||||
itemRenderer: ipPool => <NetworksCell ipPool={ipPool} />
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
itemRenderer: ipPool => <span className='pull-right'>
|
||||
<ActionButton btnStyle='default' handler={deleteIpPool} handlerParam={ipPool.id} icon='delete' />
|
||||
</span>
|
||||
}
|
||||
]
|
||||
|
||||
render () {
|
||||
if (process.env.XOA_PLAN < 4) {
|
||||
return <Container><Upgrade place='health' available={4} /></Container>
|
||||
}
|
||||
|
||||
const { ipPools, intl } = this.props
|
||||
const { creatingIpPool } = this.state
|
||||
return <div>
|
||||
<Row>
|
||||
<Col size={6}>
|
||||
<form id='newIpPoolForm' className='form-inline'>
|
||||
<SingleLineRow>
|
||||
<Col mediumSize={6}>
|
||||
<input
|
||||
className='form-control'
|
||||
disabled={creatingIpPool}
|
||||
placeholder={intl.formatMessage(messages.ipPoolName)}
|
||||
ref='name'
|
||||
required
|
||||
style={FULL_WIDTH}
|
||||
type='text'
|
||||
/>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<input
|
||||
className='form-control'
|
||||
disabled={creatingIpPool}
|
||||
placeholder={intl.formatMessage(messages.ipPoolIps)}
|
||||
ref='ips'
|
||||
required
|
||||
style={FULL_WIDTH}
|
||||
type='text'
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<br />
|
||||
<SingleLineRow>
|
||||
<Col mediumSize={12}>
|
||||
<SelectNetwork
|
||||
disabled={creatingIpPool}
|
||||
multi
|
||||
ref='networks'
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<br />
|
||||
<SingleLineRow>
|
||||
<Col mediumSize={6}>
|
||||
<ActionButton
|
||||
btnStyle='success'
|
||||
form='newIpPoolForm' icon='add'
|
||||
handler={this._create}
|
||||
>
|
||||
{_('ipsCreate')}
|
||||
</ActionButton>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</form>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr />
|
||||
{isEmpty(ipPools)
|
||||
? <p><em>{_('ipsNoIpPool')}</em></p>
|
||||
: <SortedTable collection={ipPools} columns={this._ipColumns} defaultColumn={0} />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
5
src/xo-app/settings/logs/index.css
Normal file
5
src/xo-app/settings/logs/index.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.widthLimit {
|
||||
overflow-wrap: break-word;
|
||||
width: 100%;
|
||||
max-width: 30em;
|
||||
}
|
||||
89
src/xo-app/settings/logs/index.js
Normal file
89
src/xo-app/settings/logs/index.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import BaseComponent from 'base-component'
|
||||
import Copiable from 'copiable'
|
||||
import find from 'lodash/find'
|
||||
import forEach from 'lodash/forEach'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import size from 'lodash/size'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import { subscribeApiLogs, subscribeUsers, deleteApiLog } from 'xo'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import { alert, confirm } from 'modal'
|
||||
import styles from './index.css'
|
||||
|
||||
@addSubscriptions({
|
||||
logs: subscribeApiLogs,
|
||||
users: subscribeUsers
|
||||
})
|
||||
export default class Logs extends BaseComponent {
|
||||
_showStack = log => alert(_('logStack'), <Copiable tagName='pre'>{`${log.data.method}
|
||||
${JSON.stringify(log.data.params, null, 2)}
|
||||
${log.data.error.stack}`}</Copiable>)
|
||||
_showParams = log => alert(_('logParams'), <Copiable tagName='pre'>{JSON.stringify(log.data.params, null, 2)}</Copiable>)
|
||||
_deleteAllLogs = () =>
|
||||
confirm({
|
||||
title: _('logDeleteAllTitle'),
|
||||
body: _('logDeleteAllMessage')
|
||||
}).then(() =>
|
||||
forEach(this.props.logs, (log, id) => deleteApiLog(id))
|
||||
)
|
||||
render () {
|
||||
const columns = [
|
||||
{
|
||||
name: '',
|
||||
itemRenderer: log => <Tooltip content={_('logDisplayDetails')}><ActionRowButton icon='preview' handler={this._showStack} handlerParam={log} /></Tooltip>
|
||||
},
|
||||
{
|
||||
name: _('logUser'),
|
||||
itemRenderer: log => {
|
||||
if (log.data.userId == null) {
|
||||
return _('unknownUser')
|
||||
}
|
||||
return this.props.users ? find(this.props.users, user => user.id === log.data.userId).email : '...'
|
||||
},
|
||||
sortCriteria: log => log.data.userId
|
||||
},
|
||||
{
|
||||
name: _('logMessage'),
|
||||
itemRenderer: log => <pre className={styles.widthLimit}>{log.data.error && log.data.error.message}</pre>,
|
||||
sortCriteria: log => log.data.error && log.data.error.message
|
||||
},
|
||||
{
|
||||
name: _('logTime'),
|
||||
itemRenderer: log => <span>
|
||||
{log.time && <FormattedDate value={new Date(log.time)} month='long' day='numeric' year='numeric' hour='2-digit' minute='2-digit' second='2-digit' />}
|
||||
</span>,
|
||||
sortCriteria: log => log.time,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
{
|
||||
itemRenderer: log => <ActionRowButton btnStyle='default' handler={deleteApiLog} handlerParam={log.id} icon='delete' />
|
||||
}
|
||||
]
|
||||
const { logs } = this.props
|
||||
if (!logs) {
|
||||
return <h3>{_('loadingLogs')}</h3>
|
||||
}
|
||||
return <div>
|
||||
{size(logs)
|
||||
? <div>
|
||||
<span className='pull-xs-right'>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={this._deleteAllLogs}
|
||||
icon='delete'
|
||||
labelId='logDeleteAll'
|
||||
/>
|
||||
</span>
|
||||
{' '}
|
||||
<SortedTable collection={map(logs, (log, id) => ({ ...log, id }))} columns={columns} defaultColumn={4} />
|
||||
</div>
|
||||
: <p>{_('noLogs')}</p>}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ class AbstractRemote extends Component {
|
||||
} = this.props
|
||||
|
||||
return <tr>
|
||||
<td></td>
|
||||
<td />
|
||||
<td><Text value={remote.name} onChange={this._changeName} placeholder='remote name*' /></td>
|
||||
<td>{this._renderRemoteInfo(remote)}</td>
|
||||
<td>{this._renderAuthInfo(remote)}</td>
|
||||
@@ -257,10 +257,10 @@ export default class Remotes extends Component {
|
||||
<th className='text-info'>Local</th>
|
||||
<th>Name</th>
|
||||
<th>Path</th>
|
||||
<th></th>
|
||||
<th />
|
||||
<th>State</th>
|
||||
<th>Error</th>
|
||||
<th></th>
|
||||
<th />
|
||||
</tr>
|
||||
{map(remotes.file, (remote, key) => <LocalRemote remote={remote} key={key} />)}
|
||||
</tbody>
|
||||
@@ -271,10 +271,10 @@ export default class Remotes extends Component {
|
||||
<th className='text-info'>NFS</th>
|
||||
<th>Name</th>
|
||||
<th>Device</th>
|
||||
<th></th>
|
||||
<th />
|
||||
<th>State</th>
|
||||
<th>Error</th>
|
||||
<th></th>
|
||||
<th />
|
||||
</tr>
|
||||
{map(remotes.nfs, (remote, key) => <NfsRemote remote={remote} key={key} />)}
|
||||
</tbody>
|
||||
@@ -288,7 +288,7 @@ export default class Remotes extends Component {
|
||||
<th>Auth</th>
|
||||
<th>State</th>
|
||||
<th>Error</th>
|
||||
<th></th>
|
||||
<th />
|
||||
</tr>
|
||||
{map(remotes.smb, (remote, key) => <SmbRemote remote={remote} key={key} />)}
|
||||
</tbody>
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import map from 'lodash/map'
|
||||
import Tooltip from 'tooltip'
|
||||
import React, { Component } from 'react'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import { Container } from 'grid'
|
||||
@@ -68,24 +69,28 @@ export default class Servers extends Component {
|
||||
</td>
|
||||
<td>
|
||||
{server.status === 'disconnected'
|
||||
? <ActionRowButton
|
||||
btnStyle='secondary'
|
||||
handler={connectServer}
|
||||
handlerParam={server}
|
||||
icon='connect'
|
||||
style={{
|
||||
marginRight: '0.5em'
|
||||
}}
|
||||
/>
|
||||
: <ActionRowButton
|
||||
btnStyle='warning'
|
||||
handler={disconnectServer}
|
||||
handlerParam={server}
|
||||
icon='disconnect'
|
||||
style={{
|
||||
marginRight: '0.5em'
|
||||
}}
|
||||
/>
|
||||
? <Tooltip content={_('serverConnect')}>
|
||||
<ActionRowButton
|
||||
btnStyle='secondary'
|
||||
handler={connectServer}
|
||||
handlerParam={server}
|
||||
icon='connect'
|
||||
style={{
|
||||
marginRight: '0.5em'
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
: <Tooltip content={_('serverDisconnect')}>
|
||||
<ActionRowButton
|
||||
btnStyle='warning'
|
||||
handler={disconnectServer}
|
||||
handlerParam={server}
|
||||
icon='disconnect'
|
||||
style={{
|
||||
marginRight: '0.5em'
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
<ActionRowButton
|
||||
btnStyle='danger'
|
||||
|
||||
@@ -120,9 +120,11 @@ export default class Sr extends Component {
|
||||
value={sr.name_description}
|
||||
onChange={nameDescription => editSr(sr, { nameDescription })}
|
||||
/>
|
||||
<span className='text-muted'>
|
||||
{' - '}{container.name_label}
|
||||
</span>
|
||||
{container &&
|
||||
<span className='text-muted'>
|
||||
{' - '}{container.name_label}
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
|
||||
@@ -46,7 +46,7 @@ export const TaskItem = connectStore(() => ({
|
||||
{' ' + Math.round(task.progress * 100)}%
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<progress style={TASK_ITEM_STYLE} className='progress' value={task.progress * 100} max='100'></progress>
|
||||
<progress style={TASK_ITEM_STYLE} className='progress' value={task.progress * 100} max='100' />
|
||||
</Col>
|
||||
<Col mediumSize={2}>
|
||||
<ButtonGroup>
|
||||
|
||||
@@ -20,12 +20,9 @@
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.cleanButtonContainer {
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.filesRow {
|
||||
display: flex;
|
||||
.vmContainer {
|
||||
margin-bottom: 1em;
|
||||
border-radius: 0.25rem;
|
||||
border: Solid 0.05em dropzoneColor;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,48 @@
|
||||
import * as FormGrid from 'form-grid'
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import Dropzone from 'react-dropzone'
|
||||
import * as FormGrid from 'form-grid'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import _ from 'intl'
|
||||
import filter from 'lodash/filter'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import propTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { importVms, isSrWritable } from 'xo'
|
||||
import { SizeInput } from 'form'
|
||||
import {
|
||||
createFinder,
|
||||
createGetObject,
|
||||
createGetObjectsOfType,
|
||||
createSelector
|
||||
} from 'selectors'
|
||||
import {
|
||||
connectStore,
|
||||
formatSize,
|
||||
mapPlus,
|
||||
noop
|
||||
} from 'utils'
|
||||
import {
|
||||
SelectNetwork,
|
||||
SelectPool,
|
||||
SelectSr
|
||||
} from 'select-objects'
|
||||
import { formatSize } from 'utils'
|
||||
|
||||
import Page from '../page'
|
||||
import parseOvaFile from './ova'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const FORMAT_TO_HANDLER = {
|
||||
ova: parseOvaFile,
|
||||
xva: noop
|
||||
}
|
||||
|
||||
const HEADER = (
|
||||
<Container>
|
||||
<Row>
|
||||
@@ -30,109 +53,327 @@ const HEADER = (
|
||||
</Container>
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
descriptionLabel: propTypes.string,
|
||||
disks: propTypes.objectOf(
|
||||
propTypes.shape({
|
||||
capacity: propTypes.number.isRequired,
|
||||
descriptionLabel: propTypes.string.isRequired,
|
||||
nameLabel: propTypes.string.isRequired,
|
||||
path: propTypes.string.isRequired
|
||||
})
|
||||
),
|
||||
memory: propTypes.number,
|
||||
nameLabel: propTypes.string,
|
||||
nCpus: propTypes.number,
|
||||
networks: propTypes.array,
|
||||
pool: propTypes.object.isRequired
|
||||
})
|
||||
@connectStore(() => {
|
||||
const getHostMaster = createGetObject(
|
||||
(_, props) => props.pool.master
|
||||
)
|
||||
const getPifs = createGetObjectsOfType('PIF').pick(
|
||||
(state, props) => getHostMaster(state, props).$PIFs
|
||||
)
|
||||
const getDefaultNetworkId = createSelector(
|
||||
createFinder(
|
||||
getPifs,
|
||||
[ pif => pif.management ]
|
||||
),
|
||||
pif => pif.$network
|
||||
)
|
||||
|
||||
return {
|
||||
defaultNetwork: getDefaultNetworkId
|
||||
}
|
||||
}, { withRef: true })
|
||||
class VmData extends Component {
|
||||
get value () {
|
||||
const { props, refs } = this
|
||||
return {
|
||||
descriptionLabel: refs.descriptionLabel.value,
|
||||
disks: map(props.disks, ({ capacity, path, position }, diskId) => ({
|
||||
capacity,
|
||||
descriptionLabel: refs[`disk-description-${diskId}`].value,
|
||||
nameLabel: refs[`disk-name-${diskId}`].value,
|
||||
path,
|
||||
position
|
||||
})),
|
||||
memory: +refs.memory.value,
|
||||
nameLabel: refs.nameLabel.value,
|
||||
networks: map(props.networks, (_, networkId) => refs[`network-${networkId}`].value.id),
|
||||
nCpus: +refs.nCpus.value
|
||||
}
|
||||
}
|
||||
|
||||
_getNetworkPredicate = createSelector(
|
||||
() => this.props.pool.id,
|
||||
id => network => network.$pool === id
|
||||
)
|
||||
|
||||
render () {
|
||||
const {
|
||||
descriptionLabel,
|
||||
defaultNetwork,
|
||||
disks,
|
||||
memory,
|
||||
nameLabel,
|
||||
nCpus,
|
||||
networks
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmNameLabel')}</label>
|
||||
<input className='form-control' ref='nameLabel' defaultValue={nameLabel} type='text' required />
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmNameDescription')}</label>
|
||||
<input className='form-control' ref='descriptionLabel' defaultValue={descriptionLabel} type='text' required />
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('nCpus')}</label>
|
||||
<input className='form-control' ref='nCpus' defaultValue={nCpus} type='number' required />
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmMemory')}</label>
|
||||
<SizeInput defaultValue={memory} ref='memory' required />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
{!isEmpty(disks)
|
||||
? map(disks, (disk, diskId) => (
|
||||
<Row key={diskId}>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>
|
||||
{_('diskInfo', {
|
||||
position: `${disk.position}`,
|
||||
capacity: formatSize(disk.capacity)
|
||||
})}
|
||||
</label>
|
||||
<input className='form-control' ref={`disk-name-${diskId}`} defaultValue={disk.nameLabel} type='text' required />
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('diskDescription')}</label>
|
||||
<input className='form-control' ref={`disk-description-${diskId}`} defaultValue={disk.descriptionLabel} type='text' required />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)) : _('noDisks')
|
||||
}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
{networks.length > 0
|
||||
? map(networks, (name, networkId) => (
|
||||
<div className='form-group' key={networkId}>
|
||||
<label>{_('networkInfo', { name })}</label>
|
||||
<SelectNetwork defaultValue={defaultNetwork} ref={`network-${networkId}`} predicate={this._getNetworkPredicate()} />
|
||||
</div>
|
||||
)) : _('noNetworks')
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const parseFile = async (file, type, func) => {
|
||||
try {
|
||||
return {
|
||||
data: await func(file),
|
||||
file,
|
||||
type
|
||||
}
|
||||
} catch (error) {
|
||||
return { error, file, type }
|
||||
}
|
||||
}
|
||||
|
||||
export default class Import extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state.files = []
|
||||
this.state.vms = []
|
||||
}
|
||||
|
||||
_import = () => importVms(this.state.files, this.refs.selectSr.value.id)
|
||||
_import = () => {
|
||||
const { state } = this
|
||||
return importVms(
|
||||
mapPlus(state.vms, (vm, push, vmIndex) => {
|
||||
if (!vm.error) {
|
||||
const ref = this.refs[`vm-data-${vmIndex}`]
|
||||
push({
|
||||
...vm,
|
||||
data: ref && ref.value
|
||||
})
|
||||
}
|
||||
}),
|
||||
state.sr
|
||||
)
|
||||
}
|
||||
|
||||
_handleDrop = async files => {
|
||||
const vms = await Promise.all(mapPlus(files, (file, push) => {
|
||||
const { name } = file
|
||||
const extIndex = name.lastIndexOf('.')
|
||||
|
||||
let func
|
||||
let type
|
||||
|
||||
if (
|
||||
extIndex >= 0 &&
|
||||
(type = name.substring(extIndex + 1)) &&
|
||||
(func = FORMAT_TO_HANDLER[type])
|
||||
) {
|
||||
push(parseFile(file, type, func))
|
||||
}
|
||||
}))
|
||||
|
||||
_onDrop = files => {
|
||||
this.setState({
|
||||
files: filter(files, file => file.name.endsWith('.xva'))
|
||||
vms: orderBy(vms, vm => [ vm.error != null, vm.type, vm.file.name ])
|
||||
})
|
||||
}
|
||||
|
||||
_onCleanSelectedVms = () => {
|
||||
_handleCleanSelectedVms = () => {
|
||||
this.setState({
|
||||
files: []
|
||||
vms: []
|
||||
})
|
||||
}
|
||||
|
||||
_handleSelectedPool = pool => {
|
||||
const srPredicate = pool !== ''
|
||||
? sr => sr.$pool === pool.id && isSrWritable(sr)
|
||||
: undefined
|
||||
if (pool === '') {
|
||||
this.setState({
|
||||
pool: undefined,
|
||||
sr: undefined,
|
||||
srPredicate: undefined
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
pool,
|
||||
sr: pool.default_SR,
|
||||
srPredicate: sr => sr.$pool === this.state.pool.id && isSrWritable(sr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_handleSelectedSr = sr => {
|
||||
this.setState({
|
||||
srPredicate
|
||||
}, () => { this.refs.selectSr.value = pool.default_SR })
|
||||
sr: sr === '' ? undefined : sr
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { files, srPredicate } = this.state
|
||||
const {
|
||||
pool,
|
||||
sr,
|
||||
srPredicate,
|
||||
vms
|
||||
} = this.state
|
||||
|
||||
return <Page header={HEADER} title='newImport' formatTitle>
|
||||
{process.env.XOA_PLAN > 1
|
||||
? <Container>
|
||||
<form id='import-form'>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('vmImportToPool')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<SelectPool onChange={this._handleSelectedPool} required />
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('vmImportToSr')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<SelectSr
|
||||
disabled={!srPredicate}
|
||||
predicate={srPredicate}
|
||||
ref='selectSr'
|
||||
required
|
||||
/>
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
<Dropzone onDrop={this._onDrop} className={styles.dropzone} activeClassName={styles.activeDropzone}>
|
||||
<div className={styles.dropzoneText}>{_('importVmsList')}</div>
|
||||
</Dropzone>
|
||||
<hr />
|
||||
<h5>{_('vmsToImport')}</h5>
|
||||
{files.length ? (
|
||||
<Row className={styles.filesRow}>
|
||||
<Col mediumSize={10}>
|
||||
<ul className='list-group'>
|
||||
{map(files, (file, key) => (
|
||||
<li key={key} className='list-group-item'>
|
||||
{file.name}
|
||||
<span className='pull-xs-right'>{`(${formatSize(file.size)})`}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Col>
|
||||
<Col mediumSize={2} className={styles.cleanButtonContainer}>
|
||||
<button
|
||||
className='btn btn-secondary'
|
||||
onClick={this._onCleanSelectedVms}
|
||||
type='button'
|
||||
>
|
||||
{_('importVmsCleanList')}
|
||||
</button>
|
||||
</Col>
|
||||
</Row>
|
||||
) : <p>{_('noSelectedVms')}</p>}
|
||||
<hr />
|
||||
<div className='form-group pull-xs-right'>
|
||||
<div className='btn-toolbar'>
|
||||
<div className='btn-group'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={!files.length}
|
||||
form='import-form'
|
||||
handler={this._import}
|
||||
icon='import'
|
||||
redirectOnSuccess='/'
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
</ActionButton>
|
||||
? (
|
||||
<Container>
|
||||
<form id='import-form'>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('vmImportToPool')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<SelectPool value={pool} onChange={this._handleSelectedPool} required />
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('vmImportToSr')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<SelectSr
|
||||
disabled={!sr}
|
||||
onChange={this._handleSelectedSr}
|
||||
predicate={srPredicate}
|
||||
required
|
||||
value={sr}
|
||||
/>
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
{sr && (
|
||||
<div>
|
||||
<Dropzone onDrop={this._handleDrop} className={styles.dropzone} activeClassName={styles.activeDropzone}>
|
||||
<div className={styles.dropzoneText}>{_('importVmsList')}</div>
|
||||
</Dropzone>
|
||||
<hr />
|
||||
<h5>{_('vmsToImport')}</h5>
|
||||
{vms.length > 0
|
||||
? (
|
||||
<div>
|
||||
{map(vms, ({ data, error, file, type }, vmIndex) => (
|
||||
<div key={file.preview} className={styles.vmContainer}>
|
||||
<strong>{file.name}</strong>
|
||||
<span className='pull-xs-right'>
|
||||
<strong>{`(${formatSize(file.size)})`}</strong>
|
||||
</span>
|
||||
{!error
|
||||
? (data &&
|
||||
<div>
|
||||
<hr />
|
||||
<div className='alert alert-info' role='alert'>
|
||||
<strong>{_('vmImportFileType', { type })}</strong> {_('vmImportConfigAlert')}
|
||||
</div>
|
||||
<VmData {...data} ref={`vm-data-${vmIndex}`} pool={pool} />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<hr />
|
||||
<div className='alert alert-danger' role='alert'>
|
||||
<strong>{_('vmImportError')}</strong> {(error && error.message) || _('noVmImportErrorDescription')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : <p>{_('noSelectedVms')}</p>
|
||||
}
|
||||
<hr />
|
||||
<div className='form-group pull-xs-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={!vms.length}
|
||||
className='m-r-1'
|
||||
form='import-form'
|
||||
handler={this._import}
|
||||
icon='import'
|
||||
redirectOnSuccess='/'
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
</ActionButton>
|
||||
<button
|
||||
className='btn btn-secondary'
|
||||
onClick={this._handleCleanSelectedVms}
|
||||
type='button'
|
||||
>
|
||||
{_('importVmsCleanList')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Container>
|
||||
: <Container><Upgrade place='vmImport' available={2} /></Container>
|
||||
)}
|
||||
</form>
|
||||
</Container>
|
||||
) : <Container><Upgrade place='vmImport' available={2} /></Container>
|
||||
}
|
||||
</Page>
|
||||
}
|
||||
|
||||
174
src/xo-app/vm-import/ova/index.js
Normal file
174
src/xo-app/vm-import/ova/index.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import find from 'lodash/find'
|
||||
import forEach from 'lodash/forEach'
|
||||
import tar from 'tar-stream'
|
||||
import xml2js from 'xml2js'
|
||||
import {
|
||||
ensureArray,
|
||||
htmlFileToStream,
|
||||
streamToString
|
||||
} from 'utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// See: http://opennodecloud.com/howto/2013/12/25/howto-ON-ovf-reference.html
|
||||
// See: http://www.dmtf.org/sites/default/files/standards/documents/DSP0243_1.0.0.pdf
|
||||
// See: http://www.dmtf.org/sites/default/files/standards/documents/DSP0243_2.1.0.pdf
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const MEMORY_UNIT_TO_FACTOR = {
|
||||
k: 1024,
|
||||
m: 1048576,
|
||||
g: 1073741824,
|
||||
t: 1099511627776
|
||||
}
|
||||
|
||||
const RESOURCE_TYPE_TO_HANDLER = {
|
||||
// CPU.
|
||||
'3': (data, {
|
||||
'rasd:VirtualQuantity': nCpus
|
||||
}) => {
|
||||
data.nCpus = +nCpus
|
||||
},
|
||||
// RAM.
|
||||
'4': (data, {
|
||||
'rasd:AllocationUnits': unit,
|
||||
'rasd:VirtualQuantity': quantity
|
||||
}) => {
|
||||
data.memory = quantity * allocationUnitsToFactor(unit)
|
||||
},
|
||||
// Network.
|
||||
'10': ({ networks }, {
|
||||
'rasd:AutomaticAllocation': enabled,
|
||||
'rasd:Connection': name
|
||||
}) => {
|
||||
if (enabled) {
|
||||
networks.push(name)
|
||||
}
|
||||
},
|
||||
// Disk.
|
||||
'17': ({ disks }, {
|
||||
'rasd:AddressOnParent': position,
|
||||
'rasd:Description': description = 'No description',
|
||||
'rasd:ElementName': name,
|
||||
'rasd:HostResource': resource
|
||||
}) => {
|
||||
const diskId = resource.match(/^(?:ovf:)?\/disk\/(.+)$/)
|
||||
const disk = diskId && disks[diskId[1]]
|
||||
|
||||
if (disk) {
|
||||
disk.descriptionLabel = description
|
||||
disk.nameLabel = name
|
||||
disk.position = +position
|
||||
} else {
|
||||
// TODO: Log error in U.I.
|
||||
console.error(`No disk found: '${diskId}'.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allocationUnitsToFactor = unit => {
|
||||
const intValue = unit.match(/\^([0-9]+)$/)
|
||||
return intValue != null
|
||||
? Math.pow(2, intValue[1])
|
||||
: MEMORY_UNIT_TO_FACTOR[unit.charAt(0).toLowerCase()]
|
||||
}
|
||||
|
||||
const filterDisks = disks => {
|
||||
for (const diskId in disks) {
|
||||
if (disks[diskId].position == null) {
|
||||
// TODO: Log error in U.I.
|
||||
console.error(`No position specified for '${diskId}'.`)
|
||||
delete disks[diskId]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const parseOvaFile = file => (
|
||||
new Promise((resolve, reject) => {
|
||||
const stream = htmlFileToStream(file)
|
||||
const extract = tar.extract()
|
||||
|
||||
stream.on('error', reject)
|
||||
|
||||
// tar module can work with bad tar files...
|
||||
// So it's necessary to reject at end of stream.
|
||||
extract.on('finish', () => { reject(new Error('No ovf file found.')) })
|
||||
extract.on('error', reject)
|
||||
extract.on('entry', ({ name }, stream, cb) => {
|
||||
// Not a XML file.
|
||||
const extIndex = name.lastIndexOf('.')
|
||||
if (extIndex === -1 || name.substring(extIndex + 1) !== 'ovf') {
|
||||
stream.on('end', cb)
|
||||
stream.resume()
|
||||
return
|
||||
}
|
||||
|
||||
// XML file.
|
||||
streamToString(stream).then(xmlString => {
|
||||
xml2js.parseString(xmlString, {
|
||||
mergeAttrs: true,
|
||||
explicitArray: false
|
||||
}, (err, res) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
Envelope: {
|
||||
DiskSection: { Disk: disks },
|
||||
References: { File: files },
|
||||
VirtualSystem: system
|
||||
}
|
||||
} = res
|
||||
|
||||
const data = {
|
||||
disks: {},
|
||||
networks: []
|
||||
}
|
||||
const hardware = system.VirtualHardwareSection
|
||||
|
||||
// Get VM name/description.
|
||||
data.nameLabel = hardware.System['vssd:VirtualSystemIdentifier']
|
||||
data.descriptionLabel =
|
||||
(system.AnnotationSection && system.AnnotationSection.Annotation) ||
|
||||
(system.OperatingSystemSection && system.OperatingSystemSection.Description)
|
||||
|
||||
// Get disks.
|
||||
forEach(ensureArray(disks), disk => {
|
||||
const file = find(ensureArray(files), file => file['ovf:id'] === disk['ovf:fileRef'])
|
||||
const unit = disk['ovf:capacityAllocationUnits']
|
||||
|
||||
data.disks[disk['ovf:diskId']] = {
|
||||
capacity: disk['ovf:capacity'] * ((unit && allocationUnitsToFactor(unit)) || 1),
|
||||
path: file && file['ovf:href']
|
||||
}
|
||||
})
|
||||
|
||||
// Get hardware info: CPU, RAM, disks, networks...
|
||||
forEach(ensureArray(hardware.Item), item => {
|
||||
const handler = RESOURCE_TYPE_TO_HANDLER[item['rasd:ResourceType']]
|
||||
if (!handler) {
|
||||
return
|
||||
}
|
||||
handler(data, item)
|
||||
})
|
||||
|
||||
// Remove disks which not have a position.
|
||||
// (i.e. no info in hardware.Item section.)
|
||||
filterDisks(data.disks)
|
||||
|
||||
// Done!
|
||||
resolve(data)
|
||||
cb()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
stream.pipe(extract)
|
||||
})
|
||||
)
|
||||
export { parseOvaFile as default }
|
||||
@@ -1,5 +1,6 @@
|
||||
import _ from 'intl'
|
||||
import assign from 'lodash/assign'
|
||||
import BaseComponent from 'base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
@@ -8,12 +9,13 @@ import map from 'lodash/map'
|
||||
import { NavLink, NavTabs } from 'nav'
|
||||
import Page from '../page'
|
||||
import pick from 'lodash/pick'
|
||||
import React, { cloneElement, Component } from 'react'
|
||||
import React, { cloneElement } from 'react'
|
||||
import VmActionBar from './action-bar'
|
||||
import { Select, Text } from 'editable'
|
||||
import {
|
||||
editVm,
|
||||
fetchVmStats,
|
||||
isVmRunning,
|
||||
migrateVm
|
||||
} from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
@@ -37,8 +39,6 @@ import TabSnapshots from './tab-snapshots'
|
||||
import TabLogs from './tab-logs'
|
||||
import TabAdvanced from './tab-advanced'
|
||||
|
||||
const isRunning = vm => vm && vm.power_state === 'Running'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@routes('general', {
|
||||
@@ -113,7 +113,7 @@ const isRunning = vm => vm && vm.power_state === 'Running'
|
||||
}
|
||||
}
|
||||
})
|
||||
export default class Vm extends Component {
|
||||
export default class Vm extends BaseComponent {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
@@ -123,7 +123,7 @@ export default class Vm extends Component {
|
||||
this.cancel()
|
||||
}
|
||||
|
||||
if (!isRunning(vm)) {
|
||||
if (!isVmRunning(vm)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -162,9 +162,9 @@ export default class Vm extends Component {
|
||||
this.context.router.push('/')
|
||||
}
|
||||
|
||||
if (!isRunning(vmCur) && isRunning(vmNext)) {
|
||||
if (!isVmRunning(vmCur) && isVmRunning(vmNext)) {
|
||||
this.loop(vmNext)
|
||||
} else if (isRunning(vmCur) && !isRunning(vmNext)) {
|
||||
} else if (isVmRunning(vmCur) && !isVmRunning(vmNext)) {
|
||||
this.setState({
|
||||
statsOverview: undefined
|
||||
})
|
||||
@@ -243,6 +243,8 @@ export default class Vm extends Component {
|
||||
</Container>
|
||||
}
|
||||
|
||||
_toggleHeader = () => this.setState({ collapsedHeader: !this.state.collapsedHeader })
|
||||
|
||||
render () {
|
||||
const { container, vm } = this.props
|
||||
|
||||
@@ -262,8 +264,8 @@ export default class Vm extends Component {
|
||||
]), pick(this.state, [
|
||||
'statsOverview'
|
||||
]))
|
||||
return <Page header={this.header()} title={`${vm.name_label}${container ? ` (${container.name_label})` : ''}`}>
|
||||
{cloneElement(this.props.children, childProps)}
|
||||
return <Page header={this.header()} collapsedHeader={this.state.collapsedHeader} title={`${vm.name_label}${container ? ` (${container.name_label})` : ''}`}>
|
||||
{cloneElement(this.props.children, { ...childProps, toggleHeader: this._toggleHeader })}
|
||||
</Page>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import invoke from 'invoke'
|
||||
import IsoDevice from 'iso-device'
|
||||
import NoVnc from 'react-novnc'
|
||||
import React from 'react'
|
||||
import { resolveUrl } from 'xo'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Button } from 'react-bootstrap-4/lib'
|
||||
import { resolveUrl, isVmRunning } from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
CpuSparkLines,
|
||||
@@ -17,6 +19,11 @@ import {
|
||||
} from 'xo-sparklines'
|
||||
|
||||
export default class TabConsole extends Component {
|
||||
componentWillReceiveProps (props) {
|
||||
if (isVmRunning(this.props.vm) && !isVmRunning(props.vm)) {
|
||||
this.props.minimalLayout && this._toggleMinimalLayout()
|
||||
}
|
||||
}
|
||||
_sendCtrlAltDel = () => {
|
||||
this.refs.noVnc.sendCtrlAltDel()
|
||||
}
|
||||
@@ -36,21 +43,32 @@ export default class TabConsole extends Component {
|
||||
_getClipboardContent = () =>
|
||||
this.refs.clipboard && this.refs.clipboard.value
|
||||
|
||||
_toggleMinimalLayout = () => {
|
||||
this.props.toggleHeader()
|
||||
this.setState({ minimalLayout: !this.state.minimalLayout })
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
statsOverview,
|
||||
vm
|
||||
} = this.props
|
||||
const {
|
||||
minimalLayout,
|
||||
scale
|
||||
} = this.state
|
||||
|
||||
if (vm.power_state !== 'Running') {
|
||||
if (!isVmRunning(vm)) {
|
||||
return (
|
||||
<p>Console is only available for running VMs.</p>
|
||||
<Container>
|
||||
<p>Console is only available for running VMs.</p>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{statsOverview && <Row className='text-xs-center'>
|
||||
{!minimalLayout && statsOverview && <Row className='text-xs-center'>
|
||||
<Col mediumSize={3}>
|
||||
<p>
|
||||
<Icon icon='cpu' size={2} />
|
||||
@@ -81,10 +99,10 @@ export default class TabConsole extends Component {
|
||||
</Col>
|
||||
</Row>}
|
||||
<Row>
|
||||
<Col mediumSize={5}>
|
||||
<Col mediumSize={3}>
|
||||
<IsoDevice vm={vm} />
|
||||
</Col>
|
||||
<Col mediumSize={5}>
|
||||
<Col mediumSize={3}>
|
||||
<div className='input-group'>
|
||||
<input type='text' className='form-control' ref='clipboard' onChange={this._setRemoteClipboard} />
|
||||
<span className='input-group-btn'>
|
||||
@@ -104,11 +122,34 @@ export default class TabConsole extends Component {
|
||||
<Icon icon='vm-keyboard' /> {_('ctrlAltDelButtonLabel')}
|
||||
</button>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<input
|
||||
className='form-control'
|
||||
max={3}
|
||||
min={0.1}
|
||||
onChange={this.linkState('scale')}
|
||||
step={0.1}
|
||||
type='range'
|
||||
value={scale}
|
||||
/>
|
||||
</Col>
|
||||
<Col mediumSize={1}>
|
||||
<Tooltip content={minimalLayout ? _('showHeaderTooltip') : _('hideHeaderTooltip')}>
|
||||
<Button bsStyle='secondary' onClick={this._toggleMinimalLayout}>
|
||||
<Icon icon={minimalLayout ? 'caret' : 'caret-up'} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='console'>
|
||||
<Col>
|
||||
<NoVnc ref='noVnc' url={resolveUrl(`consoles/${vm.id}`)} onClipboardChange={this._getRemoteClipboard} />
|
||||
<p><em><Icon icon='info' /> {_('tipLabel')} {_('tipConsoleLabel')}</em></p>
|
||||
<NoVnc
|
||||
onClipboardChange={this._getRemoteClipboard}
|
||||
ref='noVnc'
|
||||
scale={scale}
|
||||
url={resolveUrl(`consoles/${vm.id}`)}
|
||||
/>
|
||||
{!minimalLayout && <p><em><Icon icon='info' /> {_('tipLabel')} {_('tipConsoleLabel')}</em></p>}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -24,11 +24,13 @@ import { XoSelect, Size, Text } from 'editable'
|
||||
import {
|
||||
attachDiskToVm,
|
||||
createDisk,
|
||||
connectVbd,
|
||||
deleteVbd,
|
||||
deleteVdi,
|
||||
disconnectVbd,
|
||||
editVdi,
|
||||
isSrWritable,
|
||||
isVmRunning,
|
||||
migrateVdi,
|
||||
setBootableVbd,
|
||||
setVmBootOrder
|
||||
@@ -145,7 +147,7 @@ class AttachDisk extends Component {
|
||||
const { vm, vbds, onClose = noop } = this.props
|
||||
const { vdi } = this.state
|
||||
const { bootable, readOnly } = this.refs
|
||||
const _isFreeForWriting = vdi => some(vdi.$VBDs, id => {
|
||||
const _isFreeForWriting = vdi => vdi.$VBDs.length === 0 || some(vdi.$VBDs, id => {
|
||||
const vbd = vbds[id]
|
||||
return !vbd || !vbd.attached || vbd.read_only
|
||||
})
|
||||
@@ -444,6 +446,14 @@ export default class TabDisks extends Component {
|
||||
{_('vbdStatusDisconnected')}
|
||||
</span>
|
||||
<ButtonGroup className='pull-xs-right'>
|
||||
{isVmRunning(vm) &&
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
icon='connect'
|
||||
handler={connectVbd}
|
||||
handlerParam={vbd}
|
||||
/>
|
||||
}
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
icon='vdi-forget'
|
||||
|
||||
@@ -5,6 +5,7 @@ import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import Tags from 'tags'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addTag, editVm, removeTag } from 'xo'
|
||||
import { BlockLink } from 'link'
|
||||
import { FormattedRelative } from 'react-intl'
|
||||
@@ -79,8 +80,7 @@ export default ({
|
||||
</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
{/* TODO: tooltip and better icon usage */}
|
||||
<BlockLink to={`/vms/${vm.id}/advanced`}><h1><Icon className='text-info' icon={vm.os_version && vm.os_version.distro && osFamily(vm.os_version.distro)} /></h1></BlockLink>
|
||||
<BlockLink to={`/vms/${vm.id}/advanced`}><Tooltip content={vm.os_version ? vm.os_version.name : _('unknownOsName')}><h1><Icon className='text-info' icon={vm.os_version && vm.os_version.distro && osFamily(vm.os_version.distro)} /></h1></Tooltip></BlockLink>
|
||||
</Col>
|
||||
</Row>
|
||||
{!vm.xenTools && vm.power_state === 'Running' &&
|
||||
|
||||
@@ -1,32 +1,68 @@
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import concat from 'lodash/concat'
|
||||
import every from 'lodash/every'
|
||||
import find from 'lodash/find'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import propTypes from 'prop-types'
|
||||
import React, { Component } from 'react'
|
||||
import remove from 'lodash/remove'
|
||||
import TabButton from 'tab-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { isIp, isIpV4 } from 'ip'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { connectStore, noop } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
createFinder,
|
||||
createGetObject,
|
||||
createGetObjectsOfType,
|
||||
createSelector
|
||||
} from 'selectors'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { SelectNetwork } from 'select-objects'
|
||||
import { SelectNetwork, SelectIp } from 'select-objects'
|
||||
import { XoSelect } from 'editable'
|
||||
|
||||
import {
|
||||
connectVif,
|
||||
createVmInterface,
|
||||
deleteVif,
|
||||
disconnectVif
|
||||
disconnectVif,
|
||||
isVmRunning,
|
||||
setVif
|
||||
} from 'xo'
|
||||
|
||||
const IP_COLUMN_STYLE = { maxWidth: '20em' }
|
||||
const TABLE_STYLE = { minWidth: '0' }
|
||||
|
||||
@propTypes({
|
||||
onClose: propTypes.func,
|
||||
vm: propTypes.object.isRequired
|
||||
})
|
||||
@connectStore(() => {
|
||||
const getHostMaster = createGetObject(
|
||||
(_, props) => props.pool && props.pool.master
|
||||
)
|
||||
const getPifs = createGetObjectsOfType('PIF').pick(
|
||||
(state, props) => {
|
||||
const hostMaster = getHostMaster(state, props)
|
||||
return hostMaster && hostMaster.$PIFs
|
||||
}
|
||||
)
|
||||
const getDefaultNetworkId = createSelector(
|
||||
createFinder(
|
||||
getPifs,
|
||||
[ pif => pif.management ]
|
||||
),
|
||||
pif => pif && pif.$network
|
||||
)
|
||||
return {
|
||||
defaultNetworkId: getDefaultNetworkId
|
||||
}
|
||||
})
|
||||
@injectIntl
|
||||
class NewVif extends Component {
|
||||
constructor (props) {
|
||||
@@ -57,7 +93,7 @@ class NewVif extends Component {
|
||||
const formatMessage = this.props.intl.formatMessage
|
||||
return <form id='newVifForm'>
|
||||
<div className='form-group'>
|
||||
<SelectNetwork predicate={this._getNetworkPredicate()} onChange={this._selectNetwork} required />
|
||||
<SelectNetwork defaultValue={this.props.defaultNetworkId} predicate={this._getNetworkPredicate()} onChange={this._selectNetwork} required />
|
||||
</div>
|
||||
<fieldset className='form-inline'>
|
||||
<div className='form-group'>
|
||||
@@ -102,11 +138,64 @@ export default class TabNetwork extends Component {
|
||||
_toggleNewVif = () => this.setState({
|
||||
newVif: !this.state.newVif
|
||||
})
|
||||
_toggleNewIp = vifIndex => {
|
||||
const { showNewIpForm } = this.state
|
||||
this.setState({
|
||||
showNewIpForm: { ...showNewIpForm, [vifIndex]: !(showNewIpForm && showNewIpForm[vifIndex]) }
|
||||
})
|
||||
}
|
||||
|
||||
_saveIp = (vifIndex, ipIndex, newIp) => {
|
||||
if (!isIp(newIp.id)) {
|
||||
throw new Error('Not a valid IP')
|
||||
}
|
||||
const vif = this.props.vifs[vifIndex]
|
||||
const { allowedIpv4Addresses, allowedIpv6Addresses } = vif
|
||||
if (isIpV4(newIp.id)) {
|
||||
allowedIpv4Addresses[ipIndex] = newIp.id
|
||||
} else {
|
||||
allowedIpv6Addresses[ipIndex - allowedIpv4Addresses.length] = newIp.id
|
||||
}
|
||||
setVif(vif, { allowedIpv4Addresses, allowedIpv6Addresses })
|
||||
}
|
||||
_addIp = (vifIndex, ip) => {
|
||||
this._toggleNewIp(vifIndex)
|
||||
if (!isIp(ip.id)) {
|
||||
return
|
||||
}
|
||||
const vif = this.props.vifs[vifIndex]
|
||||
let { allowedIpv4Addresses, allowedIpv6Addresses } = vif
|
||||
if (isIpV4(ip.id)) {
|
||||
allowedIpv4Addresses = [ ...allowedIpv4Addresses, ip.id ]
|
||||
} else {
|
||||
allowedIpv6Addresses = [ ...allowedIpv6Addresses, ip.id ]
|
||||
}
|
||||
setVif(vif, { allowedIpv4Addresses, allowedIpv6Addresses })
|
||||
}
|
||||
_deleteIp = ({ vifIndex, ipIndex }) => {
|
||||
const vif = this.props.vifs[vifIndex]
|
||||
const { allowedIpv4Addresses, allowedIpv6Addresses } = vif
|
||||
if (ipIndex < allowedIpv4Addresses.length) {
|
||||
remove(allowedIpv4Addresses, (_, i) => i === ipIndex)
|
||||
} else {
|
||||
remove(allowedIpv6Addresses, (_, i) => i === ipIndex - allowedIpv4Addresses.length)
|
||||
}
|
||||
setVif(vif, { allowedIpv4Addresses, allowedIpv6Addresses })
|
||||
}
|
||||
|
||||
_getIpPredicate = vifIndex => selectedIp =>
|
||||
every(this._concatIps(this.props.vifs[vifIndex]), vifIp => vifIp !== selectedIp.id)
|
||||
_getIpPoolPredicate = vifNetwork => ipPool =>
|
||||
find(ipPool.networks, network => network === vifNetwork)
|
||||
|
||||
_noIps = vif => isEmpty(vif.allowedIpv4Addresses) && isEmpty(vif.allowedIpv6Addresses)
|
||||
_concatIps = vif => concat(vif.allowedIpv4Addresses, vif.allowedIpv6Addresses)
|
||||
|
||||
render () {
|
||||
const { newVif } = this.state
|
||||
const { newVif, showNewIpForm } = this.state
|
||||
const {
|
||||
networks,
|
||||
pool,
|
||||
vifs,
|
||||
vm
|
||||
} = this.props
|
||||
@@ -124,30 +213,72 @@ export default class TabNetwork extends Component {
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
{newVif && <div><NewVif vm={vm} onClose={this._toggleNewVif} /><hr /></div>}
|
||||
{newVif && <div><NewVif vm={vm} pool={pool} onClose={this._toggleNewVif} /><hr /></div>}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
{!isEmpty(vifs)
|
||||
? <span>
|
||||
<table className='table' style={{ minWidth: '0' }}>
|
||||
<table className='table' style={TABLE_STYLE}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{_('vifDeviceLabel')}</th>
|
||||
<th>{_('vifMacLabel')}</th>
|
||||
<th>{_('vifMtuLabel')}</th>
|
||||
<th>{_('vifNetworkLabel')}</th>
|
||||
<th>{_('vifAllowedIps')}</th>
|
||||
<th>{_('vifStatusLabel')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(vifs, vif =>
|
||||
<tr key={vif.id}>
|
||||
{map(vifs, (vif, vifIndex) => {
|
||||
const lockedNetwork = networks[vif.$network].defaultIsLocked
|
||||
|
||||
return <tr key={vif.id}>
|
||||
<td>VIF #{vif.device}</td>
|
||||
<td><pre>{vif.MAC}</pre></td>
|
||||
<td>{vif.MTU}</td>
|
||||
<td>{networks[vif.$network] && networks[vif.$network].name_label}</td>
|
||||
<td style={IP_COLUMN_STYLE}>
|
||||
<Container>
|
||||
{this._noIps(vif)
|
||||
? <Row>
|
||||
<Col><em>{_('vifNoIps')}</em></Col>
|
||||
</Row>
|
||||
: map(this._concatIps(vif), (ip, ipIndex) => <Row>
|
||||
<Col size={10}>
|
||||
<XoSelect
|
||||
onChange={newIp => this._saveIp(vifIndex, ipIndex, newIp)}
|
||||
predicate={this._getIpPredicate(vifIndex)}
|
||||
value={ip}
|
||||
xoType='ip'
|
||||
>
|
||||
{ip}
|
||||
</XoSelect>
|
||||
</Col>
|
||||
<Col size={1}>
|
||||
<ActionRowButton handler={this._deleteIp} handlerParam={{ vifIndex, ipIndex }} icon='delete' />
|
||||
</Col>
|
||||
</Row>)
|
||||
}
|
||||
<Row>
|
||||
<Col size={10}>
|
||||
{showNewIpForm && showNewIpForm[vifIndex]
|
||||
? <span onBlur={() => this._toggleNewIp(vifIndex)}>
|
||||
<SelectIp
|
||||
autoFocus
|
||||
onChange={ip => this._addIp(vifIndex, ip)}
|
||||
containerPredicate={this._getIpPoolPredicate(vif.$network)}
|
||||
predicate={this._getIpPredicate(vifIndex)}
|
||||
required
|
||||
/>
|
||||
</span>
|
||||
: <ActionButton btnStyle='success' size='small' handler={this._toggleNewIp} handlerParam={vifIndex} icon='add' />}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</td>
|
||||
<td>
|
||||
{vif.attached
|
||||
? <span>
|
||||
@@ -167,11 +298,13 @@ export default class TabNetwork extends Component {
|
||||
{_('vifStatusDisconnected')}
|
||||
</span>
|
||||
<ButtonGroup className='pull-xs-right'>
|
||||
<ActionRowButton
|
||||
icon='connect'
|
||||
handler={connectVif}
|
||||
handlerParam={vif}
|
||||
/>
|
||||
{isVmRunning(vm) &&
|
||||
<ActionRowButton
|
||||
icon='connect'
|
||||
handler={connectVif}
|
||||
handlerParam={vif}
|
||||
/>
|
||||
}
|
||||
<ActionRowButton
|
||||
icon='remove'
|
||||
handler={deleteVif}
|
||||
@@ -180,9 +313,17 @@ export default class TabNetwork extends Component {
|
||||
</ButtonGroup>
|
||||
</span>
|
||||
}
|
||||
{' '}
|
||||
{lockedNetwork && isEmpty(this._concatIps(vif))
|
||||
? <Tooltip content={_('vifLockedNetworkNoIps')}>
|
||||
<Icon icon='error' />
|
||||
</Tooltip>
|
||||
: <Tooltip content={lockedNetwork && _('vifLockedNetwork')}>
|
||||
<Icon icon={lockedNetwork ? 'lock' : 'unlock'} />
|
||||
</Tooltip>}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{vm.addresses && !isEmpty(vm.addresses)
|
||||
|
||||
@@ -15,7 +15,9 @@ import {
|
||||
createGetObjectsOfType
|
||||
} from 'selectors'
|
||||
import {
|
||||
copyVm,
|
||||
deleteVm,
|
||||
exportVm,
|
||||
editVm,
|
||||
revertSnapshot,
|
||||
snapshotVm
|
||||
@@ -72,6 +74,22 @@ export default class TabSnapshot extends Component {
|
||||
</td>
|
||||
<td>
|
||||
<ButtonGroup>
|
||||
<Tooltip content={_('copySnapshot')}>
|
||||
<ActionRowButton
|
||||
btnStyle='primary'
|
||||
handler={copyVm}
|
||||
handlerParam={snapshot}
|
||||
icon='vm-copy'
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content={_('exportSnapshot')}>
|
||||
<ActionRowButton
|
||||
btnStyle='primary'
|
||||
handler={exportVm}
|
||||
handlerParam={snapshot}
|
||||
icon='vm-export'
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content={_('revertSnapshot')}>
|
||||
<ActionRowButton
|
||||
btnStyle='warning'
|
||||
|
||||
@@ -12,7 +12,8 @@ import Tooltip from 'tooltip'
|
||||
import xoaUpdater, { exposeTrial, isTrialRunning } from 'xoa-updater'
|
||||
import { confirm } from 'modal'
|
||||
import { connectStore } from 'utils'
|
||||
import { Container } from 'grid'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { error } from 'notification'
|
||||
import { Password } from 'form'
|
||||
import { serverVersion } from 'xo'
|
||||
@@ -76,6 +77,7 @@ export default class XoaUpdates extends Component {
|
||||
|
||||
const { registration } = this.props
|
||||
const alreadyRegistered = (registration.state === 'registered')
|
||||
|
||||
if (alreadyRegistered) {
|
||||
try {
|
||||
await confirm({
|
||||
@@ -85,6 +87,7 @@ export default class XoaUpdates extends Component {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.setState({ askRegisterAgain: false })
|
||||
return xoaUpdater.register(email.value, password.value, alreadyRegistered)
|
||||
.then(() => { email.value = password.value = '' })
|
||||
}
|
||||
@@ -116,6 +119,7 @@ export default class XoaUpdates extends Component {
|
||||
_trialAvailable = trial => trial.state === 'default' && isTrialRunning(trial.trial)
|
||||
_trialConsumed = trial => trial.state === 'default' && !isTrialRunning(trial.trial) && !exposeTrial(trial.trial)
|
||||
_updaterDown = trial => isEmpty(trial) || trial.state === 'ERROR'
|
||||
_toggleAskRegisterAgain = () => this.setState({ askRegisterAgain: !this.state.askRegisterAgain })
|
||||
|
||||
_startTrial = async () => {
|
||||
try {
|
||||
@@ -132,6 +136,7 @@ export default class XoaUpdates extends Component {
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.setState({ askRegisterAgain: false })
|
||||
serverVersion.then(serverVersion => {
|
||||
this.setState({ serverVersion })
|
||||
})
|
||||
@@ -154,6 +159,8 @@ export default class XoaUpdates extends Component {
|
||||
} = this.props
|
||||
let { configuration } = this.props // Configuration from the store
|
||||
|
||||
const alreadyRegistered = (registration.state === 'registered')
|
||||
|
||||
configuration = assign({}, configuration)
|
||||
const {
|
||||
proxyHost,
|
||||
@@ -173,141 +180,168 @@ export default class XoaUpdates extends Component {
|
||||
<p className='text-danger'>{_('noUpdaterWarning')}</p>
|
||||
</div>
|
||||
: <div>
|
||||
<p>{_('currentVersion')} {`xo-server ${this.state.serverVersion}`} / {`xo-web ${pkg.version}`}</p>
|
||||
<p>
|
||||
<strong>{states[state]}</strong>
|
||||
{' '}
|
||||
<ActionButton
|
||||
btnStyle='info'
|
||||
handler={update}
|
||||
icon='refresh'>
|
||||
{_('update')}
|
||||
</ActionButton>
|
||||
{' '}
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
handler={upgrade}
|
||||
icon='upgrade'>
|
||||
{_('upgrade')}
|
||||
</ActionButton>
|
||||
</p>
|
||||
<div>
|
||||
{map(log, (log, key) => (
|
||||
<p key={key}>
|
||||
<span className={textClasses[log.level]} >{log.date}</span>: <span dangerouslySetInnerHTML={{__html: ansiUp.ansi_to_html(log.message)}} />
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<h2>{_('settings')} {configEdited ? '*' : ''}</h2>
|
||||
<form className='form-inline'>
|
||||
<fieldset>
|
||||
<div className='form-group'>
|
||||
<input
|
||||
className='form-control'
|
||||
placeholder='Host (myproxy.example.org)'
|
||||
type='text'
|
||||
value={configuration.proxyHost}
|
||||
onChange={this._handleProxyHostChange}
|
||||
/>
|
||||
</div>
|
||||
{' '}
|
||||
<div className='form-group'>
|
||||
<input
|
||||
className='form-control'
|
||||
placeholder='Port (3128 ?...)'
|
||||
type='text'
|
||||
value={configuration.proxyPort}
|
||||
onChange={this._handleProxyPortChange}
|
||||
/>
|
||||
</div>
|
||||
{' '}
|
||||
<div className='form-group'>
|
||||
<input
|
||||
className='form-control'
|
||||
placeholder='User name'
|
||||
type='text'
|
||||
value={configuration.proxyUser}
|
||||
onChange={this._handleProxyUserChange}
|
||||
/>
|
||||
</div>
|
||||
{' '}
|
||||
<div className='form-group'>
|
||||
<Password
|
||||
placeholder='password'
|
||||
ref='proxyPassword'
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<br />
|
||||
<fieldset>
|
||||
<ActionButton icon='save' btnStyle='primary' handler={this._configure}>{_('saveResourceSet')}</ActionButton>
|
||||
{' '}
|
||||
<button type='button' className='btn btn-default' onClick={this._handleConfigReset} disabled={!configEdited}>{_('resetResourceSet')}</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
<h2>{_('registration')}</h2>
|
||||
<p>
|
||||
<strong>{registration.state}</strong>
|
||||
{registration.email && <span> to {registration.email}</span>}
|
||||
<span className='text-danger'> {registration.error}</span>
|
||||
</p>
|
||||
<form id='registrationForm' className='form-inline'>
|
||||
<div className='form-group'>
|
||||
<input
|
||||
className='form-control'
|
||||
placeholder='account email'
|
||||
ref='email'
|
||||
required
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
{' '}
|
||||
<div className='form-group'>
|
||||
<Password
|
||||
placeholder='password'
|
||||
ref='password'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{' '}
|
||||
<ActionButton form='registrationForm' icon='success' btnStyle='primary' handler={this._register}>{_('register')}</ActionButton>
|
||||
</form>
|
||||
{+process.env.XOA_PLAN === 1 &&
|
||||
<div>
|
||||
<h2>{_('trial')}</h2>
|
||||
{this._trialAllowed(trial) &&
|
||||
<div>
|
||||
{registration.state !== 'registered' && <p>{_('trialRegistration')}</p>}
|
||||
{registration.state === 'registered' &&
|
||||
<ActionButton btnStyle='success' handler={this._startTrial} icon='trial'>{_('trialStartButton')}</ActionButton>
|
||||
<Row>
|
||||
<Col mediumSize={12}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<UpdateTag /> {states[state]}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<p>{_('currentVersion')} {`xo-server ${this.state.serverVersion}`} / {`xo-web ${pkg.version}`}</p>
|
||||
<ActionButton
|
||||
btnStyle='info'
|
||||
handler={update}
|
||||
icon='refresh'>
|
||||
{_('refresh')}
|
||||
</ActionButton>
|
||||
{' '}
|
||||
<ActionButton
|
||||
btnStyle='success'
|
||||
handler={upgrade}
|
||||
icon='upgrade'>
|
||||
{_('upgrade')}
|
||||
</ActionButton>
|
||||
<hr />
|
||||
<div>
|
||||
{map(log, (log, key) => (
|
||||
<p key={key}>
|
||||
<span className={textClasses[log.level]} >{log.date}</span>: <span dangerouslySetInnerHTML={{__html: ansiUp.ansi_to_html(log.message)}} />
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{_('proxySettings')} {configEdited ? '*' : ''}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<form>
|
||||
<fieldset>
|
||||
<div className='form-group'>
|
||||
<input
|
||||
className='form-control'
|
||||
placeholder='Host (myproxy.example.org)'
|
||||
type='text'
|
||||
value={configuration.proxyHost}
|
||||
onChange={this._handleProxyHostChange}
|
||||
/>
|
||||
</div>
|
||||
{' '}
|
||||
<div className='form-group'>
|
||||
<input
|
||||
className='form-control'
|
||||
placeholder='Port (eg: 3128)'
|
||||
type='text'
|
||||
value={configuration.proxyPort}
|
||||
onChange={this._handleProxyPortChange}
|
||||
/>
|
||||
</div>
|
||||
{' '}
|
||||
<div className='form-group'>
|
||||
<input
|
||||
className='form-control'
|
||||
placeholder='Username'
|
||||
type='text'
|
||||
value={configuration.proxyUser}
|
||||
onChange={this._handleProxyUserChange}
|
||||
/>
|
||||
</div>
|
||||
{' '}
|
||||
<div className='form-group'>
|
||||
<Password
|
||||
placeholder='Password'
|
||||
ref='proxyPassword'
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<br />
|
||||
<fieldset>
|
||||
<ActionButton icon='save' btnStyle='primary' handler={this._configure}>{_('saveResourceSet')}</ActionButton>
|
||||
{' '}
|
||||
<button type='button' className='btn btn-default' onClick={this._handleConfigReset} disabled={!configEdited}>{_('resetResourceSet')}</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{_('registration')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<strong>{registration.state}</strong>
|
||||
{registration.email && <span> to {registration.email}</span>}
|
||||
<span className='text-danger'> {registration.error}</span>
|
||||
{(!alreadyRegistered || this.state.askRegisterAgain)
|
||||
? <form id='registrationForm'>
|
||||
<div className='form-group'>
|
||||
<input
|
||||
className='form-control'
|
||||
placeholder='Your email account'
|
||||
ref='email'
|
||||
required
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
{' '}
|
||||
<div className='form-group'>
|
||||
<Password
|
||||
placeholder='Your password'
|
||||
ref='password'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{' '}
|
||||
<ActionButton form='registrationForm' icon='success' btnStyle='primary' handler={this._register}>{_('register')}</ActionButton>
|
||||
</form>
|
||||
: <ActionButton icon='edit' btnStyle='primary' handler={this._toggleAskRegisterAgain}>{_('editRegistration')}</ActionButton>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{this._trialAvailable(trial) &&
|
||||
<p className='text-success'>{_('trialAvailableUntil', {date: new Date(trial.trial.end)})}</p>
|
||||
}
|
||||
{this._trialConsumed(trial) &&
|
||||
<p>{_('trialConsumed')}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{(process.env.XOA_PLAN > 1 && process.env.XOA_PLAN < 5) &&
|
||||
<div>
|
||||
{trial.state === 'trustedTrial' &&
|
||||
<p>{trial.message}</p>
|
||||
}
|
||||
{trial.state === 'untrustedTrial' &&
|
||||
<p className='text-danger'>{trial.message}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{process.env.XOA_PLAN < 5 &&
|
||||
<div>
|
||||
{this._updaterDown(trial) &&
|
||||
<p className='text-danger'>{_('trialLocked')}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{+process.env.XOA_PLAN === 1 &&
|
||||
<div>
|
||||
<h2>{_('trial')}</h2>
|
||||
{this._trialAllowed(trial) &&
|
||||
<div>
|
||||
{registration.state !== 'registered' && <p>{_('trialRegistration')}</p>}
|
||||
{registration.state === 'registered' &&
|
||||
<ActionButton btnStyle='success' handler={this._startTrial} icon='trial'>{_('trialStartButton')}</ActionButton>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{this._trialAvailable(trial) &&
|
||||
<p className='text-success'>{_('trialAvailableUntil', {date: new Date(trial.trial.end)})}</p>
|
||||
}
|
||||
{this._trialConsumed(trial) &&
|
||||
<p>{_('trialConsumed')}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{(process.env.XOA_PLAN > 1 && process.env.XOA_PLAN < 5) &&
|
||||
<div>
|
||||
{trial.state === 'trustedTrial' &&
|
||||
<p>{trial.message}</p>
|
||||
}
|
||||
{trial.state === 'untrustedTrial' &&
|
||||
<p className='text-danger'>{trial.message}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{process.env.XOA_PLAN < 5 &&
|
||||
<div>
|
||||
{this._updaterDown(trial) &&
|
||||
<p className='text-danger'>{_('trialLocked')}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
}
|
||||
</Container>
|
||||
|
||||
Reference in New Issue
Block a user