Compare commits
30 Commits
xo-web/v5.
...
exposeVMAp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1083aba33c | ||
|
|
d90156ff15 | ||
|
|
45b6e010df | ||
|
|
1fbdb7799e | ||
|
|
3050fd7ce1 | ||
|
|
1988934f8a | ||
|
|
5331ced4fe | ||
|
|
74066f7d44 | ||
|
|
98e3aa89bb | ||
|
|
070bb65740 | ||
|
|
4e51959e3b | ||
|
|
b5ff695c74 | ||
|
|
f879e2ace0 | ||
|
|
9aeabce4d8 | ||
|
|
2f17cd4ba9 | ||
|
|
60e70a08c1 | ||
|
|
6e0ba2bae3 | ||
|
|
feb996890e | ||
|
|
a06bc85142 | ||
|
|
f530aef92b | ||
|
|
2eb7330335 | ||
|
|
89157e7b7e | ||
|
|
3834e2ef91 | ||
|
|
a711231955 | ||
|
|
0a5e301b3e | ||
|
|
c82b9893c5 | ||
|
|
f4dfabc34c | ||
|
|
059521aeda | ||
|
|
debca09e2c | ||
|
|
0699cfc449 |
36
CHANGELOG.md
36
CHANGELOG.md
@@ -1,41 +1,5 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.11.0** (2017-07-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Storage VHD chain health [\#2178](https://github.com/vatesfr/xo-web/issues/2178)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- No web VNC console [\#2258](https://github.com/vatesfr/xo-web/issues/2258)
|
||||
- Patching issues [\#2254](https://github.com/vatesfr/xo-web/issues/2254)
|
||||
- Advanced button in VM creation for self service user [\#2202](https://github.com/vatesfr/xo-web/issues/2202)
|
||||
- Hide "new VM" menu entry if not admin or not self service user [\#2191](https://github.com/vatesfr/xo-web/issues/2191)
|
||||
|
||||
## **5.10.0** (2017-06-30)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Improve backup log display [\#2239](https://github.com/vatesfr/xo-web/issues/2239)
|
||||
- Patch SR detection improvement [\#2215](https://github.com/vatesfr/xo-web/issues/2215)
|
||||
- Less strict coalesce detection [\#2207](https://github.com/vatesfr/xo-web/issues/2207)
|
||||
- IP pool UI improvement [\#2203](https://github.com/vatesfr/xo-web/issues/2203)
|
||||
- Ability to clear "Auto power on" flag for DR-ed VM [\#2097](https://github.com/vatesfr/xo-web/issues/2097)
|
||||
- [Delta backup restoration] Choose SR for each VDIs [\#2070](https://github.com/vatesfr/xo-web/issues/2070)
|
||||
- Ability to forget an host (even if no longer present) [\#1934](https://github.com/vatesfr/xo-web/issues/1934)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Cross pool migrate fail [\#2248](https://github.com/vatesfr/xo-web/issues/2248)
|
||||
- ActionButtons with modals stay in pending state forever [\#2222](https://github.com/vatesfr/xo-web/issues/2222)
|
||||
- Permission issue for a user on self service VMs [\#2212](https://github.com/vatesfr/xo-web/issues/2212)
|
||||
- Self-Service resource loophole [\#2198](https://github.com/vatesfr/xo-web/issues/2198)
|
||||
- Backup log no longer shows the name of destination VM [\#2195](https://github.com/vatesfr/xo-web/issues/2195)
|
||||
- State not restored when exiting modal dialog [\#2194](https://github.com/vatesfr/xo-web/issues/2194)
|
||||
- [Xapi#exportDeltaVm] Cannot read property 'managed' of undefined [\#2189](https://github.com/vatesfr/xo-web/issues/2189)
|
||||
- VNC keyboard layout change [\#404](https://github.com/vatesfr/xo-web/issues/404)
|
||||
|
||||
## **5.9.0** (2017-05-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.11.0",
|
||||
"version": "5.9.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -31,7 +31,6 @@
|
||||
"npm": ">=3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nraynaud/novnc": "^0.6.1-1",
|
||||
"ansi_up": "^1.3.0",
|
||||
"asap": "^2.0.4",
|
||||
"babel-eslint": "^7.0.0",
|
||||
@@ -94,6 +93,7 @@
|
||||
"moment": "^2.13.0",
|
||||
"moment-timezone": "^0.5.4",
|
||||
"notifyjs": "^3.0.0",
|
||||
"novnc-node": "^0.5.3",
|
||||
"promise-toolbox": "^0.9.4",
|
||||
"random-password": "^0.1.2",
|
||||
"react": "^15.4.1",
|
||||
|
||||
@@ -1,48 +1,47 @@
|
||||
import ActionButton from 'action-button'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React, { cloneElement } from 'react'
|
||||
import { noop } from 'lodash'
|
||||
import React from 'react'
|
||||
import { map, noop } from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
import ActionButton from './action-button'
|
||||
import ButtonGroup from './button-group'
|
||||
|
||||
export const Action = ({ display, handler, handlerParam, icon, label, redirectOnSuccess }) =>
|
||||
<ActionButton
|
||||
handler={handler}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
redirectOnSuccess={redirectOnSuccess}
|
||||
size='large'
|
||||
tooltip={display === 'icon' ? label : undefined}
|
||||
>
|
||||
{display === 'both' && label}
|
||||
</ActionButton>
|
||||
|
||||
Action.propTypes = {
|
||||
display: propTypes.oneOf([ 'icon', 'both' ]),
|
||||
handler: propTypes.func.isRequired,
|
||||
icon: propTypes.string.isRequired,
|
||||
label: propTypes.node,
|
||||
redirectOnSuccess: propTypes.string
|
||||
}
|
||||
|
||||
const ActionBar = ({ children, handlerParam = noop, display = 'both' }) =>
|
||||
const ActionBar = ({ actions, param }) => (
|
||||
<ButtonGroup>
|
||||
{React.Children.map(children, (child, key) => {
|
||||
if (!child) {
|
||||
{map(actions, (button, index) => {
|
||||
if (!button) {
|
||||
return
|
||||
}
|
||||
|
||||
const { props } = child
|
||||
return cloneElement(child, {
|
||||
display: props.display || display,
|
||||
handlerParam: props.handlerParam || handlerParam,
|
||||
key
|
||||
})
|
||||
const {
|
||||
handler,
|
||||
handlerParam = param,
|
||||
icon,
|
||||
label,
|
||||
pending,
|
||||
redirectOnSuccess
|
||||
} = button
|
||||
return <ActionButton
|
||||
key={index}
|
||||
handler={handler || noop}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
pending={pending}
|
||||
redirectOnSuccess={redirectOnSuccess}
|
||||
size='large'
|
||||
tooltip={_(label)}
|
||||
/>
|
||||
})}
|
||||
</ButtonGroup>
|
||||
|
||||
)
|
||||
ActionBar.propTypes = {
|
||||
display: propTypes.oneOf([ 'icon', 'both' ]),
|
||||
handlerParam: propTypes.any
|
||||
actions: React.PropTypes.arrayOf(
|
||||
React.PropTypes.shape({
|
||||
label: React.PropTypes.string.isRequired,
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
handler: React.PropTypes.func,
|
||||
redirectOnSuccess: React.PropTypes.string
|
||||
})
|
||||
).isRequired,
|
||||
display: React.PropTypes.oneOf(['icon', 'text', 'both'])
|
||||
}
|
||||
export { ActionBar as default }
|
||||
|
||||
128
src/common/drag-n-drop-order.js
Normal file
128
src/common/drag-n-drop-order.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import HTML5Backend from 'react-dnd-html5-backend'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import { DragDropContext, DragSource, DropTarget } from 'react-dnd'
|
||||
import { Toggle } from 'form'
|
||||
|
||||
const orderItemSource = {
|
||||
beginDrag: props => ({
|
||||
id: props.id,
|
||||
index: props.index
|
||||
})
|
||||
}
|
||||
|
||||
const orderItemTarget = {
|
||||
hover: (props, monitor, component) => {
|
||||
const dragIndex = monitor.getItem().index
|
||||
const hoverIndex = props.index
|
||||
if (dragIndex === hoverIndex) {
|
||||
return
|
||||
}
|
||||
props.move(dragIndex, hoverIndex)
|
||||
monitor.getItem().index = hoverIndex
|
||||
}
|
||||
}
|
||||
|
||||
@DropTarget('orderItem', orderItemTarget, connect => ({
|
||||
connectDropTarget: connect.dropTarget()
|
||||
}))
|
||||
@DragSource('orderItem', orderItemSource, (connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging()
|
||||
}))
|
||||
@propTypes({
|
||||
connectDragSource: propTypes.func.isRequired,
|
||||
connectDropTarget: propTypes.func.isRequired,
|
||||
index: propTypes.number.isRequired,
|
||||
isDragging: propTypes.bool.isRequired,
|
||||
id: propTypes.any.isRequired,
|
||||
item: propTypes.object.isRequired,
|
||||
move: propTypes.func.isRequired
|
||||
})
|
||||
class OrderItem extends Component {
|
||||
_toggle = checked => {
|
||||
const { item } = this.props
|
||||
item.active = checked
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { item, connectDragSource, connectDropTarget, toggle } = this.props
|
||||
return connectDragSource(connectDropTarget(
|
||||
<li className='list-group-item'>
|
||||
<Icon icon='grab' />
|
||||
{' '}
|
||||
<Icon icon='grab' />
|
||||
{' '}
|
||||
{item.text}
|
||||
{toggle && <span className='pull-right'>
|
||||
<Toggle value={item.active} onChange={this._toggle} />
|
||||
</span>}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
onClose: propTypes.func
|
||||
})
|
||||
@DragDropContext(HTML5Backend)
|
||||
export default class DragNDropOrder extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
const { parseOrderParam, parseOrder } = props
|
||||
this.state = parseOrder(parseOrderParam)
|
||||
}
|
||||
|
||||
_moveOrderItem = (dragIndex, hoverIndex) => {
|
||||
const order = this.state.order.slice()
|
||||
const dragItem = order.splice(dragIndex, 1)
|
||||
if (dragItem.length) {
|
||||
order.splice(hoverIndex, 0, dragItem.pop())
|
||||
this.setState({order})
|
||||
}
|
||||
}
|
||||
|
||||
_reset = () => {
|
||||
const { parseOrderParam, parseOrder } = this.props
|
||||
this.state = this.setState(parseOrder(parseOrderParam))
|
||||
}
|
||||
|
||||
_save = () => {
|
||||
const { order, toggleActive } = this.state
|
||||
this.props.setOrder(this.props.parseOrderParam, order, toggleActive)
|
||||
}
|
||||
|
||||
_toggleOnChange = event => this.setState({toggleActive: event})
|
||||
|
||||
render () {
|
||||
const { order, toggleActive } = this.state
|
||||
const { toggleItems } = this.props
|
||||
return <form>
|
||||
{!toggleItems && <Toggle value={toggleActive} onChange={this._toggleOnChange} />}
|
||||
<ul>
|
||||
{map(order, (item, index) => <OrderItem
|
||||
toggle={toggleItems}
|
||||
key={index}
|
||||
index={index}
|
||||
id={item.id}
|
||||
// FIXME missing translation
|
||||
item={item}
|
||||
move={this._moveOrderItem}
|
||||
/>)}
|
||||
</ul>
|
||||
<fieldset className='form-inline'>
|
||||
<span className='pull-right'>
|
||||
<ActionButton icon='save' btnStyle='primary' handler={this._save}>{_('saveBootOption')}</ActionButton>
|
||||
{' '}
|
||||
<ActionButton icon='reset' handler={this._reset}>{_('resetBootOption')}</ActionButton>
|
||||
</span>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
@@ -390,6 +390,7 @@ const MAP_TYPE_SELECT = {
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
labelProp: propTypes.string.isRequired,
|
||||
value: propTypes.oneOfType([
|
||||
propTypes.string,
|
||||
propTypes.object
|
||||
|
||||
@@ -15,7 +15,7 @@ import Select from './select'
|
||||
multi: propTypes.bool,
|
||||
onChange: propTypes.func,
|
||||
options: propTypes.array,
|
||||
placeholder: propTypes.node,
|
||||
placeholder: propTypes.string,
|
||||
predicate: propTypes.func,
|
||||
required: propTypes.bool,
|
||||
value: propTypes.any
|
||||
|
||||
@@ -31,3 +31,8 @@ export const SR = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const vmGroup = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
@@ -48,10 +48,6 @@ const getMessage = (props, messageId, values, render) => {
|
||||
{render}
|
||||
</FormattedMessage>
|
||||
}
|
||||
getMessage.keyValue = (key, value) => getMessage('keyValue', {
|
||||
key: <strong>{key}</strong>,
|
||||
value
|
||||
})
|
||||
|
||||
export { getMessage as default }
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,6 @@ var forEach = require('lodash/forEach')
|
||||
var isString = require('lodash/isString')
|
||||
|
||||
var messages = {
|
||||
keyValue: '{key}: {value}',
|
||||
|
||||
statusConnecting: 'Connecting',
|
||||
statusDisconnected: 'Disconnected',
|
||||
statusLoading: 'Loading…',
|
||||
@@ -40,6 +38,7 @@ var messages = {
|
||||
// ----- Titles -----
|
||||
homePage: 'Home',
|
||||
homeVmPage: 'VMs',
|
||||
homeVmGroupPage: 'VM-Groups',
|
||||
homeHostPage: 'Hosts',
|
||||
homePoolPage: 'Pools',
|
||||
homeTemplatePage: 'Templates',
|
||||
@@ -68,6 +67,7 @@ var messages = {
|
||||
taskMenu: 'Tasks',
|
||||
taskPage: 'Tasks',
|
||||
newVmPage: 'VM',
|
||||
newVmGroupPage: 'VM-Group',
|
||||
newSrPage: 'Storage',
|
||||
newServerPage: 'Server',
|
||||
newImport: 'Import',
|
||||
@@ -103,7 +103,7 @@ var messages = {
|
||||
|
||||
// ----- Home view ------
|
||||
homeFetchingData: 'Fetching data…',
|
||||
homeWelcome: 'Welcome to Xen Orchestra!',
|
||||
homeWelcome: 'Welcome on Xen Orchestra!',
|
||||
homeWelcomeText: 'Add your XenServer hosts or pools',
|
||||
homeConnectServerText: 'Some XenServers have been registered but are not connected',
|
||||
homeHelp: 'Want some help?',
|
||||
@@ -123,6 +123,7 @@ var messages = {
|
||||
homeTypePool: 'Pool',
|
||||
homeTypeHost: 'Host',
|
||||
homeTypeVm: 'VM',
|
||||
homeTypeVmGroup: 'VM group',
|
||||
homeTypeSr: 'SR',
|
||||
homeTypeVmTemplate: 'Template',
|
||||
homeSort: 'Sort',
|
||||
@@ -220,11 +221,6 @@ var messages = {
|
||||
cronPattern: 'Cron Pattern:',
|
||||
backupEditNotFoundTitle: 'Cannot edit backup',
|
||||
backupEditNotFoundMessage: 'Missing required info for edition',
|
||||
successfulJobCall: 'Successful',
|
||||
failedJobCall: 'Failed',
|
||||
jobCallInProgess: 'In progress',
|
||||
jobTransferredDataSize: 'size:',
|
||||
jobTransferredDataSpeed: 'speed:',
|
||||
job: 'Job',
|
||||
jobModalTitle: 'Job {job}',
|
||||
jobId: 'ID',
|
||||
@@ -257,7 +253,7 @@ var messages = {
|
||||
noJobs: 'No jobs found.',
|
||||
noSchedules: 'No schedules found',
|
||||
jobActionPlaceHolder: 'Select a xo-server API command',
|
||||
jobTimeoutPlaceHolder: 'Timeout (number of seconds after which a VM is considered failed)',
|
||||
jobTimeoutPlaceHolder: ' Timeout (number of seconds after which a VM is considered failed)',
|
||||
jobSchedules: 'Schedules',
|
||||
jobScheduleNamePlaceHolder: 'Name of your schedule',
|
||||
jobScheduleJobPlaceHolder: 'Select a Job',
|
||||
@@ -450,13 +446,7 @@ var messages = {
|
||||
convertVmToTemplateLabel: 'Convert to template',
|
||||
vmConsoleLabel: 'Console',
|
||||
|
||||
// ----- SR advanced tab -----
|
||||
|
||||
srUnhealthyVdiNameLabel: 'Name',
|
||||
srUnhealthyVdiSize: 'Size',
|
||||
srUnhealthyVdiDepth: 'Depth',
|
||||
srUnhealthyVdiTitle: 'VDI to coalesce',
|
||||
|
||||
// ----- SR tabs -----
|
||||
// ----- SR actions -----
|
||||
srRescan: 'Rescan all disks',
|
||||
srReconnectAll: 'Connect to all hosts',
|
||||
@@ -548,7 +538,7 @@ var messages = {
|
||||
hostCpusNumber: 'Core (socket)',
|
||||
hostManufacturerinfo: 'Manufacturer info',
|
||||
hostBiosinfo: 'BIOS info',
|
||||
licenseHostSettingsLabel: 'License',
|
||||
licenseHostSettingsLabel: 'Licence',
|
||||
hostLicenseType: 'Type',
|
||||
hostLicenseSocket: 'Socket',
|
||||
hostLicenseExpiry: 'Expiry',
|
||||
@@ -609,7 +599,7 @@ var messages = {
|
||||
patchStatus: 'Status',
|
||||
patchStatusApplied: 'Applied',
|
||||
patchStatusNotApplied: 'Missing patches',
|
||||
patchNothing: 'No patches detected',
|
||||
patchNothing: 'No patch detected',
|
||||
patchReleaseDate: 'Release date',
|
||||
patchGuidance: 'Guidance',
|
||||
patchAction: 'Action',
|
||||
@@ -637,6 +627,7 @@ var messages = {
|
||||
advancedTabName: 'Advanced',
|
||||
networkTabName: 'Network',
|
||||
disksTabName: 'Disk{disks, plural, one {} other {s}}',
|
||||
managementTabName: 'Management',
|
||||
|
||||
powerStateHalted: 'halted',
|
||||
powerStateRunning: 'running',
|
||||
@@ -674,6 +665,7 @@ var messages = {
|
||||
copyToClipboardLabel: 'Copy',
|
||||
ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
|
||||
tipLabel: 'Tip:',
|
||||
tipConsoleLabel: 'Due to a XenServer issue, non-US keyboard layouts aren\'t well supported. Switch your own layout to US to workaround it.',
|
||||
hideHeaderTooltip: 'Hide infos',
|
||||
showHeaderTooltip: 'Show infos',
|
||||
|
||||
@@ -892,6 +884,7 @@ var messages = {
|
||||
|
||||
// ----- New VM -----
|
||||
newVmCreateNewVmOn: 'Create a new VM on {select}',
|
||||
newVmCreateNewVmOn2: 'Create a new VM on {select1} or {select2}',
|
||||
newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
|
||||
newVmInfoPanel: 'Infos',
|
||||
newVmNameLabel: 'Name',
|
||||
@@ -945,6 +938,32 @@ var messages = {
|
||||
newVmHideAdvanced: 'Hide advanced settings',
|
||||
newVmShare: 'Share this VM',
|
||||
|
||||
// ----- VM-Group-----
|
||||
newVmGroupTitle: 'Create a new Vm-Group on ',
|
||||
newVmGroupNameLabel: 'Label',
|
||||
newVmGroupDescriptionLabel: 'Description',
|
||||
newVmGroupReset: 'Reset',
|
||||
newVmGroupCreate: 'Create',
|
||||
newVmGroupInfoPanel: 'Infos',
|
||||
|
||||
// ----- VM-Group item -----
|
||||
powerStateVmGroupRunning: 'All vms are running',
|
||||
powerStateVmGroupHalted: 'All vms are halted',
|
||||
powerStateVmGroupBusy: 'Contains vms halted and vms running',
|
||||
|
||||
// ----- VM-Group management Tab -----
|
||||
'attachVmButton': 'new VM',
|
||||
'vmsBootOrder': 'Boot order',
|
||||
'vmGroupLabel': 'Label',
|
||||
'vmGroupDescription': 'Description',
|
||||
'vmGroupActions': 'Actions',
|
||||
|
||||
// ----- VM-Group general Tab -----
|
||||
'vmGroupCurrentStatus': 'Current status',
|
||||
|
||||
// ----- VM-Group stats Tab -----
|
||||
'vmGroupAllVm': 'All VMs',
|
||||
|
||||
// ----- Self -----
|
||||
resourceSets: 'Resource sets',
|
||||
noResourceSets: 'No resource sets.',
|
||||
@@ -1022,10 +1041,6 @@ var messages = {
|
||||
availableBackupsColumn: 'Available Backups',
|
||||
backupRestoreErrorTitle: 'Missing parameters',
|
||||
backupRestoreErrorMessage: 'Choose a SR and a backup',
|
||||
backupRestoreSelectDefaultSr: 'Select default SR…',
|
||||
backupRestoreChooseSrForEachVdis: 'Choose a SR for each VDI',
|
||||
backupRestoreVdiLabel: 'VDI',
|
||||
backupRestoreSrLabel: 'SR',
|
||||
displayBackup: 'Display backups',
|
||||
importBackupTitle: 'Import VM',
|
||||
importBackupMessage: 'Starting your backup import',
|
||||
@@ -1089,22 +1104,20 @@ var messages = {
|
||||
migrateVmModalTitle: 'Migrate VM',
|
||||
migrateVmSelectHost: 'Select a destination host:',
|
||||
migrateVmSelectMigrationNetwork: 'Select a migration network:',
|
||||
migrateVmSelectSrs: 'For each VDI, select an SR:',
|
||||
migrateVmSelectNetworks: 'For each VIF, select a network:',
|
||||
migrateVmsSelectSr: 'Select a destination SR:',
|
||||
migrateVmsSelectSrIntraPool: 'Select a destination SR for local disks:',
|
||||
migrateVmsSelectNetwork: 'Select a network on which to connect each VIF:',
|
||||
migrateVmsSmartMapping: 'Smart mapping',
|
||||
migrateVmName: 'Name',
|
||||
migrateVmSr: 'SR',
|
||||
migrateVmVif: 'VIF',
|
||||
migrateVmNetwork: 'Network',
|
||||
migrateVmNoTargetHost: 'No target host',
|
||||
migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
|
||||
migrateVmNoDefaultSrError: 'No default SR',
|
||||
migrateVmNotConnectedDefaultSrError: 'Default SR not connected to host',
|
||||
chooseSrForEachVdisModalSelectSr: 'For each VDI, select an SR:',
|
||||
chooseSrForEachVdisModalMainSr: 'Select main SR…',
|
||||
chooseSrForEachVdisModalVdiLabel: 'VDI',
|
||||
chooseSrForEachVdisModalSrLabel: 'SR*',
|
||||
chooseSrForEachVdisModalOptionalEntry: '* optional',
|
||||
deleteVmGroupModalTitle: 'Delete VM-Group',
|
||||
deleteVmGroupModalMessage: 'Are you sure you want to delete this VMGroup ?',
|
||||
deleteVdiModalTitle: 'Delete VDI',
|
||||
deleteVdiModalMessage: 'Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST',
|
||||
revertVmModalTitle: 'Revert your VM',
|
||||
@@ -1173,11 +1186,6 @@ var messages = {
|
||||
detachHostModalMessage: 'Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST.',
|
||||
detachHost: 'Detach',
|
||||
|
||||
// ----- Forget host -----
|
||||
forgetHostModalTitle: 'Forget host',
|
||||
forgetHostModalMessage: 'Are you sure you want to forget {host} from its pool? Be sure this host can\'t be back online, or use detach instead.',
|
||||
forgetHost: 'Forget',
|
||||
|
||||
// ----- Network -----
|
||||
newNetworkCreate: 'Create network',
|
||||
newBondedNetworkCreate: 'Create bonded network',
|
||||
@@ -1247,7 +1255,7 @@ var messages = {
|
||||
refresh: 'Refresh',
|
||||
upgrade: 'Upgrade',
|
||||
noUpdaterCommunity: 'No updater available for Community Edition',
|
||||
considerSubscribe: 'Please consider subscribing and trying it with all the features for free during 15 days on {link}.',
|
||||
considerSubscribe: 'Please consider subscribe and try it with all features for free during 15 days on {link}.',
|
||||
noUpdaterWarning: 'Manual update could break your current installation due to dependencies issues, do it with caution',
|
||||
currentVersion: 'Current version:',
|
||||
register: 'Register',
|
||||
|
||||
@@ -44,21 +44,11 @@ export class BlockLink extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
_addAuxClickListener = ref => {
|
||||
// FIXME: when https://github.com/facebook/react/issues/8529 is fixed,
|
||||
// remove and use onAuxClickCapture.
|
||||
// In Chrome ^55, middle-clicking triggers auxclick event instead of click
|
||||
if (ref !== null) {
|
||||
ref.addEventListener('auxclick', this._onClickCapture)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, tagName = 'div' } = this.props
|
||||
const Component = tagName
|
||||
return (
|
||||
<Component
|
||||
ref={this._addAuxClickListener}
|
||||
style={this._style}
|
||||
onClickCapture={this._onClickCapture}
|
||||
>
|
||||
|
||||
@@ -29,7 +29,7 @@ const modal = (content, onClose) => {
|
||||
buttons: propTypes.arrayOf(propTypes.shape({
|
||||
btnStyle: propTypes.string,
|
||||
icon: propTypes.string,
|
||||
label: propTypes.node.isRequired,
|
||||
label: propTypes.string.isRequired,
|
||||
tooltip: propTypes.node,
|
||||
value: propTypes.any
|
||||
})).isRequired,
|
||||
@@ -58,6 +58,8 @@ class GenericModal extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { Body, Footer, Header, Title } = ReactModal
|
||||
|
||||
const {
|
||||
buttons,
|
||||
icon,
|
||||
@@ -67,33 +69,34 @@ class GenericModal extends Component {
|
||||
const body = _addRef(this.props.children, 'body')
|
||||
|
||||
return <div>
|
||||
<ReactModal.Header closeButton>
|
||||
<ReactModal.Title>
|
||||
<Header closeButton>
|
||||
<Title>
|
||||
{icon
|
||||
? <span><Icon icon={icon} /> {title}</span>
|
||||
: title
|
||||
}
|
||||
</ReactModal.Title>
|
||||
</ReactModal.Header>
|
||||
<ReactModal.Body>
|
||||
</Title>
|
||||
</Header>
|
||||
<Body>
|
||||
{body}
|
||||
</ReactModal.Body>
|
||||
<ReactModal.Footer>
|
||||
</Body>
|
||||
<Footer>
|
||||
{map(buttons, ({
|
||||
label,
|
||||
tooltip,
|
||||
value,
|
||||
icon,
|
||||
...props
|
||||
}, key) => {
|
||||
}) => {
|
||||
const button = <Button
|
||||
onClick={() => this._resolve(value)}
|
||||
key={value}
|
||||
{...props}
|
||||
>
|
||||
{icon !== undefined && <Icon icon={icon} fixedWidth />}
|
||||
{label}
|
||||
</Button>
|
||||
return <span key={key}>
|
||||
return <span>
|
||||
{tooltip !== undefined
|
||||
? <Tooltip content={tooltip}>{button}</Tooltip>
|
||||
: button
|
||||
@@ -106,7 +109,7 @@ class GenericModal extends Component {
|
||||
{_('genericCancel')}
|
||||
</Button>
|
||||
}
|
||||
</ReactModal.Footer>
|
||||
</Footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -206,8 +209,14 @@ export default class Modal extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { showModal } = this.state
|
||||
/* TODO: remove this work-around and use
|
||||
* ReactModal.Body, ReactModal.Header, ...
|
||||
* after this issue has been fixed:
|
||||
* https://phabricator.babeljs.io/T6976
|
||||
*/
|
||||
return (
|
||||
<ReactModal show={this.state.showModal} onHide={this._onHide}>
|
||||
<ReactModal show={showModal} onHide={this._onHide}>
|
||||
{this.state.content}
|
||||
</ReactModal>
|
||||
)
|
||||
|
||||
18
src/common/react-novnc.js
vendored
18
src/common/react-novnc.js
vendored
@@ -1,7 +1,8 @@
|
||||
import React, { Component } from 'react'
|
||||
import RFB from '@nraynaud/novnc/lib/rfb'
|
||||
import { createBackoff } from 'jsonrpc-websocket-client'
|
||||
import { RFB } from 'novnc-node'
|
||||
import {
|
||||
format as formatUrl,
|
||||
parse as parseUrl,
|
||||
resolve as resolveUrl
|
||||
} from 'url'
|
||||
@@ -41,7 +42,7 @@ export default class NoVnc extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (state !== 'disconnected' || this.refs.canvas == null) {
|
||||
if (state !== 'disconnected') {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -90,23 +91,14 @@ export default class NoVnc extends Component {
|
||||
const rfb = this._rfb = new RFB({
|
||||
encrypt: isSecure,
|
||||
target: this.refs.canvas,
|
||||
wsProtocols: [ 'chat' ],
|
||||
onClipboard: onClipboardChange && ((_, text) => {
|
||||
onClipboardChange(text)
|
||||
}),
|
||||
onUpdateState: this._onUpdateState
|
||||
})
|
||||
|
||||
// remove leading slashes from the path
|
||||
//
|
||||
// a leading slassh will be added by noVNC
|
||||
const clippedPath = url.path.replace(/^\/+/, '')
|
||||
|
||||
// a port is required
|
||||
//
|
||||
// if not available from the URL, use the default ones
|
||||
const port = url.port || (isSecure ? 443 : 80)
|
||||
|
||||
rfb.connect(url.hostname, port, null, clippedPath)
|
||||
rfb.connect(formatUrl(url))
|
||||
disableShortcuts()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
omit
|
||||
} from 'lodash'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import propTypes from './prop-types-decorator'
|
||||
|
||||
// do not forward `state` to ActionButton
|
||||
const Button = styled(p => <ActionButton {...omit(p, 'state')} />)`
|
||||
const Button = styled(ActionButton)`
|
||||
background-color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateBg`]};
|
||||
border: 2px solid ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};
|
||||
color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};
|
||||
|
||||
@@ -18,9 +18,7 @@ const TabButton = ({
|
||||
{...props}
|
||||
size='large'
|
||||
style={STYLE}
|
||||
>
|
||||
{labelId !== undefined && <span className='hidden-md-down'>{_(labelId)}</span>}
|
||||
</ActionButton>
|
||||
><span className='hidden-md-down'>{_(labelId)}</span></ActionButton>
|
||||
)
|
||||
export { TabButton as default }
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import mapValues from 'lodash/mapValues'
|
||||
import React from 'react'
|
||||
import ReadableStream from 'readable-stream'
|
||||
import replace from 'lodash/replace'
|
||||
import sample from 'lodash/sample'
|
||||
import startsWith from 'lodash/startsWith'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
@@ -64,12 +63,8 @@ export const addSubscriptions = subscriptions => Component => {
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this._unsubscribes = map(
|
||||
isFunction(subscriptions)
|
||||
? subscriptions(this.props)
|
||||
: subscriptions,
|
||||
(subscribe, prop) =>
|
||||
subscribe(value => this._setState({ [prop]: value }))
|
||||
this._unsubscribes = map(isFunction(subscriptions) ? subscriptions() : subscriptions, (subscribe, prop) =>
|
||||
subscribe(value => this._setState({ [prop]: value }))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -273,11 +268,6 @@ export const formatSize = bytes => humanFormat(bytes, { scale: 'binary', unit: '
|
||||
|
||||
export const formatSizeRaw = bytes => humanFormat.raw(bytes, { scale: 'binary', unit: 'B' })
|
||||
|
||||
export const formatSpeed = (bytes, milliseconds) => humanFormat(
|
||||
bytes * 1e3 / milliseconds,
|
||||
{ scale: 'binary', unit: 'B/s' }
|
||||
)
|
||||
|
||||
export const parseSize = size => {
|
||||
let bytes = humanFormat.parse.raw(size, { scale: 'binary' })
|
||||
if (bytes.unit && bytes.unit !== 'B') {
|
||||
@@ -551,17 +541,3 @@ export const getCoresPerSocketPossibilities = (maxCoresPerSocket, vCPUs) => {
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// Generates a random human-readable string of length `length`
|
||||
// Useful to generate random default names intended for the UI user
|
||||
export const generateReadableRandomString = (() => {
|
||||
const CONSONANTS = 'bdfgklmnprtvz'.split('')
|
||||
const VOWELS = 'aeiou'.split('')
|
||||
return (length = 8) => {
|
||||
const result = new Array(length)
|
||||
for (let i = 0; i < length; ++i) {
|
||||
result[i] = sample((i & 1) === 0 ? VOWELS : CONSONANTS)
|
||||
}
|
||||
return result.join('')
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
find,
|
||||
flatten,
|
||||
floor,
|
||||
forEach,
|
||||
map,
|
||||
max,
|
||||
size,
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
} from 'lodash'
|
||||
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import { computeArraysSum } from '../xo-stats'
|
||||
import { computeArraysSum, computeArraysAvg } from '../xo-stats'
|
||||
import { formatSize } from '../utils'
|
||||
|
||||
import styles from './index.css'
|
||||
@@ -214,6 +215,54 @@ export const PoolCpuLineChart = injectIntl(propTypes({
|
||||
)
|
||||
}))
|
||||
|
||||
export const VmGroupCpuLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstVmData = data[0]
|
||||
const length = getStatsLength(firstVmData.stats.cpus)
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
}
|
||||
|
||||
const series = map(data, ({ vm, stats }) => ({
|
||||
name: vm,
|
||||
data: computeArraysSum(stats.cpus)
|
||||
}))
|
||||
|
||||
if (addSumSeries) {
|
||||
series.push({
|
||||
name: intl.formatMessage(messages.vmGroupAllVm),
|
||||
data: computeArraysSum(map(series, 'data')),
|
||||
className: styles.dashedLine
|
||||
})
|
||||
}
|
||||
|
||||
const nbCpusByVm = map(data, ({ stats }) => stats.cpus.length)
|
||||
|
||||
return (
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
intl,
|
||||
nValues: length,
|
||||
endTimestamp: firstVmData.endTimestamp,
|
||||
interval: firstVmData.interval,
|
||||
valueTransform: value => `${floor(value)}%`
|
||||
}),
|
||||
high: 100 * (addSumSeries ? sum(nbCpusByVm) : max(nbCpusByVm)),
|
||||
...options
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}))
|
||||
|
||||
export const MemoryLineChart = injectIntl(propTypes({
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
@@ -302,6 +351,57 @@ export const PoolMemoryLineChart = injectIntl(propTypes({
|
||||
)
|
||||
}))
|
||||
|
||||
export const VmGroupMemoryLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstVmData = data[0]
|
||||
const {
|
||||
memory,
|
||||
memoryUsed
|
||||
} = firstVmData.stats
|
||||
|
||||
if (!memory || !memoryUsed) {
|
||||
return templateError
|
||||
}
|
||||
|
||||
const series = map(data, ({ vm, stats }) => ({
|
||||
name: vm,
|
||||
data: stats.memoryUsed
|
||||
}))
|
||||
|
||||
if (addSumSeries) {
|
||||
series.push({
|
||||
name: intl.formatMessage(messages.vmGroupAllVm),
|
||||
data: computeArraysSum(map(data, 'stats.memoryUsed')),
|
||||
className: styles.dashedLine
|
||||
})
|
||||
}
|
||||
|
||||
const currentMemoryByHost = map(data, ({ stats }) => stats.memory[stats.memory.length - 1])
|
||||
|
||||
return (
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
intl,
|
||||
nValues: firstVmData.stats.memoryUsed.length,
|
||||
endTimestamp: firstVmData.endTimestamp,
|
||||
interval: firstVmData.interval,
|
||||
valueTransform: formatSize
|
||||
}),
|
||||
high: addSumSeries ? sum(currentMemoryByHost) : max(currentMemoryByHost),
|
||||
...options
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}))
|
||||
|
||||
export const XvdLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
@@ -334,6 +434,60 @@ export const XvdLineChart = injectIntl(propTypes({
|
||||
)
|
||||
}))
|
||||
|
||||
export const VmGroupXvdLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstVmData = data[0]
|
||||
const {
|
||||
memory,
|
||||
memoryUsed
|
||||
} = firstVmData.stats
|
||||
|
||||
if (!memory || !memoryUsed) {
|
||||
return templateError
|
||||
}
|
||||
|
||||
const series = flatten(map(data, ({ stats, vm }) =>
|
||||
map(stats.xvds, (xvd, key) => {
|
||||
return {
|
||||
name: `${vm} (${key})`,
|
||||
data: computeArraysAvg(stats.xvds[key])
|
||||
}
|
||||
})
|
||||
))
|
||||
|
||||
const datas = []
|
||||
forEach(series, ({ data }) => datas.push(data))
|
||||
if (addSumSeries) {
|
||||
series.push({
|
||||
name: intl.formatMessage(messages.vmGroupAllVm),
|
||||
data: computeArraysSum(datas),
|
||||
className: styles.dashedLine
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
intl,
|
||||
nValues: firstVmData.stats.xvds.r.length,
|
||||
endTimestamp: data.endTimestamp,
|
||||
interval: data.interval,
|
||||
valueTransform: formatSize
|
||||
}),
|
||||
...options
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}))
|
||||
|
||||
export const VifLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
@@ -443,6 +597,48 @@ export const PoolPifLineChart = injectIntl(propTypes({
|
||||
)
|
||||
}))
|
||||
|
||||
export const VmGroupVifLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstVmData = data[0]
|
||||
const length = firstVmData.stats && getStatsLength(firstVmData.stats.vifs.rx)
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
}
|
||||
const series = addSumSeries
|
||||
? map(ios, io => ({
|
||||
name: `${intl.formatMessage(messages.vmGroupAllVm)} (${io})`,
|
||||
data: computeArraysSum(map(data, ({ stats }) => computeArraysSum(stats.vifs[io])))
|
||||
}))
|
||||
: flatten(map(data, ({ stats, vm }) =>
|
||||
map(ios, io => ({
|
||||
name: `${vm} (${io})`,
|
||||
data: computeArraysSum(stats.vifs[io])
|
||||
}))
|
||||
))
|
||||
return (
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
intl,
|
||||
nValues: length,
|
||||
endTimestamp: firstVmData.endTimestamp,
|
||||
interval: firstVmData.interval,
|
||||
valueTransform: formatSize
|
||||
}),
|
||||
...options
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}))
|
||||
|
||||
export const LoadLineChart = injectIntl(propTypes({
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import Collapse from 'collapse'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import { every, forEach, map } from 'lodash'
|
||||
|
||||
import _ from '../../intl'
|
||||
import propTypes from '../../prop-types-decorator'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
import { createSelector } from '../../selectors'
|
||||
import { SelectSr } from '../../select-objects'
|
||||
import { isSrWritable } from 'xo'
|
||||
import {
|
||||
Container,
|
||||
Col
|
||||
} from 'grid'
|
||||
|
||||
// Can 2 SRs on the same pool have 2 VDIs used by the same VM
|
||||
const areSrsCompatible = (sr1, sr2) =>
|
||||
sr1.shared || sr2.shared || sr1.$container === sr2.$container
|
||||
|
||||
const Collapsible = ({collapsible, children, ...props}) => collapsible
|
||||
? <Collapse {...props}>{children}</Collapse>
|
||||
: <div>
|
||||
<span>{props.buttonText}</span>
|
||||
<br />
|
||||
{children}
|
||||
</div>
|
||||
|
||||
Collapsible.propTypes = {
|
||||
collapsible: propTypes.bool.isRequired,
|
||||
children: propTypes.node.isRequired
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
vdis: propTypes.array.isRequired,
|
||||
predicate: propTypes.func
|
||||
})
|
||||
export default class ChooseSrForEachVdisModal extends Component {
|
||||
state = {
|
||||
mapVdisSrs: {}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
if (
|
||||
this.props.predicate !== undefined &&
|
||||
newProps.predicate !== this.props.predicate
|
||||
) {
|
||||
this.state = {
|
||||
mainSr: undefined,
|
||||
mapVdisSrs: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onChange = props => {
|
||||
this.setState(props)
|
||||
this.props.onChange(props)
|
||||
}
|
||||
|
||||
_onChangeMainSr = newSr => {
|
||||
const oldSr = this.state.mainSr
|
||||
|
||||
if (oldSr == null || newSr == null || oldSr.$pool !== newSr.$pool) {
|
||||
this.setState({
|
||||
mapVdisSrs: {}
|
||||
})
|
||||
} else if (!newSr.shared) {
|
||||
const mapVdisSrs = {...this.state.mapVdisSrs}
|
||||
forEach(mapVdisSrs, (sr, vdi) => {
|
||||
if (sr != null && newSr !== sr && sr.$container !== newSr.$container && !sr.shared) {
|
||||
delete mapVdisSrs[vdi]
|
||||
}
|
||||
})
|
||||
this._onChange({mapVdisSrs})
|
||||
}
|
||||
|
||||
this._onChange({
|
||||
mainSr: newSr
|
||||
})
|
||||
}
|
||||
|
||||
_getSrPredicate = createSelector(
|
||||
() => this.state.mainSr,
|
||||
() => this.state.mapVdisSrs,
|
||||
(mainSr, mapVdisSrs) => sr =>
|
||||
isSrWritable(sr) &&
|
||||
mainSr.$pool === sr.$pool &&
|
||||
areSrsCompatible(mainSr, sr) &&
|
||||
every(mapVdisSrs, selectedSr => selectedSr == null || areSrsCompatible(selectedSr, sr))
|
||||
)
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const { vdis } = props
|
||||
const {
|
||||
mainSr,
|
||||
mapVdisSrs
|
||||
} = state
|
||||
|
||||
const srPredicate = props.predicate || this._getSrPredicate()
|
||||
|
||||
return <div>
|
||||
<SelectSr
|
||||
onChange={mainSr => props.predicate !== undefined
|
||||
? this._onChange({mainSr})
|
||||
: this._onChangeMainSr(mainSr)
|
||||
}
|
||||
predicate={props.predicate || isSrWritable}
|
||||
placeholder={_('chooseSrForEachVdisModalMainSr')}
|
||||
value={mainSr}
|
||||
/>
|
||||
<br />
|
||||
{vdis != null && mainSr != null &&
|
||||
<Collapsible collapsible={vdis.length >= 3} buttonText={_('chooseSrForEachVdisModalSelectSr')}>
|
||||
<br />
|
||||
<Container>
|
||||
<SingleLineRow>
|
||||
<Col size={6}><strong>{_('chooseSrForEachVdisModalVdiLabel')}</strong></Col>
|
||||
<Col size={6}><strong>{_('chooseSrForEachVdisModalSrLabel')}</strong></Col>
|
||||
</SingleLineRow>
|
||||
{map(vdis, vdi =>
|
||||
<SingleLineRow key={vdi.uuid}>
|
||||
<Col size={6}>{ vdi.name_label || vdi.name }</Col>
|
||||
<Col size={6}>
|
||||
<SelectSr
|
||||
onChange={sr => this._onChange({ mapVdisSrs: { ...mapVdisSrs, [vdi.uuid]: sr } })}
|
||||
value={mapVdisSrs[vdi.uuid]}
|
||||
predicate={srPredicate}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
)}
|
||||
<i>{_('chooseSrForEachVdisModalOptionalEntry')}</i>
|
||||
</Container>
|
||||
</Collapsible>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export const XEN_VIDEORAM_VALUES = [1, 2, 4, 8, 16]
|
||||
// ===================================================================
|
||||
|
||||
export const isSrWritable = sr => sr && sr.content_type !== 'iso' && sr.size > 0
|
||||
export const isSrShared = sr => sr && sr.shared
|
||||
export const isSrShared = sr => sr && sr.$PBDs.length > 1
|
||||
export const isVmRunning = vm => vm && vm.power_state === 'Running'
|
||||
|
||||
// ===================================================================
|
||||
@@ -275,6 +275,8 @@ export const subscribeIpPools = createSubscription(() => _call('ipPool.getAll'))
|
||||
|
||||
export const subscribeResourceCatalog = createSubscription(() => _call('cloud.getResourceCatalog'))
|
||||
|
||||
export const subscribeVmGroups = createSubscription(() => _call('vmGroup.get'))
|
||||
|
||||
const xosanSubscriptions = {}
|
||||
export const subscribeIsInstallingXosan = (pool, cb) => {
|
||||
const poolId = resolveId(pool)
|
||||
@@ -308,18 +310,6 @@ subscribeHostMissingPatches.forceRefresh = host => {
|
||||
}
|
||||
}
|
||||
|
||||
const unhealthyVdiChainsLengthSubscriptionsBySr = {}
|
||||
export const createSrUnhealthyVdiChainsLengthSubscription = sr => {
|
||||
sr = resolveId(sr)
|
||||
let subscription = unhealthyVdiChainsLengthSubscriptionsBySr[sr]
|
||||
if (subscription === undefined) {
|
||||
subscription = unhealthyVdiChainsLengthSubscriptionsBySr[sr] = createSubscription(
|
||||
() => _call('sr.getUnhealthyVdiChainsLength', { sr })
|
||||
)
|
||||
}
|
||||
return subscription
|
||||
}
|
||||
|
||||
// System ============================================================
|
||||
|
||||
export const apiMethods = _call('system.getMethodsInfo')
|
||||
@@ -427,16 +417,6 @@ export const detachHost = host => (
|
||||
)
|
||||
)
|
||||
|
||||
export const forgetHost = host => (
|
||||
confirm({
|
||||
icon: 'host-forget',
|
||||
title: _('forgetHostModalTitle'),
|
||||
body: _('forgetHostModalMessage', { host: <strong>{host.name_label}</strong> })
|
||||
}).then(
|
||||
() => _call('host.forget', { host: resolveId(host) })
|
||||
)
|
||||
)
|
||||
|
||||
export const setDefaultSr = sr => (
|
||||
_call('pool.setDefaultSr', { sr: resolveId(sr) })
|
||||
)
|
||||
@@ -974,13 +954,8 @@ export const importBackup = ({ remote, file, sr }) => (
|
||||
_call('vm.importBackup', resolveIds({ remote, file, sr }))
|
||||
)
|
||||
|
||||
export const importDeltaBackup = ({ remote, file, sr, mapVdisSrs }) => (
|
||||
_call('vm.importDeltaBackup', resolveIds({
|
||||
remote,
|
||||
filePath: file,
|
||||
sr,
|
||||
mapVdisSrs: resolveIds(mapVdisSrs)
|
||||
}))
|
||||
export const importDeltaBackup = ({ remote, file, sr }) => (
|
||||
_call('vm.importDeltaBackup', resolveIds({ remote, filePath: file, sr }))
|
||||
)
|
||||
|
||||
import RevertSnapshotModalBody from './revert-snapshot-modal' // eslint-disable-line import/first
|
||||
@@ -1062,6 +1037,10 @@ export const attachDiskToVm = (vdi, vm, { bootable, mode, position }) => (
|
||||
})
|
||||
)
|
||||
|
||||
export const removeAppliance = vm => {
|
||||
_call('vm.removeAppliance', { id: resolveId(vm) })
|
||||
}
|
||||
|
||||
// DISK ---------------------------------------------------------------
|
||||
|
||||
export const createDisk = (name, size, sr) => (
|
||||
@@ -1978,3 +1957,33 @@ export const downloadAndInstallXosanPack = pool =>
|
||||
)
|
||||
|
||||
export const registerXosan = namespace => _call('cloud.registerResource', { namespace: 'xosan' })
|
||||
|
||||
// VM-Group ----------------------------------------------------------------------
|
||||
|
||||
export const startVmGroup = vmGroup => {
|
||||
_call('vmGroup.start', { id: resolveId(vmGroup) })
|
||||
}
|
||||
export const shutdownVmGroup = vmGroup => _call('vmGroup.shutdown', { id: resolveId(vmGroup) })
|
||||
export const rebootVmGroup = async vmGroup => {
|
||||
await shutdownVmGroup(vmGroup)
|
||||
await startVmGroup(vmGroup)
|
||||
}
|
||||
export const editVmGroup = (vmGroup, props) => _call('vmGroup.set', { id: resolveId(vmGroup), ...props })
|
||||
export const deleteVmGroup = (vmGroup, vms) =>
|
||||
confirm({
|
||||
title: _('deleteVmGroupModalTitle'),
|
||||
body: _('deleteVmGroupModalMessage')
|
||||
}).then(() => {
|
||||
forEach(vms, vm => removeAppliance(vm))
|
||||
_call('vmGroup.destroy', { id: resolveId(vmGroup) })
|
||||
}, Promise.reject())
|
||||
export const createVmGroup = ({ pool, name_label, name_description }) => _call('vmGroup.create', { id: resolveId(pool), name_label, name_description })
|
||||
export const startVmGroups = (vmGroupIds) => {
|
||||
forEach(vmGroupIds, id => startVmGroup({id: id}))
|
||||
}
|
||||
export const shutdownVmGroups = (vmGroupIds) => {
|
||||
forEach(vmGroupIds, id => shutdownVmGroup({id: id}))
|
||||
}
|
||||
export const rebootVmGroups = (vmGroupIds) => {
|
||||
forEach(vmGroupIds, id => rebootVmGroup({id: id}))
|
||||
}
|
||||
|
||||
@@ -93,14 +93,15 @@ export default class InstallXosanPackModal extends Component {
|
||||
</div>
|
||||
</div>
|
||||
: <div>
|
||||
{_('xosanNoPackFound')}
|
||||
<br />
|
||||
{_('xosanPackRequirements')}
|
||||
<ul>
|
||||
{map(this._getXosanPacks(), ({ name, requirements }, key) => <li key={key}>
|
||||
{_.keyValue(name, requirements && requirements.xenserver ? requirements.xenserver : '/')}
|
||||
</li>)}
|
||||
</ul>
|
||||
<p>{_('xosanNoPackFound')}</p>
|
||||
<p>
|
||||
{_('xosanPackRequirements')}
|
||||
<ul>
|
||||
{map(this._getXosanPacks(), ({ name, requirements }) => <li>
|
||||
{name}: <strong>{requirements && requirements.xenserver ? requirements.xenserver : '/'}</strong>
|
||||
</li>)}
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -3,23 +3,23 @@ import every from 'lodash/every'
|
||||
import forEach from 'lodash/forEach'
|
||||
import find from 'lodash/find'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import React from 'react'
|
||||
import store from 'store'
|
||||
|
||||
import _ from '../../intl'
|
||||
import ChooseSrForEachVdisModal from '../choose-sr-for-each-vdis-modal'
|
||||
import invoke from '../../invoke'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
import { Col } from '../../grid'
|
||||
import { getDefaultNetworkForVif } from '../utils'
|
||||
import {
|
||||
SelectHost,
|
||||
SelectNetwork
|
||||
SelectNetwork,
|
||||
SelectSr
|
||||
} from '../../select-objects'
|
||||
import {
|
||||
connectStore,
|
||||
mapPlus,
|
||||
resolveIds
|
||||
mapPlus
|
||||
} from '../../utils'
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
@@ -138,8 +138,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
get value () {
|
||||
return {
|
||||
targetHost: this.state.host && this.state.host.id,
|
||||
sr: this.state.mainSr && this.state.mainSr.id,
|
||||
mapVdisSrs: resolveIds(this.state.mapVdisSrs),
|
||||
mapVdisSrs: this.state.mapVdisSrs,
|
||||
mapVifsNetworks: this.state.mapVifsNetworks,
|
||||
migrationNetwork: this.state.migrationNetworkId
|
||||
}
|
||||
@@ -159,10 +158,11 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
return
|
||||
}
|
||||
|
||||
const { vbds, vm } = this.props
|
||||
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) {
|
||||
@@ -181,6 +181,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
doNotMigrateVdis,
|
||||
host,
|
||||
intraPool,
|
||||
mapVdisSrs: doNotMigrateVdis ? undefined : mapValues(vdis, vdi => defaultSr),
|
||||
mapVifsNetworks: undefined,
|
||||
migrationNetwork: undefined
|
||||
})
|
||||
@@ -211,6 +212,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
doNotMigrateVdis: false,
|
||||
host,
|
||||
intraPool,
|
||||
mapVdisSrs: mapValues(vdis, vdi => defaultSr),
|
||||
mapVifsNetworks: defaultNetworksForVif,
|
||||
migrationNetworkId: defaultMigrationNetworkId
|
||||
})
|
||||
@@ -224,6 +226,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
doNotMigrateVdis,
|
||||
host,
|
||||
intraPool,
|
||||
mapVdisSrs,
|
||||
mapVifsNetworks,
|
||||
migrationNetworkId
|
||||
} = this.state
|
||||
@@ -242,14 +245,25 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
</div>
|
||||
{host && !doNotMigrateVdis && <div className={styles.groupBlock}>
|
||||
<SingleLineRow>
|
||||
<Col size={12}>
|
||||
<ChooseSrForEachVdisModal
|
||||
onChange={props => this.setState(props)}
|
||||
predicate={this._getSrPredicate()}
|
||||
vdis={vdis}
|
||||
/>
|
||||
</Col>
|
||||
<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 &&
|
||||
|
||||
@@ -12,10 +12,8 @@ import some from 'lodash/some'
|
||||
import store from 'store'
|
||||
|
||||
import _ from '../../intl'
|
||||
import Icon from 'icon'
|
||||
import invoke from '../../invoke'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
import Tooltip from '../../tooltip'
|
||||
import { Col } from '../../grid'
|
||||
import { getDefaultNetworkForVif } from '../utils'
|
||||
import {
|
||||
@@ -219,7 +217,6 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
const { pools, pifs } = this.props
|
||||
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
|
||||
const defaultSrId = pools[host.$pool].default_SR
|
||||
const defaultSrConnectedToHost = some(host.$PBDs, pbd => this._getObject(pbd).SR === defaultSrId)
|
||||
const doNotMigrateVmVdis = {}
|
||||
const doNotMigrateVdi = {}
|
||||
forEach(this.props.vbdsByVm, (vbds, vm) => {
|
||||
@@ -237,8 +234,6 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
})
|
||||
const noVdisMigration = every(doNotMigrateVmVdis)
|
||||
this.setState({
|
||||
defaultSrConnectedToHost,
|
||||
defaultSrId,
|
||||
host,
|
||||
intraPool: every(this.props.vms, vm => vm.$pool === host.$pool),
|
||||
doNotMigrateVdi,
|
||||
@@ -247,7 +242,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
networkId: defaultMigrationNetworkId,
|
||||
noVdisMigration,
|
||||
smartVifMapping: true,
|
||||
srId: defaultSrConnectedToHost ? defaultSrId : undefined
|
||||
srId: defaultSrId
|
||||
})
|
||||
}
|
||||
_selectMigrationNetwork = migrationNetwork => this.setState({ migrationNetworkId: migrationNetwork.id })
|
||||
@@ -257,8 +252,6 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
|
||||
render () {
|
||||
const {
|
||||
defaultSrConnectedToHost,
|
||||
defaultSrId,
|
||||
host,
|
||||
intraPool,
|
||||
migrationNetworkId,
|
||||
@@ -297,24 +290,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
{host && (!intraPool || !noVdisMigration) &&
|
||||
<div key='sr' style={LINE_STYLE}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>
|
||||
{!intraPool ? _('migrateVmsSelectSr') : _('migrateVmsSelectSrIntraPool')}
|
||||
{' '}
|
||||
{(defaultSrId === undefined || !defaultSrConnectedToHost) &&
|
||||
<Tooltip
|
||||
content={defaultSrId !== undefined
|
||||
? _('migrateVmNotConnectedDefaultSrError')
|
||||
: _('migrateVmNoDefaultSrError')
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
icon={defaultSrId !== undefined ? 'alarm' : 'info'}
|
||||
className={defaultSrId !== undefined ? 'text-warning' : 'text-info'}
|
||||
size='lg'
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
</Col>
|
||||
<Col size={6}>{!intraPool ? _('migrateVmsSelectSr') : _('migrateVmsSelectSrIntraPool')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectSr
|
||||
onChange={this._selectSr}
|
||||
|
||||
@@ -79,14 +79,6 @@
|
||||
@extend .fa;
|
||||
@extend .fa-ellipsis-v;
|
||||
},
|
||||
&-previous {
|
||||
@extend .fa;
|
||||
@extend .fa-chevron-left;
|
||||
},
|
||||
&-next {
|
||||
@extend .fa;
|
||||
@extend .fa-chevron-right;
|
||||
},
|
||||
&-caret {
|
||||
@extend .fa;
|
||||
@extend .fa-caret-down;
|
||||
@@ -460,10 +452,6 @@
|
||||
@extend .fa-server;
|
||||
@extend .text-warning;
|
||||
}
|
||||
&-forget {
|
||||
@extend .fa;
|
||||
@extend .fa-ban;
|
||||
}
|
||||
&-working {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
|
||||
@@ -563,133 +563,131 @@ export default class New extends Component {
|
||||
|
||||
return (
|
||||
<Upgrade place='newBackup' required={2}>
|
||||
<form id='form-new-vm-backup'>
|
||||
<Wizard>
|
||||
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<Wizard><form id='form-new-vm-backup'>
|
||||
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<fieldset className='form-group'>
|
||||
<label>{_('backupOwner')}</label>
|
||||
<SelectSubject
|
||||
onChange={this.linkState('job.userId', 'id')}
|
||||
predicate={this._subjectPredicate}
|
||||
required
|
||||
value={this._getValue('job', 'userId', this.props.currentUser.id)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset className='form-group'>
|
||||
<label>{_('jobTimeoutPlaceHolder')}</label>
|
||||
<TimeoutInput
|
||||
className='form-control'
|
||||
onChange={this.linkState('job.timeout')}
|
||||
value={this._getValue('job', 'timeout')}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset className='form-group'>
|
||||
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
|
||||
<select
|
||||
className='form-control'
|
||||
id='selectBackup'
|
||||
onChange={this.linkState('job.method')}
|
||||
required
|
||||
value={method}
|
||||
>
|
||||
{_('noSelectedValue', message => <option value=''>{message}</option>)}
|
||||
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
|
||||
_(info.label, message => <option key={key} value={key}>{message}</option>)
|
||||
)}
|
||||
</select>
|
||||
</fieldset>
|
||||
{(method === 'vm.rollingDeltaBackup' || method === 'vm.deltaCopy') && <div className='alert alert-warning' role='alert'>
|
||||
<Icon icon='error' /> {_('backupVersionWarning')}
|
||||
</div>}
|
||||
{backupInfo && <div>
|
||||
<GenericInput
|
||||
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
|
||||
required
|
||||
schema={backupInfo.schema}
|
||||
uiSchema={backupInfo.uiSchema}
|
||||
onChange={this.linkState('mainParams')}
|
||||
value={this._getMainParams()}
|
||||
/>
|
||||
<fieldset className='form-group'>
|
||||
<label>{_('backupOwner')}</label>
|
||||
<SelectSubject
|
||||
onChange={this.linkState('job.userId', 'id')}
|
||||
predicate={this._subjectPredicate}
|
||||
required
|
||||
value={this._getValue('job', 'userId', this.props.currentUser.id)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset className='form-group'>
|
||||
<label>{_('jobTimeoutPlaceHolder')}</label>
|
||||
<TimeoutInput
|
||||
className='form-control'
|
||||
onChange={this.linkState('job.timeout')}
|
||||
value={this._getValue('job', 'timeout')}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset className='form-group'>
|
||||
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
|
||||
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
|
||||
<select
|
||||
className='form-control'
|
||||
id='selectBackup'
|
||||
onChange={this.linkState('job.method')}
|
||||
id='smartMode'
|
||||
onChange={this._handleSmartBackupMode}
|
||||
required
|
||||
value={method}
|
||||
value={smartBackupMode ? 'smart' : 'normal'}
|
||||
>
|
||||
{_('noSelectedValue', message => <option value=''>{message}</option>)}
|
||||
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
|
||||
_({ key }, info.label, message => <option value={key}>{message}</option>)
|
||||
)}
|
||||
{_('normalBackup', message => <option value='normal'>{message}</option>)}
|
||||
{_('smartBackup', message => <option value='smart'>{message}</option>)}
|
||||
</select>
|
||||
</fieldset>
|
||||
{(method === 'vm.rollingDeltaBackup' || method === 'vm.deltaCopy') && <div className='alert alert-warning' role='alert'>
|
||||
<Icon icon='error' /> {_('backupVersionWarning')}
|
||||
</div>}
|
||||
{backupInfo && <div>
|
||||
<GenericInput
|
||||
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
|
||||
required
|
||||
schema={backupInfo.schema}
|
||||
uiSchema={backupInfo.uiSchema}
|
||||
onChange={this.linkState('mainParams')}
|
||||
value={this._getMainParams()}
|
||||
/>
|
||||
<fieldset className='form-group'>
|
||||
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
|
||||
<select
|
||||
className='form-control'
|
||||
id='smartMode'
|
||||
onChange={this._handleSmartBackupMode}
|
||||
required
|
||||
value={smartBackupMode ? 'smart' : 'normal'}
|
||||
>
|
||||
{_('normalBackup', message => <option value='normal'>{message}</option>)}
|
||||
{_('smartBackup', message => <option value='smart'>{message}</option>)}
|
||||
</select>
|
||||
</fieldset>
|
||||
{smartBackupMode
|
||||
? <Upgrade place='newBackup' required={3}>
|
||||
<GenericInput
|
||||
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
|
||||
onChange={this.linkState('vmsParam')}
|
||||
required
|
||||
schema={SMART_SCHEMA}
|
||||
uiSchema={SMART_UI_SCHEMA}
|
||||
value={vms}
|
||||
/>
|
||||
</Upgrade>
|
||||
: <GenericInput
|
||||
{smartBackupMode
|
||||
? <Upgrade place='newBackup' required={3}>
|
||||
<GenericInput
|
||||
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
|
||||
onChange={this.linkState('vmsParam')}
|
||||
required
|
||||
schema={NO_SMART_SCHEMA}
|
||||
uiSchema={NO_SMART_UI_SCHEMA}
|
||||
schema={SMART_SCHEMA}
|
||||
uiSchema={SMART_UI_SCHEMA}
|
||||
value={vms}
|
||||
/>
|
||||
}
|
||||
</div>}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Section>
|
||||
<Section icon='schedule' title='schedule'>
|
||||
<Scheduler
|
||||
onChange={this.linkState('scheduling')}
|
||||
value={scheduling}
|
||||
/>
|
||||
</Section>
|
||||
<Section icon='preview' title='preview' summary>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<SchedulePreview cronPattern={scheduling.cronPattern} />
|
||||
{process.env.XOA_PLAN < 4 && backupInfo && process.env.XOA_PLAN < REQUIRED_XOA_PLAN[backupInfo.jobKey]
|
||||
? <Upgrade place='newBackup' available={REQUIRED_XOA_PLAN[backupInfo.jobKey]} />
|
||||
: (smartBackupMode && process.env.XOA_PLAN < 3
|
||||
? <Upgrade place='newBackup' available={3} />
|
||||
: <fieldset className='pull-right pt-1'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='mr-1'
|
||||
disabled={!backupInfo}
|
||||
form='form-new-vm-backup'
|
||||
handler={this._handleSubmit}
|
||||
icon='save'
|
||||
redirectOnSuccess='/backup/overview'
|
||||
size='large'
|
||||
>
|
||||
{_('saveBackupJob')}
|
||||
</ActionButton>
|
||||
<Button onClick={this._handleReset} size='large'>
|
||||
{_('selectTableReset')}
|
||||
</Button>
|
||||
</fieldset>)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Section>
|
||||
</Wizard>
|
||||
</form>
|
||||
</Upgrade>
|
||||
: <GenericInput
|
||||
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
|
||||
onChange={this.linkState('vmsParam')}
|
||||
required
|
||||
schema={NO_SMART_SCHEMA}
|
||||
uiSchema={NO_SMART_UI_SCHEMA}
|
||||
value={vms}
|
||||
/>
|
||||
}
|
||||
</div>}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Section>
|
||||
<Section icon='schedule' title='schedule'>
|
||||
<Scheduler
|
||||
onChange={this.linkState('scheduling')}
|
||||
value={scheduling}
|
||||
/>
|
||||
</Section>
|
||||
<Section icon='preview' title='preview' summary>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<SchedulePreview cronPattern={scheduling.cronPattern} />
|
||||
{process.env.XOA_PLAN < 4 && backupInfo && process.env.XOA_PLAN < REQUIRED_XOA_PLAN[backupInfo.jobKey]
|
||||
? <Upgrade place='newBackup' available={REQUIRED_XOA_PLAN[backupInfo.jobKey]} />
|
||||
: (smartBackupMode && process.env.XOA_PLAN < 3
|
||||
? <Upgrade place='newBackup' available={3} />
|
||||
: <fieldset className='pull-right pt-1'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='mr-1'
|
||||
disabled={!backupInfo}
|
||||
form='form-new-vm-backup'
|
||||
handler={this._handleSubmit}
|
||||
icon='save'
|
||||
redirectOnSuccess='/backup/overview'
|
||||
size='large'
|
||||
>
|
||||
{_('saveBackupJob')}
|
||||
</ActionButton>
|
||||
<Button onClick={this._handleReset} size='large'>
|
||||
{_('selectTableReset')}
|
||||
</Button>
|
||||
</fieldset>)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Section>
|
||||
</form></Wizard>
|
||||
</Upgrade>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ export default class Overview extends Component {
|
||||
<div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='schedule' /> {_('backupSchedules')}
|
||||
<h5><Icon icon='schedule' /> {_('backupSchedules')}</h5>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{schedules.length ? (
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import _, { messages } from 'intl'
|
||||
import ChooseSrForEachVdisModal from 'xo/choose-sr-for-each-vdis-modal'
|
||||
import Component from 'base-component'
|
||||
import every from 'lodash/every'
|
||||
import filter from 'lodash/filter'
|
||||
import find from 'lodash/find'
|
||||
import forEach from 'lodash/forEach'
|
||||
import getEventValue from 'get-event-value'
|
||||
import groupBy from 'lodash/groupBy'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
@@ -18,27 +15,22 @@ import SortedTable from 'sorted-table'
|
||||
import uniq from 'lodash/uniq'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { confirm } from 'modal'
|
||||
import { createSelector } from 'selectors'
|
||||
import { addSubscriptions, noop } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { FormattedDate, injectIntl } from 'react-intl'
|
||||
import { info, error } from 'notification'
|
||||
import { SelectPlainObject, Toggle } from 'form'
|
||||
import { SelectSr } from 'select-objects'
|
||||
|
||||
import {
|
||||
importBackup,
|
||||
importDeltaBackup,
|
||||
isSrWritable,
|
||||
listRemote,
|
||||
listRemoteBackups,
|
||||
startVm,
|
||||
subscribeRemotes
|
||||
} from 'xo'
|
||||
|
||||
// Can 2 SRs on the same pool have 2 VDIs used by the same VM
|
||||
const areSrsCompatible = (sr1, sr2) =>
|
||||
sr1.shared || sr2.shared || sr1.$container === sr2.$container
|
||||
|
||||
const parseDate = date => +moment(date, 'YYYYMMDDTHHmmssZ').format('x')
|
||||
|
||||
const backupOptionRenderer = backup => <span>
|
||||
@@ -57,7 +49,7 @@ const VM_COLUMNS = [
|
||||
{
|
||||
name: _('backupTags'),
|
||||
itemRenderer: ({ tagsByRemote }) => <Container>
|
||||
{map(tagsByRemote, ({ tags, remoteName }, key) => <Row key={key}>
|
||||
{map(tagsByRemote, ({ tags, remoteName }) => <Row>
|
||||
<Col mediumSize={3}><strong>{remoteName}</strong></Col>
|
||||
<Col mediumSize={9}>{tags.join(', ')}</Col>
|
||||
</Row>)}
|
||||
@@ -84,8 +76,8 @@ const openImportModal = ({ backups }) => confirm({
|
||||
body: <ImportModalBody vmName={backups[0].name} backups={backups} />
|
||||
}).then(doImport)
|
||||
|
||||
const doImport = ({ backup, mainSr, start, mapVdisSrs }) => {
|
||||
if (!mainSr || !backup) {
|
||||
const doImport = ({ backup, sr, start }) => {
|
||||
if (!sr || !backup) {
|
||||
error(_('backupRestoreErrorTitle'), _('backupRestoreErrorMessage'))
|
||||
return
|
||||
}
|
||||
@@ -95,7 +87,7 @@ const doImport = ({ backup, mainSr, start, mapVdisSrs }) => {
|
||||
}
|
||||
info(_('importBackupTitle'), _('importBackupMessage'))
|
||||
try {
|
||||
const importPromise = importMethods[backup.type]({remote: backup.remoteId, sr: mainSr, file: backup.path, mapVdisSrs}).then(id => {
|
||||
const importPromise = importMethods[backup.type]({remote: backup.remoteId, sr, file: backup.path}).then(id => {
|
||||
return id
|
||||
})
|
||||
if (start) {
|
||||
@@ -107,59 +99,16 @@ const doImport = ({ backup, mainSr, start, mapVdisSrs }) => {
|
||||
}
|
||||
|
||||
class _ModalBody extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
mapVdisSrs: {}
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.state
|
||||
}
|
||||
|
||||
_getSrPredicate = createSelector(
|
||||
() => this.state.sr,
|
||||
() => this.state.mapVdisSrs,
|
||||
(defaultSr, mapVdisSrs) => sr =>
|
||||
sr !== defaultSr &&
|
||||
isSrWritable(sr) &&
|
||||
defaultSr.$pool === sr.$pool &&
|
||||
areSrsCompatible(defaultSr, sr) &&
|
||||
every(mapVdisSrs, selectedSr => selectedSr == null || areSrsCompatible(selectedSr, sr))
|
||||
)
|
||||
|
||||
_onChangeDefaultSr = event => {
|
||||
const oldSr = this.state.sr
|
||||
const newSr = getEventValue(event)
|
||||
|
||||
if (oldSr == null || newSr == null || oldSr.$pool !== newSr.$pool) {
|
||||
this.setState({
|
||||
mapVdisSrs: {}
|
||||
})
|
||||
} else if (!newSr.shared) {
|
||||
const mapVdisSrs = {...this.state.mapVdisSrs}
|
||||
forEach(mapVdisSrs, (sr, vdi) => {
|
||||
if (sr != null && newSr !== sr && sr.$container !== newSr.$container && !sr.shared) {
|
||||
delete mapVdisSrs[vdi]
|
||||
}
|
||||
})
|
||||
this.setState({
|
||||
mapVdisSrs
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({
|
||||
sr: newSr
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { backups, intl } = this.props
|
||||
const vdis = this.state.backup && this.state.backup.vdis
|
||||
|
||||
return <div>
|
||||
<SelectSr onChange={this.linkState('sr')} predicate={isSrWritable} />
|
||||
<br />
|
||||
<SelectPlainObject
|
||||
onChange={this.linkState('backup')}
|
||||
optionKey='path'
|
||||
@@ -168,11 +117,6 @@ class _ModalBody extends Component {
|
||||
placeholder={intl.formatMessage(messages.importBackupModalSelectBackup)}
|
||||
/>
|
||||
<br />
|
||||
<ChooseSrForEachVdisModal
|
||||
vdis={vdis}
|
||||
onChange={props => this.setState(props)}
|
||||
/>
|
||||
<br />
|
||||
<Toggle onChange={this.linkState('start')} /> {_('importBackupModalStart')}
|
||||
</div>
|
||||
}
|
||||
@@ -192,26 +136,16 @@ export default class Restore extends Component {
|
||||
}
|
||||
|
||||
_listAll = async remotes => {
|
||||
const remotesInfo = await Promise.all(map(remotes, async remote => ({
|
||||
files: await listRemote(remote.id),
|
||||
backupsInfo: await listRemoteBackups(remote.id)
|
||||
})))
|
||||
|
||||
const remotesFiles = await Promise.all(map(remotes, remote => listRemote(remote.id)))
|
||||
const backupInfoByVm = {}
|
||||
|
||||
forEach(remotesInfo, (remoteInfo, index) => {
|
||||
forEach(remotesFiles, (remoteFiles, index) => {
|
||||
const remote = remotes[index]
|
||||
|
||||
forEach(remoteInfo.files, file => {
|
||||
forEach(remoteFiles, file => {
|
||||
let backup
|
||||
const deltaInfo = /^vm_delta_(.*)_([^/]+)\/([^_]+)_(.*)$/.exec(file)
|
||||
|
||||
if (deltaInfo) {
|
||||
const [ , tag, id, date, name ] = deltaInfo
|
||||
const vdis = find(remoteInfo.backupsInfo, {
|
||||
id: `${file}.json`
|
||||
}).disks
|
||||
|
||||
backup = {
|
||||
type: 'delta',
|
||||
date: parseDate(date),
|
||||
@@ -220,8 +154,7 @@ export default class Restore extends Component {
|
||||
path: file,
|
||||
tag,
|
||||
remoteId: remote.id,
|
||||
remoteName: remote.name,
|
||||
vdis
|
||||
remoteName: remote.name
|
||||
}
|
||||
} else {
|
||||
const backupInfo = /^([^_]+)_([^_]+)_(.*)\.xva$/.exec(file)
|
||||
|
||||
@@ -9,7 +9,6 @@ import Icon from 'icon'
|
||||
import invoke from 'invoke'
|
||||
import Link from 'link'
|
||||
import Page from '../page'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import Shortcuts from 'shortcuts'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
@@ -43,16 +42,18 @@ import {
|
||||
forgetSrs,
|
||||
isSrShared,
|
||||
migrateVms,
|
||||
rebootVmGroups,
|
||||
reconnectAllHostsSrs,
|
||||
rescanSrs,
|
||||
restartHosts,
|
||||
restartHostsAgents,
|
||||
restartVms,
|
||||
shutdownVmGroups,
|
||||
snapshotVms,
|
||||
startVmGroups,
|
||||
startVms,
|
||||
stopHosts,
|
||||
stopVms,
|
||||
subscribeResourceSets,
|
||||
subscribeServers
|
||||
} from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
@@ -75,8 +76,7 @@ import {
|
||||
createPager,
|
||||
createSelector,
|
||||
createSort,
|
||||
getUser,
|
||||
isAdmin
|
||||
getUser
|
||||
} from 'selectors'
|
||||
import {
|
||||
DropdownButton,
|
||||
@@ -89,6 +89,7 @@ import {
|
||||
import styles from './index.css'
|
||||
import HostItem from './host-item'
|
||||
import PoolItem from './pool-item'
|
||||
import VmGroupItem from './vm-group-item'
|
||||
import VmItem from './vm-item'
|
||||
import TemplateItem from './template-item'
|
||||
import SrItem from './sr-item'
|
||||
@@ -153,6 +154,20 @@ const OPTIONS = {
|
||||
{ labelId: 'homeSortByCpus', sortBy: 'CPUs.number', sortOrder: 'desc' }
|
||||
]
|
||||
},
|
||||
'VmGroup': {
|
||||
defaultFilter: '',
|
||||
filters: homeFilters.vmGroup,
|
||||
mainActions: [
|
||||
{ handler: shutdownVmGroups, icon: 'vm-stop', tooltip: _('stopVmLabel') },
|
||||
{ handler: startVmGroups, icon: 'vm-start', tooltip: _('startVmLabel') },
|
||||
{ handler: rebootVmGroups, icon: 'vm-reboot', tooltip: _('rebootVmLabel') }
|
||||
],
|
||||
Item: VmGroupItem,
|
||||
sortOptions: [
|
||||
{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
|
||||
{ labelId: 'homeSortByPowerstate', sortBy: 'power_state', sortOrder: 'desc' }
|
||||
]
|
||||
},
|
||||
pool: {
|
||||
defaultFilter: '',
|
||||
filters: homeFilters.pool,
|
||||
@@ -199,6 +214,7 @@ const OPTIONS = {
|
||||
|
||||
const TYPES = {
|
||||
VM: _('homeTypeVm'),
|
||||
VmGroup: _('homeTypeVmGroup'),
|
||||
'VM-template': _('homeTypeVmTemplate'),
|
||||
host: _('homeTypeHost'),
|
||||
pool: _('homeTypePool'),
|
||||
@@ -208,119 +224,25 @@ const TYPES = {
|
||||
const DEFAULT_TYPE = 'VM'
|
||||
|
||||
@addSubscriptions({
|
||||
noRegisteredServers: cb => subscribeServers(data => cb(isEmpty(data)))
|
||||
servers: subscribeServers
|
||||
})
|
||||
@connectStore(() => {
|
||||
const noServersConnected = invoke(
|
||||
createGetObjectsOfType('host'),
|
||||
hosts => state => isEmpty(hosts(state))
|
||||
)
|
||||
const getType = (_, props) => props.location.query.t || DEFAULT_TYPE
|
||||
const getObjectsByType = createGetObjectsOfType(getType)
|
||||
|
||||
return {
|
||||
areObjectsFetched,
|
||||
noServersConnected
|
||||
}
|
||||
})
|
||||
@propTypes({
|
||||
isAdmin: propTypes.bool.isRequired,
|
||||
noResourceSets: propTypes.bool.isRequired
|
||||
})
|
||||
class NoObjects_ extends Component {
|
||||
render () {
|
||||
const {
|
||||
areObjectsFetched,
|
||||
isAdmin,
|
||||
noRegisteredServers,
|
||||
noResourceSets,
|
||||
noServersConnected
|
||||
} = this.props
|
||||
|
||||
if (!areObjectsFetched) {
|
||||
return <CenterPanel>
|
||||
<h2><img src='assets/loading.svg' /></h2>
|
||||
</CenterPanel>
|
||||
return (state, props) => {
|
||||
const type = getType(state, props)
|
||||
return {
|
||||
areObjectsFetched: areObjectsFetched(state, props),
|
||||
items: getObjectsByType(state, props),
|
||||
noServersConnected: noServersConnected(state, props),
|
||||
type,
|
||||
user: getUser(state, props)
|
||||
}
|
||||
|
||||
if (noServersConnected && isAdmin) {
|
||||
return <CenterPanel>
|
||||
<Card shadow>
|
||||
<CardHeader>{_('homeWelcome')}</CardHeader>
|
||||
<CardBlock>
|
||||
<Link to='/settings/servers'>
|
||||
<Icon icon='pool' size={4} />
|
||||
<h4>{noRegisteredServers ? _('homeAddServer') : _('homeConnectServer')}</h4>
|
||||
</Link>
|
||||
<p className='text-muted'>{noRegisteredServers ? _('homeWelcomeText') : _('homeConnectServerText')}</p>
|
||||
<br /><br />
|
||||
<h3>{_('homeHelp')}</h3>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<a href='https://xen-orchestra.com/docs/' target='_blank' className='btn btn-link'>
|
||||
<Icon icon='menu-about' size={4} />
|
||||
<h4>{_('homeOnlineDoc')}</h4>
|
||||
</a>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<a href='https://xen-orchestra.com/#!/member/support' target='_blank' className='btn btn-link'>
|
||||
<Icon icon='menu-settings-users' size={4} />
|
||||
<h4>{_('homeProSupport')}</h4>
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</CenterPanel>
|
||||
}
|
||||
|
||||
return <CenterPanel>
|
||||
<Card shadow>
|
||||
<CardHeader>{_('homeNoVms')}</CardHeader>
|
||||
{(isAdmin || !noResourceSets) && <CardBlock>
|
||||
<Row>
|
||||
<Col>
|
||||
<Link to='/vms/new'>
|
||||
<Icon icon='vm' size={4} />
|
||||
<h4>{_('homeNewVm')}</h4>
|
||||
</Link>
|
||||
<p className='text-muted'>{_('homeNewVmMessage')}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
{isAdmin && <div>
|
||||
<h2>{_('homeNoVmsOr')}</h2>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<Link to='/import'>
|
||||
<Icon icon='menu-new-import' size={4} />
|
||||
<h4>{_('homeImportVm')}</h4>
|
||||
</Link>
|
||||
<p className='text-muted'>{_('homeImportVmMessage')}</p>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<Link to='/backup/restore'>
|
||||
<Icon icon='backup' size={4} />
|
||||
<h4>{_('homeRestoreBackup')}</h4>
|
||||
</Link>
|
||||
<p className='text-muted'>{_('homeRestoreBackupMessage')}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>}
|
||||
</CardBlock>}
|
||||
</Card>
|
||||
</CenterPanel>
|
||||
}
|
||||
}
|
||||
|
||||
@addSubscriptions({
|
||||
noResourceSets: cb => subscribeResourceSets(data => cb(isEmpty(data)))
|
||||
})
|
||||
@connectStore(() => {
|
||||
const type = (_, props) => props.location.query.t || DEFAULT_TYPE
|
||||
|
||||
return {
|
||||
isAdmin,
|
||||
items: createGetObjectsOfType(type),
|
||||
type,
|
||||
user: getUser
|
||||
}
|
||||
})
|
||||
export default class Home extends Component {
|
||||
@@ -629,11 +551,7 @@ export default class Home extends Component {
|
||||
// Header --------------------------------------------------------------------
|
||||
|
||||
_renderHeader () {
|
||||
const {
|
||||
isAdmin,
|
||||
noResourceSets,
|
||||
type
|
||||
} = this.props
|
||||
const { type } = this.props
|
||||
const { filters } = OPTIONS[type]
|
||||
const customFilters = this._getCustomFilters()
|
||||
|
||||
@@ -655,7 +573,7 @@ export default class Home extends Component {
|
||||
{name}
|
||||
</MenuItem>
|
||||
),
|
||||
<MenuItem key='divider' divider />
|
||||
<MenuItem divider />
|
||||
]}
|
||||
{map(filters, (filter, label) =>
|
||||
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
|
||||
@@ -686,14 +604,13 @@ export default class Home extends Component {
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
{(isAdmin || !noResourceSets) && <Col mediumSize={3} className='text-xs-right'>
|
||||
<Col mediumSize={3} className='text-xs-right'>
|
||||
<Link
|
||||
className='btn btn-success'
|
||||
to='/vms/new'
|
||||
>
|
||||
to='/vms/new'>
|
||||
<Icon icon='vm-new' /> {_('homeNewVm')}
|
||||
</Link>
|
||||
</Col>}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
}
|
||||
@@ -702,17 +619,89 @@ export default class Home extends Component {
|
||||
|
||||
render () {
|
||||
const {
|
||||
isAdmin,
|
||||
noResourceSets
|
||||
areObjectsFetched,
|
||||
noServersConnected,
|
||||
servers,
|
||||
user
|
||||
} = this.props
|
||||
|
||||
const nItems = this._getNumberOfItems()
|
||||
const isAdmin = user && user.permission === 'admin'
|
||||
const noRegisteredServers = !servers || !servers.length
|
||||
|
||||
if (nItems < 1) {
|
||||
return <NoObjects_
|
||||
isAdmin={isAdmin}
|
||||
noResourceSets={noResourceSets}
|
||||
/>
|
||||
if (!areObjectsFetched) {
|
||||
return <CenterPanel>
|
||||
<h2><img src='assets/loading.svg' /></h2>
|
||||
</CenterPanel>
|
||||
}
|
||||
|
||||
if (noServersConnected && isAdmin) {
|
||||
return <CenterPanel>
|
||||
<Card shadow>
|
||||
<CardHeader>{_('homeWelcome')}</CardHeader>
|
||||
<CardBlock>
|
||||
<Link to='/settings/servers'>
|
||||
<Icon icon='pool' size={4} />
|
||||
<h4>{noRegisteredServers ? _('homeAddServer') : _('homeConnectServer')}</h4>
|
||||
</Link>
|
||||
<p className='text-muted'>{noRegisteredServers ? _('homeWelcomeText') : _('homeConnectServerText')}</p>
|
||||
<br /><br />
|
||||
<h3>{_('homeHelp')}</h3>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<a href='https://xen-orchestra.com/docs/' target='_blank' className='btn btn-link'>
|
||||
<Icon icon='menu-about' size={4} />
|
||||
<h4>{_('homeOnlineDoc')}</h4>
|
||||
</a>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<a href='https://xen-orchestra.com/#!/member/support' target='_blank' className='btn btn-link'>
|
||||
<Icon icon='menu-settings-users' size={4} />
|
||||
<h4>{_('homeProSupport')}</h4>
|
||||
</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</CenterPanel>
|
||||
}
|
||||
|
||||
const nItems = this._getNumberOfItems()
|
||||
if (!nItems) {
|
||||
return <CenterPanel>
|
||||
<Card shadow>
|
||||
<CardHeader>{_('homeNoVms')}</CardHeader>
|
||||
<CardBlock>
|
||||
<Row>
|
||||
<Col>
|
||||
<Link to='/vms/new'>
|
||||
<Icon icon='vm' size={4} />
|
||||
<h4>{_('homeNewVm')}</h4>
|
||||
</Link>
|
||||
<p className='text-muted'>{_('homeNewVmMessage')}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
{isAdmin && <div>
|
||||
<h2>{_('homeNoVmsOr')}</h2>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<Link to='/import'>
|
||||
<Icon icon='menu-new-import' size={4} />
|
||||
<h4>{_('homeImportVm')}</h4>
|
||||
</Link>
|
||||
<p className='text-muted'>{_('homeImportVmMessage')}</p>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<Link to='/backup/restore'>
|
||||
<Icon icon='backup' size={4} />
|
||||
<h4>{_('homeRestoreBackup')}</h4>
|
||||
</Link>
|
||||
<p className='text-muted'>{_('homeRestoreBackupMessage')}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</CenterPanel>
|
||||
}
|
||||
|
||||
const filteredItems = this._getFilteredItems()
|
||||
@@ -840,7 +829,7 @@ export default class Home extends Component {
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
{' '}
|
||||
<OverlayTrigger
|
||||
{type !== 'VmGroup' && <OverlayTrigger
|
||||
autoFocus
|
||||
trigger='click'
|
||||
rootClose
|
||||
@@ -858,7 +847,7 @@ export default class Home extends Component {
|
||||
}
|
||||
>
|
||||
<Button btnStyle='link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
|
||||
</OverlayTrigger>
|
||||
</OverlayTrigger> }
|
||||
{' '}
|
||||
<DropdownButton bsStyle='link' id='sort' title={_('homeSortBy')}>
|
||||
{map(options.sortOptions, ({ labelId, sortBy: _sortBy, sortOrder }, key) => (
|
||||
@@ -893,7 +882,7 @@ export default class Home extends Component {
|
||||
item={item}
|
||||
key={item.id}
|
||||
onSelect={this.toggleState(`selectedItems.${item.id}`)}
|
||||
selected={Boolean(selectedItems[item.id])}
|
||||
selected={selectedItems[item.id]}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
||||
77
src/xo-app/home/vm-group-item.js
Normal file
77
src/xo-app/home/vm-group-item.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Ellipsis, { EllipsisContainer } from 'ellipsis'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import Tooltip from 'tooltip'
|
||||
import { BlockLink } from 'link'
|
||||
import { Col } from 'grid'
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObject } from 'selectors'
|
||||
import { editVmGroup } from 'xo'
|
||||
import { Text } from 'editable'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@connectStore(() => {
|
||||
return (state, props) => {
|
||||
const vms = {}
|
||||
forEach(props.item.$VMs, vmId => {
|
||||
const getVM = createGetObject(() => vmId)
|
||||
vms[vmId] = getVM(state, props)
|
||||
})
|
||||
return {
|
||||
vms
|
||||
}
|
||||
}
|
||||
})
|
||||
export default class VmGroupItem extends Component {
|
||||
toggleState = stateField => () => this.setState({ [stateField]: !this.state[stateField] })
|
||||
_onSelect = () => this.props.onSelect(this.props.item.id)
|
||||
_setNameDescription = description => editVmGroup(this.props.item, {name_description: description})
|
||||
_setNameLabel = label => editVmGroup(this.props.item, {name_label: label})
|
||||
_getVmGroupState = (vmGroup) => {
|
||||
const states = map(this.props.vms, vm => vm.power_state)
|
||||
return isEmpty(states)
|
||||
? 'Busy'
|
||||
: states.indexOf('Halted') === -1
|
||||
? states[0]
|
||||
: states.indexOf('Running') === -1
|
||||
? states[0]
|
||||
: 'Busy'
|
||||
}
|
||||
|
||||
render () {
|
||||
const { item: vmGroup, selected, vms } = this.props
|
||||
return <div className={styles.item}>
|
||||
<BlockLink to={`/vm-group/${vmGroup.id}`}>
|
||||
<SingleLineRow>
|
||||
<Col smallSize={10} mediumSize={9} largeSize={3}>
|
||||
<EllipsisContainer>
|
||||
<input type='checkbox' checked={selected} onChange={this._onSelect} value={vmGroup.id} />
|
||||
|
||||
<Ellipsis>
|
||||
<Tooltip content={_(`powerStateVmGroup${this._getVmGroupState(vmGroup)}`)}>
|
||||
<Icon icon={isEmpty(vms) ? 'halted' : `${this._getVmGroupState(vmGroup).toLowerCase()}`} />
|
||||
</Tooltip>
|
||||
<Text value={vmGroup.name_label} onChange={this._setNameLabel} useLongClick />
|
||||
</Ellipsis>
|
||||
|
||||
</EllipsisContainer>
|
||||
</Col>
|
||||
<Col mediumSize={4} className='hidden-md-down'>
|
||||
<EllipsisContainer>
|
||||
<Ellipsis>
|
||||
<Text value={vmGroup.name_description} onChange={this._setNameDescription} useLongClick />
|
||||
</Ellipsis>
|
||||
</EllipsisContainer>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</BlockLink>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,11 @@ export default class VmItem extends Component {
|
||||
return vm && vm.power_state === 'Running'
|
||||
}
|
||||
|
||||
_getMigrationPredicate = createSelector(
|
||||
() => this.props.container,
|
||||
container => host => host.id !== container.id
|
||||
)
|
||||
|
||||
_getResourceSet = createFinder(
|
||||
() => this.props.resourceSets,
|
||||
createSelector(
|
||||
@@ -139,6 +144,7 @@ export default class VmItem extends Component {
|
||||
labelProp='name_label'
|
||||
onChange={this._migrateVm}
|
||||
placeholder={_('homeMigrateTo')}
|
||||
predicate={this._getMigrationPredicate()}
|
||||
useLongClick
|
||||
value={container}
|
||||
xoType='host'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import _ from 'intl'
|
||||
import ActionBar, { Action } from 'action-bar'
|
||||
import ActionBar from 'action-bar'
|
||||
import React from 'react'
|
||||
import {
|
||||
// disableHost,
|
||||
@@ -13,42 +12,44 @@ import {
|
||||
const hostActionBarByState = {
|
||||
Running: ({ host }) => (
|
||||
<ActionBar
|
||||
actions={[
|
||||
{
|
||||
icon: 'host-stop',
|
||||
label: 'stopHostLabel',
|
||||
handler: stopHost
|
||||
},
|
||||
{
|
||||
icon: 'host-restart-agent',
|
||||
label: 'restartHostAgent',
|
||||
handler: restartHostAgent
|
||||
},
|
||||
{
|
||||
icon: 'host-emergency-shutdown',
|
||||
label: 'emergencyModeLabel',
|
||||
handler: emergencyShutdownHost
|
||||
},
|
||||
{
|
||||
icon: 'host-reboot',
|
||||
label: 'rebootHostLabel',
|
||||
handler: restartHost
|
||||
}
|
||||
]}
|
||||
display='icon'
|
||||
handlerParam={host}
|
||||
>
|
||||
<Action
|
||||
handler={stopHost}
|
||||
icon='host-stop'
|
||||
label={_('stopHostLabel')}
|
||||
/>
|
||||
<Action
|
||||
handler={restartHostAgent}
|
||||
icon='host-restart-agent'
|
||||
label={_('restartHostAgent')}
|
||||
/>
|
||||
<Action
|
||||
handler={emergencyShutdownHost}
|
||||
icon='host-emergency-shutdown'
|
||||
label={_('emergencyModeLabel')}
|
||||
/>
|
||||
<Action
|
||||
handler={restartHost}
|
||||
icon='host-reboot'
|
||||
label={_('rebootHostLabel')}
|
||||
/>
|
||||
</ActionBar>
|
||||
param={host}
|
||||
/>
|
||||
),
|
||||
Halted: ({ host }) => (
|
||||
<ActionBar
|
||||
actions={[
|
||||
{
|
||||
icon: 'host-start',
|
||||
label: 'startHostLabel',
|
||||
handler: startHost
|
||||
}
|
||||
]}
|
||||
display='icon'
|
||||
handlerParam={host}
|
||||
>
|
||||
<Action
|
||||
handler={startHost}
|
||||
icon='host-start'
|
||||
label={_('startHostLabel')}
|
||||
/>
|
||||
</ActionBar>
|
||||
param={host}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -255,6 +255,7 @@ export default class Host extends Component {
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
<Col>
|
||||
<NavTabs>
|
||||
|
||||
@@ -5,7 +5,7 @@ import TabButton from 'tab-button'
|
||||
import SelectFiles from 'select-files'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Toggle } from 'form'
|
||||
import { enableHost, detachHost, disableHost, forgetHost, restartHost, installSupplementalPack } from 'xo'
|
||||
import { enableHost, detachHost, disableHost, restartHost, installSupplementalPack } from 'xo'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
@@ -59,15 +59,6 @@ export default ({
|
||||
icon='host-eject'
|
||||
labelId='detachHost'
|
||||
/>
|
||||
{host.power_state !== 'Running' &&
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={forgetHost}
|
||||
handlerParam={host}
|
||||
icon='host-forget'
|
||||
labelId='forgetHost'
|
||||
/>
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
@@ -112,7 +103,7 @@ export default ({
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{_('hostXenServerVersion')}</th>
|
||||
<Copiable tagName='td' data={host.version}>
|
||||
<Copiable tagName='td'>
|
||||
{host.license_params.sku_marketing_name} {host.version} ({host.license_params.sku_type})
|
||||
</Copiable>
|
||||
</tr>
|
||||
|
||||
@@ -92,6 +92,7 @@ export default class extends Component {
|
||||
<Row className='console'>
|
||||
<Col>
|
||||
<NoVnc ref='noVnc' url={resolveUrl(`consoles/${vmController.id}`)} onClipboardChange={this._getRemoteClipboard} />
|
||||
<p><em><Icon icon='info' /> {_('tipLabel')} {_('tipConsoleLabel')}</em></p>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -90,8 +90,6 @@ class ConfigureIpModal extends Component {
|
||||
vifsByNetwork: createGetObjectsOfType('VIF').groupBy('$network')
|
||||
}))
|
||||
class PifItem extends Component {
|
||||
state = { configModes: [] }
|
||||
|
||||
componentWillMount () {
|
||||
getIpv4ConfigModes().then(configModes =>
|
||||
this.setState({ configModes })
|
||||
@@ -128,7 +126,7 @@ class PifItem extends Component {
|
||||
|
||||
const pifInUse = some(vifsByNetwork[pif.$network], vif => vif.attached)
|
||||
|
||||
return <tr>
|
||||
return <tr key={pif.id}>
|
||||
<td>{pif.device}</td>
|
||||
<td>{networks[pif.$network].name_label}</td>
|
||||
<td>
|
||||
@@ -240,7 +238,7 @@ export default ({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(pifs, pif => <PifItem key={pif.id} pif={pif} networks={networks} />)}
|
||||
{map(pifs, pif => <PifItem pif={pif} networks={networks} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
|
||||
@@ -36,6 +36,7 @@ import Sr from './sr'
|
||||
import Tasks from './tasks'
|
||||
import User from './user'
|
||||
import Vm from './vm'
|
||||
import VmGroup from './vm-group'
|
||||
import VmImport from './vm-import'
|
||||
import XoaUpdates from './xoa-updates'
|
||||
import Xosan from './xosan'
|
||||
@@ -86,6 +87,7 @@ const BODY_STYLE = {
|
||||
'vms/import': VmImport,
|
||||
'vms/new': NewVm,
|
||||
'vms/:id': Vm,
|
||||
'vm-group/:id': VmGroup,
|
||||
'xoa-update': XoaUpdates,
|
||||
'xosan': Xosan
|
||||
})
|
||||
|
||||
@@ -381,7 +381,7 @@ export default class Jobs extends Component {
|
||||
/>
|
||||
<input type='text' ref='name' className='form-control mb-1 mt-1' placeholder={formatMessage(messages.jobNamePlaceholder)} pattern='[^_]+' required />
|
||||
<SelectPlainObject ref='method' options={actions} optionKey='method' onChange={this._handleSelectMethod} placeholder={_('jobActionPlaceHolder')} />
|
||||
<input type='number' onChange={this.linkState('timeout')} value={state.timeout || ''} className='form-control mb-1 mt-1' placeholder={formatMessage(messages.jobTimeoutPlaceHolder)} />
|
||||
<input type='number' onChange={this.linkState('timeout')} value={state.timeout} className='form-control mb-1 mt-1' placeholder='Job timeout (seconds)' />
|
||||
{action && <fieldset>
|
||||
<GenericInput ref='params' schema={action.info} uiSchema={action.uiSchema} label={action.method} required />
|
||||
{job && <p className='text-warning'>{_('jobEditMessage', { name: job.name, id: job.id.slice(4, 8) })}</p>}
|
||||
|
||||
@@ -15,13 +15,10 @@ import renderXoItem from 'render-xo-item'
|
||||
import SortedTable from 'sorted-table'
|
||||
import Tooltip from 'tooltip'
|
||||
import { alert, confirm } from 'modal'
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObject } from 'selectors'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import {
|
||||
connectStore,
|
||||
formatSize,
|
||||
formatSpeed
|
||||
} from 'utils'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
@@ -55,9 +52,9 @@ class JobParam extends Component {
|
||||
id
|
||||
} = this.props
|
||||
|
||||
return object != null
|
||||
? _.keyValue(object.type || paramKey, renderXoItem(object))
|
||||
: _.keyValue(paramKey, String(id))
|
||||
return object
|
||||
? <span><strong>{object.type || paramKey}</strong>: {renderXoItem(object)} </span>
|
||||
: <span><strong>{paramKey}:</strong> {String(id)} </span>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,33 +70,9 @@ class JobReturn extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const JobCallStateInfos = ({ end, error }) => {
|
||||
const [ icon, tooltip ] = error !== undefined
|
||||
? ['halted', 'failedJobCall']
|
||||
: end !== undefined
|
||||
? ['running', 'successfulJobCall']
|
||||
: ['busy', 'jobCallInProgess']
|
||||
|
||||
return <Tooltip content={_(tooltip)}>
|
||||
<Icon icon={icon} />
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
const JobTransferredDataInfos = ({ start, end, size }) => <div>
|
||||
<span><strong>{_('jobTransferredDataSize')}</strong> {formatSize(size)}</span>
|
||||
<br />
|
||||
<span><strong>{_('jobTransferredDataSpeed')}</strong> {formatSpeed(size, end - start)}</span>
|
||||
</div>
|
||||
|
||||
const Log = props => <ul className='list-group'>
|
||||
{map(props.log.calls, call => {
|
||||
const {
|
||||
end,
|
||||
error,
|
||||
returnedValue,
|
||||
start
|
||||
} = call
|
||||
|
||||
const { returnedValue } = call
|
||||
let id
|
||||
if (returnedValue != null) {
|
||||
id = returnedValue.id
|
||||
@@ -109,10 +82,8 @@ const Log = props => <ul className='list-group'>
|
||||
}
|
||||
|
||||
return <li key={call.callKey} className='list-group-item'>
|
||||
<strong className='text-info'>{call.method}: </strong><JobCallStateInfos end={end} error={error} /><br />
|
||||
<strong className='text-info'>{call.method}: </strong><br />
|
||||
{map(call.params, (value, key) => [ <JobParam id={value} paramKey={key} key={key} />, <br /> ])}
|
||||
{end !== undefined && _.keyValue(_('jobDuration'), <FormattedDuration duration={end - start} />)}
|
||||
{returnedValue != null && returnedValue.size !== undefined && <JobTransferredDataInfos start={start} end={end} size={returnedValue.size} />}
|
||||
{id !== undefined && <span>{' '}<JobReturn id={id} /></span>}
|
||||
{call.error &&
|
||||
<span className='text-danger'>
|
||||
@@ -237,7 +208,6 @@ export default class LogList extends Component {
|
||||
callKey: logKey,
|
||||
params: data.params,
|
||||
method: data.method,
|
||||
start: time,
|
||||
time
|
||||
}
|
||||
} else if (data.event === 'jobCall.end') {
|
||||
@@ -249,7 +219,6 @@ export default class LogList extends Component {
|
||||
entry.meta = 'error'
|
||||
} else {
|
||||
call.returnedValue = data.returnedValue
|
||||
call.end = time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ export default class Menu extends Component {
|
||||
const items = [
|
||||
{ to: '/home', icon: 'menu-home', label: 'homePage', subMenu: [
|
||||
{ to: '/home?t=VM', icon: 'vm', label: 'homeVmPage' },
|
||||
{ to: '/home?t=VmGroup', icon: 'vm', label: 'homeVmGroupPage' },
|
||||
nHosts !== 0 && { to: '/home?t=host', icon: 'host', label: 'homeHostPage' },
|
||||
!isEmpty(pools) && { to: '/home?t=pool', icon: 'pool', label: 'homePoolPage' },
|
||||
isAdmin && { to: '/home?t=VM-template', icon: 'template', label: 'homeTemplatePage' },
|
||||
@@ -166,7 +167,8 @@ export default class Menu extends Component {
|
||||
{ to: '/tasks', icon: 'task', label: 'taskMenu', pill: nTasks },
|
||||
isAdmin && { to: '/xosan', icon: 'menu-xosan', label: 'xosan' },
|
||||
!(noOperatablePools && noResourceSets) && { to: '/vms/new', icon: 'menu-new', label: 'newMenu', subMenu: [
|
||||
(isAdmin || !noResourceSets) && { to: '/vms/new', icon: 'menu-new-vm', label: 'newVmPage' },
|
||||
{ to: '/vms/new', icon: 'menu-new-vm', label: 'newVmPage' },
|
||||
{ to: '/new/vm-group', icon: 'menu-new-vm', label: 'newVmGroupPage' },
|
||||
isAdmin && { to: '/new/sr', icon: 'menu-new-sr', label: 'newSrPage' },
|
||||
isAdmin && { to: '/settings/servers', icon: 'menu-settings-servers', label: 'newServerPage' },
|
||||
!noOperatablePools && { to: '/vms/import', icon: 'menu-new-import', label: 'newImport' }
|
||||
|
||||
@@ -69,11 +69,11 @@ import {
|
||||
firstDefined,
|
||||
formatSize,
|
||||
getCoresPerSocketPossibilities,
|
||||
generateReadableRandomString,
|
||||
noop,
|
||||
resolveResourceSet
|
||||
} from 'utils'
|
||||
import {
|
||||
createFilter,
|
||||
createSelector,
|
||||
createGetObject,
|
||||
createGetObjectsOfType,
|
||||
@@ -90,6 +90,8 @@ const NB_VMS_MAX = 100
|
||||
|
||||
const getObject = createGetObject((_, id) => id)
|
||||
|
||||
const returnTrue = () => true
|
||||
|
||||
// Sub-components
|
||||
|
||||
const SectionContent = ({ column, children }) => (
|
||||
@@ -238,6 +240,9 @@ export default class NewVm extends BaseComponent {
|
||||
|
||||
// Utils -----------------------------------------------------------------------
|
||||
|
||||
getUniqueId () {
|
||||
return this._uniqueId++
|
||||
}
|
||||
get _isDiskTemplate () {
|
||||
const { template } = this.state.state
|
||||
return template &&
|
||||
@@ -398,7 +403,7 @@ export default class NewVm extends BaseComponent {
|
||||
}
|
||||
const vdi = getObject(storeState, vbd.VDI, resourceSet)
|
||||
if (vdi) {
|
||||
existingDisks[vbd.position] = {
|
||||
existingDisks[this.getUniqueId()] = {
|
||||
name_label: vdi.name_label,
|
||||
name_description: vdi.name_description,
|
||||
size: vdi.size,
|
||||
@@ -413,6 +418,7 @@ export default class NewVm extends BaseComponent {
|
||||
forEach(template.VIFs, vifId => {
|
||||
const vif = getObject(storeState, vifId, resourceSet)
|
||||
VIFs.push({
|
||||
id: this.getUniqueId(),
|
||||
network: pool || isInResourceSet(vif.$network)
|
||||
? vif.$network
|
||||
: resourceSet.objectsByType['network'][0].id
|
||||
@@ -421,6 +427,7 @@ export default class NewVm extends BaseComponent {
|
||||
if (VIFs.length === 0) {
|
||||
const networkId = this._getDefaultNetworkId()
|
||||
VIFs.push({
|
||||
id: this.getUniqueId(),
|
||||
network: networkId
|
||||
})
|
||||
}
|
||||
@@ -447,10 +454,12 @@ export default class NewVm extends BaseComponent {
|
||||
// disks
|
||||
existingDisks,
|
||||
VDIs: map(template.template_info.disks, disk => {
|
||||
const device = String(this.getUniqueId())
|
||||
return {
|
||||
...disk,
|
||||
device,
|
||||
name_description: disk.name_description || 'Created by XO',
|
||||
name_label: (name_label || 'disk') + '_' + generateReadableRandomString(5),
|
||||
name_label: (name_label || 'disk') + '_' + device,
|
||||
SR: pool
|
||||
? pool.default_SR
|
||||
: resourceSet.objectsByType['SR'][0].id
|
||||
@@ -484,6 +493,13 @@ export default class NewVm extends BaseComponent {
|
||||
objectsIds => id => includes(objectsIds, id)
|
||||
)
|
||||
|
||||
_getCanOperate = createSelector(
|
||||
() => this.props.isAdmin,
|
||||
() => this.props.permissions,
|
||||
(isAdmin, permissions) => isAdmin
|
||||
? returnTrue
|
||||
: ({ id }) => permissions && permissions[id] && permissions[id].operate
|
||||
)
|
||||
_getVmPredicate = createSelector(
|
||||
this._getIsInPool,
|
||||
this._getIsInResourceSet,
|
||||
@@ -532,7 +548,11 @@ export default class NewVm extends BaseComponent {
|
||||
},
|
||||
(networks, poolId) => filter(networks, network => network.$pool === poolId)
|
||||
)
|
||||
|
||||
_getOperatablePools = createFilter(
|
||||
() => this.props.pools,
|
||||
this._getCanOperate,
|
||||
[ (pool, canOperate) => canOperate(pool) ]
|
||||
)
|
||||
_getAffinityHostPredicate = createSelector(
|
||||
() => this.props.pool,
|
||||
() => this.state.state.existingDisks,
|
||||
@@ -635,10 +655,12 @@ export default class NewVm extends BaseComponent {
|
||||
_addVdi = () => {
|
||||
const { state } = this.state
|
||||
const { pool } = this.props
|
||||
const device = String(this.getUniqueId())
|
||||
|
||||
this._setState({ VDIs: [ ...state.VDIs, {
|
||||
device,
|
||||
name_description: 'Created by XO',
|
||||
name_label: (state.name_label || 'disk') + '_' + generateReadableRandomString(5),
|
||||
name_label: (state.name_label || 'disk') + '_' + device,
|
||||
SR: pool && pool.default_SR,
|
||||
type: 'system'
|
||||
}] })
|
||||
@@ -652,6 +674,7 @@ export default class NewVm extends BaseComponent {
|
||||
const networkId = this._getDefaultNetworkId()
|
||||
|
||||
this._setState({ VIFs: [ ...this.state.state.VIFs, {
|
||||
id: this.getUniqueId(),
|
||||
network: networkId
|
||||
}] })
|
||||
}
|
||||
@@ -686,10 +709,13 @@ export default class NewVm extends BaseComponent {
|
||||
// MAIN ------------------------------------------------------------------------
|
||||
|
||||
_renderHeader = () => {
|
||||
const {isAdmin, pool, resourceSets} = this.props
|
||||
const { pool } = this.props
|
||||
const showSelectPool = !isEmpty(this._getOperatablePools())
|
||||
const showSelectResourceSet = !this.props.isAdmin && !isEmpty(this.props.resourceSets)
|
||||
const selectPool = <span className={styles.inlineSelect}>
|
||||
<SelectPool
|
||||
onChange={this._selectPool}
|
||||
predicate={this._getCanOperate()}
|
||||
value={pool}
|
||||
/>
|
||||
</span>
|
||||
@@ -703,9 +729,14 @@ export default class NewVm extends BaseComponent {
|
||||
<Row>
|
||||
<Col mediumSize={12}>
|
||||
<h2>
|
||||
{isAdmin || !isEmpty(resourceSets)
|
||||
{showSelectPool && showSelectResourceSet
|
||||
? _('newVmCreateNewVmOn2', {
|
||||
select1: selectPool,
|
||||
select2: selectResourceSet
|
||||
})
|
||||
: showSelectPool || showSelectResourceSet
|
||||
? _('newVmCreateNewVmOn', {
|
||||
select: isAdmin ? selectPool : selectResourceSet
|
||||
select: showSelectPool ? selectPool : selectResourceSet
|
||||
})
|
||||
: _('newVmCreateNewVmNoPermission')
|
||||
}
|
||||
@@ -1159,7 +1190,7 @@ export default class NewVm extends BaseComponent {
|
||||
</div>)}
|
||||
|
||||
{/* VDIs */}
|
||||
{map(VDIs, (vdi, index) => <div key={index}>
|
||||
{map(VDIs, (vdi, index) => <div key={vdi.device}>
|
||||
<LineItem>
|
||||
<Item label={_('newVmSrLabel')}>
|
||||
<span className={styles.inlineSelect}>
|
||||
@@ -1245,7 +1276,6 @@ export default class NewVm extends BaseComponent {
|
||||
showAdvanced,
|
||||
tags
|
||||
} = this.state.state
|
||||
const { isAdmin } = this.props
|
||||
const { formatMessage } = this.props.intl
|
||||
return <Section icon='new-vm-advanced' title='newVmAdvancedPanel' done={this._isAdvancedDone()}>
|
||||
<SectionContent column>
|
||||
@@ -1381,7 +1411,7 @@ export default class NewVm extends BaseComponent {
|
||||
)}
|
||||
</LineItem>}
|
||||
</SectionContent>,
|
||||
isAdmin && <SectionContent>
|
||||
<SectionContent>
|
||||
<Item label={_('newVmAffinityHost')}>
|
||||
<SelectHost
|
||||
onChange={this._linkState('affinityHost')}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { routes } from 'utils'
|
||||
|
||||
import Sr from './sr'
|
||||
import VmGroup from './vm-group'
|
||||
|
||||
const New = routes('vm', {
|
||||
sr: Sr
|
||||
sr: Sr,
|
||||
'vm-group': VmGroup
|
||||
})(
|
||||
({ children }) => children
|
||||
)
|
||||
|
||||
@@ -491,7 +491,7 @@ export default class New extends Component {
|
||||
>
|
||||
<option value={null}>{formatMessage(messages.noSelectedValue)}</option>
|
||||
{map(typeGroups, (types, group) =>
|
||||
<optgroup key={group} label={SR_GROUP_TO_LABEL[group]}>
|
||||
<optgroup label={SR_GROUP_TO_LABEL[group]}>
|
||||
{map(types, type =>
|
||||
<option key={type} value={type}>{SR_TYPE_TO_LABEL[type]}</option>
|
||||
)}
|
||||
|
||||
153
src/xo-app/new/vm-group/index.js
Normal file
153
src/xo-app/new/vm-group/index.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import BaseComponent from 'base-component'
|
||||
import classNames from 'classnames'
|
||||
import DebounceInput from 'react-debounce-input'
|
||||
import Icon from 'icon'
|
||||
import Page from '../../page'
|
||||
import React from 'react'
|
||||
import Wizard, { Section } from 'wizard'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
createVmGroup,
|
||||
subscribeCurrentUser,
|
||||
subscribePermissions,
|
||||
subscribeResourceSets
|
||||
} from 'xo'
|
||||
import {
|
||||
createSelector,
|
||||
createGetObjectsOfType,
|
||||
getUser
|
||||
} from 'selectors'
|
||||
import { SelectPool } from 'select-objects'
|
||||
import { addSubscriptions, connectStore } from 'utils'
|
||||
|
||||
import styles from '../../new-vm/index.css'
|
||||
|
||||
const SectionContent = ({ column, children }) => (
|
||||
<div className={classNames(
|
||||
'form-inline',
|
||||
styles.sectionContent,
|
||||
column && styles.sectionContentColumn
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
const Item = ({ label, children, className }) => (
|
||||
<span className={styles.item}>
|
||||
{label && <span>{label} </span>}
|
||||
<span className={classNames(styles.input, className)}>{children}</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
@addSubscriptions({
|
||||
resourceSets: subscribeResourceSets,
|
||||
permissions: subscribePermissions,
|
||||
user: subscribeCurrentUser
|
||||
})
|
||||
@connectStore(() => ({
|
||||
isAdmin: createSelector(
|
||||
getUser,
|
||||
user => user && user.permission === 'admin'
|
||||
),
|
||||
pools: createGetObjectsOfType('pool')
|
||||
}))
|
||||
export default class VmGroup extends BaseComponent {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {name_label: '', name_description: ''}
|
||||
}
|
||||
|
||||
_selectPool = pool => {
|
||||
this.setState({ pool })
|
||||
this._reset()
|
||||
}
|
||||
|
||||
_getCanOperate = createSelector(
|
||||
() => this.props.isAdmin,
|
||||
() => this.props.permissions,
|
||||
(isAdmin, permissions) => isAdmin
|
||||
? () => true
|
||||
: ({ id }) => permissions && permissions[id] && permissions[id].operate
|
||||
)
|
||||
|
||||
_renderHeader = () => {
|
||||
const { pool } = this.state
|
||||
return <Container>
|
||||
<Row>
|
||||
<Col mediumSize={12}>
|
||||
<h2><Icon icon='sr' /> {_('newVmGroupTitle')}
|
||||
<SelectPool
|
||||
onChange={this._selectPool}
|
||||
predicate={this._getCanOperate()}
|
||||
value={pool}
|
||||
/>
|
||||
</h2>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
}
|
||||
_reset = () => this.setState({poolId: '', name_label: '', name_description: ''})
|
||||
_create = () => {
|
||||
createVmGroup(this.state)
|
||||
this.context.router.push('home?s=&t=VmGroup')
|
||||
}
|
||||
_getOnChange = item => ({target}) => this.setState({[item]: target.value})
|
||||
|
||||
render () {
|
||||
const {pool, name_label: nameLabel, name_description: nameDescription} = this.state
|
||||
return (
|
||||
<Page header={this._renderHeader()}>
|
||||
<form id='vmGroupCreation'>
|
||||
<Wizard>
|
||||
<Section icon='new-vm-infos' title='newVmGroupInfoPanel'>
|
||||
<SectionContent>
|
||||
<Item label={_('newVmGroupNameLabel')}>
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
// debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
onChange={this._getOnChange('name_label')}
|
||||
value={nameLabel}
|
||||
/>
|
||||
</Item>
|
||||
<Item label={_('newVmGroupDescriptionLabel')}>
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
// debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
onChange={this._getOnChange('name_description')}
|
||||
value={nameDescription}
|
||||
/>
|
||||
</Item>
|
||||
</SectionContent>
|
||||
</Section>
|
||||
</Wizard>
|
||||
<div className={styles.submitSection}>
|
||||
<ActionButton
|
||||
className={styles.button}
|
||||
handler={this._reset}
|
||||
icon='new-vm-reset'
|
||||
>
|
||||
{_('newVmGroupReset')}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className={styles.button}
|
||||
disabled={nameLabel === '' || pool === undefined}
|
||||
form='vmGroupCreation'
|
||||
handler={this._create}
|
||||
icon='new-vm-create'
|
||||
redirectOnSuccess={this._getRedirectionUrl}
|
||||
>
|
||||
{_('newVmGroupCreate')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</form>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import _ from 'intl'
|
||||
import ActionBar, { Action } from 'action-bar'
|
||||
import ActionBar from 'action-bar'
|
||||
import React from 'react'
|
||||
import {
|
||||
addHostToPool
|
||||
@@ -11,31 +10,30 @@ const NOT_IMPLEMENTED = () => {
|
||||
|
||||
const PoolActionBar = ({ pool }) => (
|
||||
<ActionBar
|
||||
actions={[
|
||||
{
|
||||
icon: 'add-sr',
|
||||
label: 'addSrLabel',
|
||||
redirectOnSuccess: `new/sr?host=${pool.master}`
|
||||
},
|
||||
{
|
||||
icon: 'add-vm',
|
||||
label: 'addVmLabel',
|
||||
redirectOnSuccess: `vms/new?pool=${pool.id}`
|
||||
},
|
||||
{
|
||||
icon: 'add-host',
|
||||
label: 'addHostLabel',
|
||||
handler: addHostToPool
|
||||
},
|
||||
{
|
||||
icon: 'disconnect',
|
||||
label: 'disconnectServer',
|
||||
handler: NOT_IMPLEMENTED // TODO disconnect server
|
||||
}
|
||||
]}
|
||||
display='icon'
|
||||
handlerParam={pool}
|
||||
>
|
||||
<Action
|
||||
handler={NOT_IMPLEMENTED}
|
||||
icon='add-sr'
|
||||
label={_('addSrLabel')}
|
||||
redirectOnSuccess={`new/sr?host=${pool.master}`}
|
||||
/>
|
||||
<Action
|
||||
handler={NOT_IMPLEMENTED}
|
||||
icon='add-vm'
|
||||
label={_('addVmLabel')}
|
||||
redirectOnSuccess={`vms/new?pool=${pool.id}`}
|
||||
/>
|
||||
<Action
|
||||
handler={addHostToPool}
|
||||
icon='add-host'
|
||||
label={_('addHostLabel')}
|
||||
/>
|
||||
<Action
|
||||
handler={NOT_IMPLEMENTED} // TODO disconnect server
|
||||
icon='disconnect'
|
||||
label={_('disconnectServer')}
|
||||
/>
|
||||
</ActionBar>
|
||||
param={pool}
|
||||
/>
|
||||
)
|
||||
export default PoolActionBar
|
||||
|
||||
@@ -124,6 +124,7 @@ export default class Pool extends Component {
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
<Col>
|
||||
<NavTabs>
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import _ from 'intl'
|
||||
import ActionRow from 'action-row-button'
|
||||
import Button from 'button'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import React, { Component } from 'react'
|
||||
import TabButton from 'tab-button'
|
||||
import { deleteMessage } from 'xo'
|
||||
import { createPager, createSelector } from 'selectors'
|
||||
import { createPager } from 'selectors'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
ceil,
|
||||
isEmpty,
|
||||
map
|
||||
} from 'lodash'
|
||||
|
||||
const LOGS_PER_PAGE = 10
|
||||
|
||||
export default class TabLogs extends Component {
|
||||
constructor () {
|
||||
@@ -21,12 +17,7 @@ export default class TabLogs extends Component {
|
||||
this.getLogs = createPager(
|
||||
() => this.props.logs,
|
||||
() => this.state.page,
|
||||
LOGS_PER_PAGE
|
||||
)
|
||||
|
||||
this.getNPages = createSelector(
|
||||
() => this.props.logs ? this.props.logs.length : 0,
|
||||
nLogs => ceil(nLogs / LOGS_PER_PAGE)
|
||||
10
|
||||
)
|
||||
|
||||
this.state = {
|
||||
@@ -35,12 +26,11 @@ export default class TabLogs extends Component {
|
||||
}
|
||||
|
||||
_deleteAllLogs = () => map(this.props.logs, deleteMessage)
|
||||
_nextPage = () => this.setState({ page: Math.min(this.state.page + 1, this.getNPages()) })
|
||||
_previousPage = () => this.setState({ page: Math.max(this.state.page - 1, 1) })
|
||||
_nextPage = () => this.setState({ page: this.state.page + 1 })
|
||||
_previousPage = () => this.setState({ page: this.state.page - 1 })
|
||||
|
||||
render () {
|
||||
const logs = this.getLogs()
|
||||
const { page } = this.state
|
||||
|
||||
return <Container>
|
||||
{isEmpty(logs)
|
||||
@@ -53,21 +43,15 @@ export default class TabLogs extends Component {
|
||||
: <div>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
<TabButton
|
||||
btnStyle='secondary'
|
||||
disabled={page === 1}
|
||||
handler={this._previousPage}
|
||||
icon='previous'
|
||||
/>
|
||||
<TabButton
|
||||
btnStyle='secondary'
|
||||
disabled={page === this.getNPages()}
|
||||
handler={this._nextPage}
|
||||
icon='next'
|
||||
/>
|
||||
<Button size='large' onClick={this._previousPage}>
|
||||
<
|
||||
</Button>
|
||||
<Button size='large' onClick={this._nextPage}>
|
||||
>
|
||||
</Button>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={this._removeAllLogs} // FIXME: define this method
|
||||
handler={this._removeAllLogs}
|
||||
icon='delete'
|
||||
labelId='logRemoveAll'
|
||||
/>
|
||||
|
||||
@@ -312,15 +312,15 @@ export class Edit extends Component {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
_onChangeIpPool = newIpPool => {
|
||||
const { ipPools, newIpPoolQuantity } = this.state
|
||||
_addIpPool = () => {
|
||||
const { ipPools, newIpPool, newIpPoolQuantity } = this.state
|
||||
|
||||
this.setState({
|
||||
ipPools: [ ...ipPools, { id: newIpPool.id, quantity: newIpPoolQuantity } ],
|
||||
newIpPool: undefined,
|
||||
newIpPoolQuantity: ''
|
||||
})
|
||||
}
|
||||
|
||||
_removeIpPool = index => {
|
||||
const ipPools = [ ...this.state.ipPools ]
|
||||
remove(ipPools, (_, i) => index === i)
|
||||
@@ -444,30 +444,33 @@ export class Edit extends Component {
|
||||
<Row>
|
||||
<Col mediumSize={4}>
|
||||
<Row>
|
||||
<Col mediumSize={3}>
|
||||
<strong>{_('quantity')}</strong>
|
||||
</Col>
|
||||
<Col mediumSize={7}>
|
||||
<strong>{_('ipPool')}</strong>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<strong>{_('quantity')}</strong>
|
||||
</Col>
|
||||
</Row>
|
||||
{map(state.ipPools, (ipPool, index) => <Row className='mb-1' key={index}>
|
||||
<Col mediumSize={3}>
|
||||
<input className='form-control' type='number' min={0} onChange={this.linkState(`ipPools.${index}.quantity`)} value={firstDefined(ipPool.quantity, '')} placeholder='∞' />
|
||||
</Col>
|
||||
<Col mediumSize={7}>
|
||||
<SelectIpPool onChange={this.linkState(`ipPools.${index}.id`, 'id')} value={ipPool.id} />
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<input className='form-control' type='number' min={0} onChange={this.linkState(`ipPools.${index}.quantity`)} value={firstDefined(ipPool.quantity, '')} placeholder='∞' />
|
||||
</Col>
|
||||
<Col mediumSize={2}>
|
||||
<ActionButton icon='delete' handler={this._removeIpPool} handlerParam={index} />
|
||||
</Col>
|
||||
</Row>)}
|
||||
<Row>
|
||||
<Col mediumSize={7}>
|
||||
<SelectIpPool onChange={this.linkState('newIpPool')} value={state.newIpPool} predicate={this._getIpPoolPredicate()} />
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<input className='form-control' type='number' min={0} onChange={this.linkState('newIpPoolQuantity')} value={state.newIpPoolQuantity || ''} placeholder='∞' />
|
||||
</Col>
|
||||
<Col mediumSize={7}>
|
||||
<SelectIpPool onChange={this._onChangeIpPool} value='' predicate={this._getIpPoolPredicate()} />
|
||||
<Col mediumSize={2}>
|
||||
<ActionButton icon='add' handler={this._addIpPool} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
@@ -507,7 +510,7 @@ class ResourceSet extends Component {
|
||||
} = resourceSet
|
||||
|
||||
return [
|
||||
<li key='subjects' className='list-group-item'>
|
||||
<li className='list-group-item'>
|
||||
<Subjects subjects={subjects} />
|
||||
</li>,
|
||||
...map(objectsByType, (objectsSet, type) => (
|
||||
@@ -515,7 +518,7 @@ class ResourceSet extends Component {
|
||||
{map(objectsSet, object => renderXoItem(object, { className: 'mr-1' }))}
|
||||
</li>
|
||||
)),
|
||||
!isEmpty(ipPools) && <li key='ipPools' className='list-group-item'>
|
||||
!isEmpty(ipPools) && <li className='list-group-item'>
|
||||
{map(ipPools, pool => {
|
||||
const resolvedIpPool = resolvedIpPools[pool]
|
||||
const limits = get(resourceSet, `limits[ipPool:${pool}]`)
|
||||
@@ -531,7 +534,7 @@ class ResourceSet extends Component {
|
||||
}
|
||||
)}
|
||||
</li>,
|
||||
<li key='graphs' className='list-group-item'>
|
||||
<li className='list-group-item'>
|
||||
<Row>
|
||||
<Col mediumSize={4}>
|
||||
<Card>
|
||||
@@ -613,7 +616,7 @@ class ResourceSet extends Component {
|
||||
</Col>
|
||||
</Row>
|
||||
</li>,
|
||||
<li key='actions' className='list-group-item text-xs-center'>
|
||||
<li className='list-group-item text-xs-center'>
|
||||
<div className='btn-toolbar'>
|
||||
<ActionButton btnStyle='primary' icon='edit' handler={this.toggleState('editionMode')}>{_('editResourceSet')}</ActionButton>
|
||||
<ActionButton btnStyle='danger' icon='delete' handler={deleteResourceSet} handlerParam={resourceSet}>{_('deleteResourceSet')}</ActionButton>
|
||||
|
||||
@@ -85,9 +85,9 @@ class IpsCell extends BaseComponent {
|
||||
<Row>
|
||||
<Col mediumSize={6} offset={5}><strong>{_('ipsVifs')}</strong></Col>
|
||||
</Row>
|
||||
{ipPool.addresses && map(formatIps(keys(ipPool.addresses)), (ip, key) => {
|
||||
{ipPool.addresses && map(formatIps(keys(ipPool.addresses)), ip => {
|
||||
if (isObject(ip)) { // Range of IPs
|
||||
return <Row key={key}>
|
||||
return <Row>
|
||||
<Col mediumSize={5}>
|
||||
<strong>{ip.first} <Icon icon='arrow-right' /> {ip.last}</strong>
|
||||
</Col>
|
||||
@@ -109,7 +109,7 @@ class IpsCell extends BaseComponent {
|
||||
? map(addressVifs, (vifId, index) => {
|
||||
const vif = vifs[vifId] && vifs[vifId][0]
|
||||
const network = vif && networks[vif.$network] && networks[vif.$network][0]
|
||||
return <span key={index} className='mr-1'>
|
||||
return <span className='mr-1'>
|
||||
{network && vif
|
||||
? `${network.name_label} #${vif.device}`
|
||||
: <em>{_('ipPoolUnknownVif')}</em>
|
||||
@@ -188,7 +188,7 @@ class NetworksCell extends BaseComponent {
|
||||
const { newNetworks, showNewNetworkForm } = this.state
|
||||
|
||||
return <Container>
|
||||
{map(ipPool.networks, networkId => <Row key={networkId}>
|
||||
{map(ipPool.networks, networkId => <Row>
|
||||
<Col mediumSize={11}>
|
||||
{renderXoItemFromId(networkId)}
|
||||
</Col>
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import _ from 'intl'
|
||||
import ActionBar, { Action } from 'action-bar'
|
||||
import ActionBar from 'action-bar'
|
||||
import React from 'react'
|
||||
import { forgetSr, rescanSr, reconnectAllHostsSr, disconnectAllHostsSr } from 'xo'
|
||||
|
||||
const SrActionBar = ({ sr }) => (
|
||||
<ActionBar
|
||||
actions={[
|
||||
{
|
||||
icon: 'refresh',
|
||||
label: 'srRescan',
|
||||
handler: rescanSr
|
||||
},
|
||||
{
|
||||
icon: 'sr-reconnect-all',
|
||||
label: 'srReconnectAll',
|
||||
handler: reconnectAllHostsSr
|
||||
},
|
||||
{
|
||||
icon: 'sr-disconnect-all',
|
||||
label: 'srDisconnectAll',
|
||||
handler: disconnectAllHostsSr
|
||||
},
|
||||
{
|
||||
icon: 'sr-forget',
|
||||
label: 'srForget',
|
||||
handler: forgetSr
|
||||
}
|
||||
]}
|
||||
display='icon'
|
||||
handlerParam={sr}
|
||||
>
|
||||
<Action
|
||||
handler={rescanSr}
|
||||
label={_('srRescan')}
|
||||
icon='refresh'
|
||||
/>
|
||||
<Action
|
||||
handler={reconnectAllHostsSr}
|
||||
label={_('srReconnectAll')}
|
||||
icon='sr-reconnect-all'
|
||||
/>
|
||||
<Action
|
||||
handler={disconnectAllHostsSr}
|
||||
label={_('srDisconnectAll')}
|
||||
icon='sr-disconnect-all'
|
||||
/>
|
||||
<Action
|
||||
handler={forgetSr}
|
||||
label={_('srForget')}
|
||||
icon='sr-forget'
|
||||
/>
|
||||
</ActionBar>
|
||||
param={sr}
|
||||
/>
|
||||
)
|
||||
export default SrActionBar
|
||||
|
||||
@@ -143,6 +143,7 @@ export default class Sr extends Component {
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
<Col>
|
||||
<NavTabs>
|
||||
|
||||
@@ -1,58 +1,9 @@
|
||||
import _ from 'intl'
|
||||
import Copiable from 'copiable'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import { addSubscriptions, connectStore, formatSize } from 'utils'
|
||||
import { deleteSr } from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { createSelector } from 'reselect'
|
||||
import { createSrUnhealthyVdiChainsLengthSubscription, deleteSr } from 'xo'
|
||||
import { flowRight, isEmpty, keys } from 'lodash'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: _('srUnhealthyVdiNameLabel'),
|
||||
itemRenderer: vdi => <span>{vdi.name_label}</span>,
|
||||
sortCriteria: vdi => vdi.name_label
|
||||
},
|
||||
{
|
||||
name: _('srUnhealthyVdiSize'),
|
||||
itemRenderer: vdi => formatSize(vdi.size),
|
||||
sortCriteria: vdi => vdi.size
|
||||
},
|
||||
{
|
||||
name: _('srUnhealthyVdiDepth'),
|
||||
itemRenderer: (vdi, chains) => chains[vdi.uuid],
|
||||
sortCriteria: (vdi, chains) => chains[vdi.uuid]
|
||||
}
|
||||
]
|
||||
|
||||
const UnhealthyVdiChains = flowRight(
|
||||
addSubscriptions(props => ({
|
||||
chains: createSrUnhealthyVdiChainsLengthSubscription(props.sr)
|
||||
})),
|
||||
connectStore(() => ({
|
||||
vdis: createGetObjectsOfType('VDI').pick(
|
||||
createSelector(
|
||||
(_, props) => props.chains,
|
||||
keys
|
||||
)
|
||||
)
|
||||
}))
|
||||
)(({ chains, vdis }) => isEmpty(vdis)
|
||||
? null
|
||||
: <div>
|
||||
<h3>{_('srUnhealthyVdiTitle')}</h3>
|
||||
<SortedTable
|
||||
collection={vdis}
|
||||
columns={COLUMNS}
|
||||
userData={chains}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default ({
|
||||
sr
|
||||
@@ -83,9 +34,4 @@ export default ({
|
||||
</table>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<UnhealthyVdiChains sr={sr} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -329,7 +329,7 @@ export default class User extends Component {
|
||||
<Col smallSize={10}>
|
||||
<form className='form-inline' id='changePassword'>
|
||||
<input
|
||||
autoComplete='off'
|
||||
autocomplete='off'
|
||||
className='form-control'
|
||||
onChange={this._handleOldPasswordChange}
|
||||
placeholder={formatMessage(messages.oldPasswordPlaceholder)}
|
||||
@@ -339,7 +339,7 @@ export default class User extends Component {
|
||||
/>
|
||||
{' '}
|
||||
<input type='password'
|
||||
autoComplete='off'
|
||||
autocomplete='off'
|
||||
className='form-control'
|
||||
onChange={this._handleNewPasswordChange}
|
||||
placeholder={formatMessage(messages.newPasswordPlaceholder)}
|
||||
@@ -348,7 +348,7 @@ export default class User extends Component {
|
||||
/>
|
||||
{' '}
|
||||
<input
|
||||
autoComplete='off'
|
||||
autocomplete='off'
|
||||
className='form-control'
|
||||
onChange={this._handleConfirmPasswordChange}
|
||||
placeholder={formatMessage(messages.confirmPasswordPlaceholder)}
|
||||
|
||||
53
src/xo-app/vm-group/action-bar.js
Normal file
53
src/xo-app/vm-group/action-bar.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import ActionBar from 'action-bar'
|
||||
import React from 'react'
|
||||
import { connectStore } from 'utils'
|
||||
import { includes } from 'lodash'
|
||||
import { isAdmin } from 'selectors'
|
||||
import {
|
||||
rebootVmGroup,
|
||||
startVmGroup,
|
||||
shutdownVmGroup
|
||||
} from 'xo'
|
||||
|
||||
const vmGroupActionBarByState = ({ isAdmin, vmGroup }) => {
|
||||
const actions = []
|
||||
if (vmGroup.allowed_operations.includes('start') || vmGroup.allowed_operations.length === 0) {
|
||||
actions.push({
|
||||
icon: 'vm-start',
|
||||
label: 'startVmLabel',
|
||||
handler: startVmGroup,
|
||||
pending: includes(vmGroup.current_operations, 'start')
|
||||
})
|
||||
}
|
||||
if (vmGroup.allowed_operations.includes('shutdown') || vmGroup.allowed_operations.length === 0) {
|
||||
actions.push({
|
||||
icon: 'vm-stop',
|
||||
label: 'stopVmLabel',
|
||||
handler: shutdownVmGroup,
|
||||
pending: includes(vmGroup.current_operations, 'shutdown')
|
||||
})
|
||||
}
|
||||
actions.push({
|
||||
icon: 'vm-reboot',
|
||||
label: 'rebootVmLabel',
|
||||
handler: rebootVmGroup,
|
||||
pending: includes(vmGroup.current_operations, 'shutdown') || includes(vmGroup.current_operations, 'start')
|
||||
})
|
||||
return <ActionBar
|
||||
actions={actions}
|
||||
display='icon'
|
||||
param={vmGroup}
|
||||
/>
|
||||
}
|
||||
|
||||
const VmGroupActionBar = connectStore({
|
||||
isAdmin
|
||||
})(({ isAdmin, vmGroup }) => {
|
||||
const ActionBar = vmGroupActionBarByState
|
||||
if (!ActionBar) {
|
||||
return <p>No action bar for state</p>
|
||||
}
|
||||
|
||||
return <ActionBar isAdmin={isAdmin} vmGroup={vmGroup} />
|
||||
})
|
||||
export default VmGroupActionBar
|
||||
131
src/xo-app/vm-group/index.js
Normal file
131
src/xo-app/vm-group/index.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import _ from 'intl'
|
||||
import BaseComponent from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import React, { cloneElement } from 'react'
|
||||
import {
|
||||
connectStore,
|
||||
routes
|
||||
} from 'utils'
|
||||
import {
|
||||
assign,
|
||||
forEach,
|
||||
map,
|
||||
pick
|
||||
} from 'lodash'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetObject } from 'selectors'
|
||||
import { editVmGroup } from 'xo'
|
||||
import { NavLink, NavTabs } from 'nav'
|
||||
import { Text } from 'editable'
|
||||
|
||||
import Page from '../page'
|
||||
import TabAdvanced from './tab-advanced'
|
||||
import TabGeneral from './tab-general'
|
||||
import TabManagement from './tab-management'
|
||||
import TabStats from './tab-stats'
|
||||
import VmGroupActionBar from './action-bar'
|
||||
// ===================================================================
|
||||
|
||||
@routes('general', {
|
||||
advanced: TabAdvanced,
|
||||
general: TabGeneral,
|
||||
management: TabManagement,
|
||||
stats: TabStats
|
||||
})
|
||||
@connectStore(() => {
|
||||
const getVmGroup = createGetObject()
|
||||
|
||||
return (state, props) => {
|
||||
const vmGroup = getVmGroup(state, props)
|
||||
if (!vmGroup) {
|
||||
return {}
|
||||
}
|
||||
const vms = {}
|
||||
forEach(vmGroup.$VMs, vmId => {
|
||||
const getVM = createGetObject(() => vmId)
|
||||
vms[vmId] = getVM(state, props)
|
||||
})
|
||||
return {
|
||||
vmGroup,
|
||||
vms
|
||||
}
|
||||
}
|
||||
})
|
||||
export default class VmGroup extends BaseComponent {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
|
||||
_setNameDescription = description => editVmGroup(this.props.vmGroup, {name_description: description})
|
||||
_setNameLabel = label => editVmGroup(this.props.vmGroup, {name_label: label})
|
||||
_getVmGroupState = () => {
|
||||
const states = map(this.props.vms, vm => vm.power_state)
|
||||
return (isEmpty(states)
|
||||
? 'busy'
|
||||
: states.indexOf('Halted') === -1
|
||||
? states[0]
|
||||
: states.indexOf('Running') === -1
|
||||
? states[0]
|
||||
: 'busy').toLowerCase()
|
||||
}
|
||||
|
||||
header () {
|
||||
const { vmGroup, vms } = this.props
|
||||
if (!vmGroup) {
|
||||
return <Icon icon='loading' />
|
||||
}
|
||||
return <Container>
|
||||
<Row>
|
||||
<Col mediumSize={6} className='header-title'>
|
||||
<h2>
|
||||
<Icon icon={isEmpty(vms) ? `vm-halted` : `vm-${this._getVmGroupState()}`} />
|
||||
{' '}
|
||||
<Text value={vmGroup.name_label} onChange={this._setNameLabel} />
|
||||
</h2>
|
||||
<span>
|
||||
<Text
|
||||
value={vmGroup.name_description}
|
||||
onChange={this._setNameDescription}
|
||||
/>
|
||||
<span className='text-muted'>
|
||||
{' '}
|
||||
</span>
|
||||
</span>
|
||||
</Col>
|
||||
<Col mediumSize={6} className='text-xs-center'>
|
||||
<div>
|
||||
<VmGroupActionBar vmGroup={vmGroup} />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
<Col>
|
||||
<NavTabs>
|
||||
<NavLink to={`/vm-group/${vmGroup.id}/general`}>{_('generalTabName')}</NavLink>
|
||||
<NavLink to={`/vm-group/${vmGroup.id}/stats`}>{_('statsTabName')}</NavLink>
|
||||
<NavLink to={`/vm-group/${vmGroup.id}/management`}>{_('managementTabName')}</NavLink>
|
||||
<NavLink to={`/vm-group/${vmGroup.id}/advanced`}>{_('advancedTabName')}</NavLink>
|
||||
</NavTabs>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
}
|
||||
|
||||
render () {
|
||||
const { container, vmGroup } = this.props
|
||||
|
||||
if (!vmGroup) {
|
||||
return <h1>{_('statusLoading')}</h1>
|
||||
}
|
||||
|
||||
const childProps = assign(pick(this.props, [
|
||||
'vmGroup',
|
||||
'vms'
|
||||
]))
|
||||
return <Page header={this.header()} title={`${vmGroup.name_label}${container ? ` (${container.name_label})` : ''}`}>
|
||||
{cloneElement(this.props.children, { ...childProps })}
|
||||
</Page>
|
||||
}
|
||||
}
|
||||
55
src/xo-app/vm-group/tab-advanced.js
Normal file
55
src/xo-app/vm-group/tab-advanced.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Copiable from 'copiable'
|
||||
import React from 'react'
|
||||
import TabButton from 'tab-button'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { deleteVmGroup } from 'xo'
|
||||
import { noop } from 'utils'
|
||||
|
||||
export default class TabAdvanced extends Component {
|
||||
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
|
||||
_deleteVmGroup = (vmGroup, vms) => {
|
||||
deleteVmGroup(vmGroup, vms).then(
|
||||
() => this.context.router.push('home?s=&t=VmGroup'),
|
||||
noop
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { vmGroup, vms } = this.props
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={() => this._deleteVmGroup(vmGroup, vms)}
|
||||
icon='vm-delete'
|
||||
labelId='vmRemoveButton'
|
||||
/>
|
||||
</Col>
|
||||
<div>
|
||||
<h3>{_('xenSettingsLabel')}</h3>
|
||||
{ vmGroup &&
|
||||
<table className='table'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{_('uuid')}</th>
|
||||
<Copiable tagName='td'>
|
||||
{vmGroup.id}
|
||||
</Copiable>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
67
src/xo-app/vm-group/tab-general.js
Normal file
67
src/xo-app/vm-group/tab-general.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import _ from 'intl'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import reduce from 'lodash/reduce'
|
||||
import React from 'react'
|
||||
import size from 'lodash/size'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { getObject } from 'selectors'
|
||||
|
||||
export default connectStore(() => {
|
||||
const getMemoryTotal = (state, props) => {
|
||||
const vdiIds = new Set()
|
||||
forEach(props.vms, vm => forEach(vm.$VBDs, vbdId => vdiIds.add(getObject(state, vbdId).VDI)))
|
||||
return reduce(Array.from(vdiIds), (sum, vdiId) => {
|
||||
const vdi = getObject(state, vdiId)
|
||||
return vdi !== undefined
|
||||
? sum + vdi.size
|
||||
: sum
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const getMemoryDynamicTotal = props => reduce(props.vms, (sum, vm) => vm.memory.dynamic[1] + sum, 0)
|
||||
|
||||
const getNbCPU = props => reduce(props.vms, (sum, vm) => vm.CPUs.number + sum, 0)
|
||||
|
||||
return (state, props) => ({
|
||||
memoryTotal: getMemoryTotal(state, props),
|
||||
memoryDynamical: getMemoryDynamicTotal(props),
|
||||
nbCPU: getNbCPU(props)
|
||||
})
|
||||
})(({ vms, memoryTotal, memoryDynamical, nbCPU, vmGroup }) => {
|
||||
return (
|
||||
<Container>
|
||||
<br />
|
||||
<div>
|
||||
<Row className='text-xs-center'>
|
||||
<Col mediumSize={3}>
|
||||
<h2>{size(vms)}x <Icon icon='vm' size='lg' /></h2>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<h2>{nbCPU}x <Icon icon='cpu' size='lg' /></h2>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<h2 className='form-inline'>
|
||||
{formatSize(memoryDynamical)}
|
||||
<span><Icon icon='memory' size='lg' /></span>
|
||||
</h2>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<h2>{formatSize(memoryTotal)} <Icon icon='disk' size='lg' /></h2>
|
||||
</Col>
|
||||
</Row>
|
||||
{isEmpty(vmGroup.current_operations)
|
||||
? null
|
||||
: <Row className='text-xs-center'>
|
||||
<Col>
|
||||
<h4>{_('vmGroupCurrentStatus')}{' '}{map(vmGroup.current_operations)[0]}</h4>
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
})
|
||||
145
src/xo-app/vm-group/tab-management.js
Normal file
145
src/xo-app/vm-group/tab-management.js
Normal file
@@ -0,0 +1,145 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import Component from 'base-component'
|
||||
import DragNDropOrder from 'drag-n-drop-order'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { editVm, removeAppliance } from 'xo'
|
||||
import { Row, Col } from 'grid'
|
||||
import { SelectVm } from 'select-objects'
|
||||
|
||||
const VM_COLUMNS = [
|
||||
{
|
||||
name: _('vmGroupLabel'),
|
||||
itemRenderer: vm => (<span><Tooltip
|
||||
content={isEmpty(vm.current_operations)
|
||||
? _(`powerState${vm.power_state}`)
|
||||
: <div>{_(`powerState${vm.power_state}`)}{' ('}{map(vm.current_operations)[0]}{')'}</div>
|
||||
}
|
||||
>
|
||||
{isEmpty(vm.current_operations)
|
||||
? <Icon icon={`${vm.power_state.toLowerCase()}`} />
|
||||
: <Icon icon='busy' />
|
||||
}
|
||||
</Tooltip>
|
||||
|
||||
<Link to={`/vms/${vm.id}`}>
|
||||
{vm.name_label}
|
||||
</Link>
|
||||
</span>),
|
||||
sortCriteria: vm => vm.name_label
|
||||
},
|
||||
{
|
||||
name: _('vmGroupDescription'),
|
||||
itemRenderer: vm => vm.name_description,
|
||||
sortCriteria: vm => vm.name_description
|
||||
},
|
||||
{
|
||||
name: _('vmGroupActions'),
|
||||
itemRenderer: vm => (
|
||||
<ActionRowButton
|
||||
btnStyle='danger'
|
||||
handler={(vm) => removeAppliance(vm)}
|
||||
handlerParam={vm}
|
||||
icon='delete'
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
export default class TabManagement extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
attachVm: false,
|
||||
bootOrder: false
|
||||
}
|
||||
}
|
||||
|
||||
parseBootOrder = vms => {
|
||||
// FIXME missing translation
|
||||
var previousOrder = vms[Object.keys(vms)[0]].order
|
||||
var toggleActive = false
|
||||
const orderVms = sortBy(vms, vm => {
|
||||
if (vm.order !== previousOrder) toggleActive = true
|
||||
return vm.order
|
||||
})
|
||||
const order = []
|
||||
forEach(orderVms, vm => {
|
||||
order.push({id: vm.id, text: vm.name_label})
|
||||
})
|
||||
return {order, toggleActive}
|
||||
}
|
||||
|
||||
setVmBootOrder = (vms, order, toggleActive) => {
|
||||
var orderValue = 0
|
||||
forEach(order, (vm, key) => {
|
||||
editVm(vms[vm.id], { order: toggleActive ? orderValue : 0 })
|
||||
orderValue += 1
|
||||
})
|
||||
}
|
||||
|
||||
_addVm = () => {
|
||||
forEach(this.state.vmsToAdd, vm => editVm(vm, { appliance: this.props.vmGroup.id }))
|
||||
}
|
||||
_selectVm = vmsToAdd => this.setState({vmsToAdd})
|
||||
_toggleBootOrder = () => this.setState({
|
||||
bootOrder: !this.state.bootOrder,
|
||||
attachVm: false
|
||||
})
|
||||
_toggleNewVm = () => this.setState({
|
||||
attachVm: !this.state.attachVm,
|
||||
bootOrder: false
|
||||
})
|
||||
_vmPredicate = vm => vm.appliance === null
|
||||
|
||||
render () {
|
||||
const { vms } = this.props
|
||||
const { attachVm, bootOrder } = this.state
|
||||
return isEmpty(vms)
|
||||
? <form id='attachVm'>
|
||||
<SelectVm multi onChange={this._selectVm} predicate={this._vmPredicate} required />
|
||||
<span className='pull-right'>
|
||||
<ActionButton form='attachVm' icon='add' btnStyle='primary' handler={this._addVm}>{_('add')}</ActionButton>
|
||||
</span>
|
||||
</form>
|
||||
: (<div>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
<TabButton
|
||||
btnStyle={attachVm ? 'info' : 'primary'}
|
||||
handler={this._toggleNewVm}
|
||||
icon='add'
|
||||
labelId='attachVmButton'
|
||||
/>
|
||||
<TabButton
|
||||
btnStyle={bootOrder ? 'info' : 'primary'}
|
||||
handler={this._toggleBootOrder}
|
||||
icon='sort'
|
||||
labelId='vmsBootOrder'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{bootOrder && <div><DragNDropOrder parseOrderParam={vms} parseOrder={this.parseBootOrder} setOrder={this.setVmBootOrder} toggleItems={false} onClose={this._toggleBootOrder} /><hr /></div>}
|
||||
{attachVm && <div>
|
||||
<form id='attachVm'>
|
||||
<SelectVm multi onChange={this._selectVm} predicate={this._vmPredicate} required />
|
||||
<span className='pull-right'>
|
||||
<ActionButton form='attachVm' icon='add' btnStyle='primary' handler={this._addVm}>{_('add')}</ActionButton>
|
||||
</span>
|
||||
</form>
|
||||
</div>}
|
||||
<SortedTable collection={vms} columns={VM_COLUMNS} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
138
src/xo-app/vm-group/tab-stats.js
Normal file
138
src/xo-app/vm-group/tab-stats.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import filter from 'lodash/filter'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { Toggle } from 'form'
|
||||
import { fetchVmStats } from 'xo'
|
||||
import {
|
||||
VmGroupCpuLineChart,
|
||||
VmGroupMemoryLineChart,
|
||||
VmGroupVifLineChart,
|
||||
VmGroupXvdLineChart
|
||||
} from 'xo-line-chart'
|
||||
|
||||
export default class TabStats extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state.useCombinedValues = false
|
||||
}
|
||||
|
||||
loop (vmGroup = this.props.vmGroup) {
|
||||
if (this.cancel) {
|
||||
this.cancel()
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
this.cancel = () => { cancelled = true }
|
||||
|
||||
Promise.all(map(filter(this.props.vms, vm => vm.power_state === 'Running'), vm =>
|
||||
fetchVmStats(vm, this.state.granularity).then(
|
||||
stats => ({
|
||||
vm: vm.name_label,
|
||||
...stats
|
||||
})
|
||||
)
|
||||
)).then(stats => {
|
||||
if (cancelled || !stats[0]) {
|
||||
return
|
||||
}
|
||||
this.cancel = null
|
||||
|
||||
clearTimeout(this.timeout)
|
||||
this.setState({
|
||||
stats,
|
||||
selectStatsLoading: false
|
||||
}, () => {
|
||||
this.timeout = setTimeout(this.loop, stats[0].interval * 1000)
|
||||
})
|
||||
})
|
||||
}
|
||||
loop = ::this.loop
|
||||
|
||||
handleSelectStats (event) {
|
||||
const granularity = event.target.value
|
||||
clearTimeout(this.timeout)
|
||||
|
||||
this.setState({
|
||||
granularity,
|
||||
selectStatsLoading: true
|
||||
}, this.loop)
|
||||
}
|
||||
handleSelectStats = ::this.handleSelectStats
|
||||
|
||||
componentWillMount () {
|
||||
this.loop()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearTimeout(this.timeout)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
granularity,
|
||||
selectStatsLoading,
|
||||
stats,
|
||||
useCombinedValues
|
||||
} = this.state
|
||||
return !stats
|
||||
? <p>No stats.</p>
|
||||
: process.env.XOA_PLAN > 2
|
||||
? <Container>
|
||||
<Row>
|
||||
<Col mediumSize={5}>
|
||||
<div className='form-group'>
|
||||
<Tooltip content={_('useStackedValuesOnStats')}>
|
||||
<Toggle value={useCombinedValues} onChange={this.linkState('useCombinedValues')} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={1}>
|
||||
{selectStatsLoading && (
|
||||
<div className='text-xs-right'>
|
||||
<Icon icon='loading' size={2} />
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='btn-tab'>
|
||||
<select className='form-control' onChange={this.handleSelectStats} defaultValue={granularity} >
|
||||
{_('statLastTenMinutes', message => <option value='seconds'>{message}</option>)}
|
||||
{_('statLastTwoHours', message => <option value='minutes'>{message}</option>)}
|
||||
{_('statLastWeek', message => <option value='hours'>{message}</option>)}
|
||||
{_('statLastYear', message => <option value='days'>{message}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'><Icon icon='cpu' size={1} /> {_('statsCpu')}</h5>
|
||||
<VmGroupCpuLineChart addSumSeries={useCombinedValues} data={stats} />
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'><Icon icon='memory' size={1} /> {_('statsMemory')}</h5>
|
||||
<VmGroupMemoryLineChart addSumSeries={useCombinedValues} data={stats} />
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<hr />
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'><Icon icon='network' size={1} /> {_('statsNetwork')}</h5>
|
||||
<VmGroupVifLineChart key={useCombinedValues ? 'stacked' : 'unstacked'} addSumSeries={useCombinedValues} data={stats} />
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<h5 className='text-xs-center'><Icon icon='disk' size={1} /> {_('statDisk')}</h5>
|
||||
<VmGroupXvdLineChart addSumSeries={useCombinedValues} data={stats} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
: <Container><Upgrade place='hostStats' available={3} /></Container>
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import _ from 'intl'
|
||||
import ActionBar, { Action } from 'action-bar'
|
||||
import ActionBar from 'action-bar'
|
||||
import React from 'react'
|
||||
import { addSubscriptions, connectStore } from 'utils'
|
||||
import { find, includes } from 'lodash'
|
||||
import { createSelector, getCheckPermissions, getUser } from 'selectors'
|
||||
import { connectStore } from 'utils'
|
||||
import { includes } from 'lodash'
|
||||
import { isAdmin } from 'selectors'
|
||||
import {
|
||||
cloneVm,
|
||||
copyVm,
|
||||
@@ -13,160 +12,142 @@ import {
|
||||
resumeVm,
|
||||
snapshotVm,
|
||||
startVm,
|
||||
stopVm,
|
||||
subscribeResourceSets
|
||||
stopVm
|
||||
} from 'xo'
|
||||
|
||||
const vmActionBarByState = {
|
||||
Running: ({ vm, isSelfUser, canAdministrate }) => (
|
||||
Running: ({ isAdmin, vm }) => (
|
||||
<ActionBar
|
||||
display='icon'
|
||||
handlerParam={vm}
|
||||
>
|
||||
<Action
|
||||
handler={stopVm}
|
||||
icon='vm-stop'
|
||||
label={_('stopVmLabel')}
|
||||
pending={includes(vm.current_operations, 'clean_shutdown')}
|
||||
/>
|
||||
<Action
|
||||
handler={restartVm}
|
||||
icon='vm-reboot'
|
||||
label={_('rebootVmLabel')}
|
||||
pending={includes(vm.current_operations, 'clean_reboot')}
|
||||
/>
|
||||
{!isSelfUser && <Action
|
||||
handler={migrateVm}
|
||||
icon='vm-migrate'
|
||||
label={_('migrateVmLabel')}
|
||||
pending={
|
||||
includes(vm.current_operations, 'migrate_send') ||
|
||||
includes(vm.current_operations, 'pool_migrate')
|
||||
actions={[
|
||||
{
|
||||
icon: 'vm-stop',
|
||||
label: 'stopVmLabel',
|
||||
handler: stopVm,
|
||||
pending: includes(vm.current_operations, 'clean_shutdown')
|
||||
},
|
||||
{
|
||||
icon: 'vm-reboot',
|
||||
label: 'rebootVmLabel',
|
||||
handler: restartVm,
|
||||
pending: includes(vm.current_operations, 'clean_reboot')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-migrate',
|
||||
label: 'migrateVmLabel',
|
||||
handler: migrateVm,
|
||||
pending:
|
||||
includes(vm.current_operations, 'migrate_send') ||
|
||||
includes(vm.current_operations, 'pool_migrate')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-snapshot',
|
||||
label: 'snapshotVmLabel',
|
||||
handler: snapshotVm,
|
||||
pending: includes(vm.current_operations, 'snapshot')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'export',
|
||||
label: 'exportVmLabel',
|
||||
handler: exportVm,
|
||||
pending: includes(vm.current_operations, 'export')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-copy',
|
||||
label: 'copyVmLabel',
|
||||
handler: copyVm,
|
||||
pending: includes(vm.current_operations, 'copy')
|
||||
}
|
||||
/>}
|
||||
{!isSelfUser && <Action
|
||||
handler={snapshotVm}
|
||||
icon='vm-snapshot'
|
||||
label={_('snapshotVmLabel')}
|
||||
pending={includes(vm.current_operations, 'snapshot')}
|
||||
/>}
|
||||
{!isSelfUser && canAdministrate && <Action
|
||||
handler={exportVm}
|
||||
icon='export'
|
||||
label={_('exportVmLabel')}
|
||||
pending={includes(vm.current_operations, 'export')}
|
||||
/>}
|
||||
{!isSelfUser && canAdministrate && <Action
|
||||
handler={copyVm}
|
||||
icon='vm-copy'
|
||||
label={_('copyVmLabel')}
|
||||
pending={includes(vm.current_operations, 'copy')}
|
||||
/>}
|
||||
</ActionBar>
|
||||
),
|
||||
Halted: ({ vm, isSelfUser, canAdministrate }) => (
|
||||
<ActionBar
|
||||
]}
|
||||
display='icon'
|
||||
handlerParam={vm}
|
||||
>
|
||||
<Action
|
||||
handler={startVm}
|
||||
icon='vm-start'
|
||||
label={_('startVmLabel')}
|
||||
pending={includes(vm.current_operations, 'start')}
|
||||
/>
|
||||
{!isSelfUser && canAdministrate && <Action
|
||||
handler={cloneVm}
|
||||
icon='vm-fast-clone'
|
||||
label={_('fastCloneVmLabel')}
|
||||
pending={includes(vm.current_operations, 'clone')}
|
||||
/>}
|
||||
{!isSelfUser && <Action
|
||||
handler={migrateVm}
|
||||
icon='vm-migrate'
|
||||
label={_('migrateVmLabel')}
|
||||
pending={includes(vm.current_operations, 'pool_migrate')}
|
||||
/>}
|
||||
{!isSelfUser && <Action
|
||||
handler={snapshotVm}
|
||||
icon='vm-snapshot'
|
||||
label={_('snapshotVmLabel')}
|
||||
pending={includes(vm.current_operations, 'snapshot')}
|
||||
/>}
|
||||
{!isSelfUser && canAdministrate && <Action
|
||||
handler={exportVm}
|
||||
icon='export'
|
||||
label={_('exportVmLabel')}
|
||||
pending={includes(vm.current_operations, 'export')}
|
||||
/>}
|
||||
{!isSelfUser && canAdministrate && <Action
|
||||
handler={copyVm}
|
||||
icon='vm-copy'
|
||||
label={_('copyVmLabel')}
|
||||
pending={includes(vm.current_operations, 'copy')}
|
||||
/>}
|
||||
</ActionBar>
|
||||
param={vm}
|
||||
/>
|
||||
),
|
||||
Suspended: ({ vm, isSelfUser, canAdministrate }) => (
|
||||
Halted: ({ isAdmin, vm }) => (
|
||||
<ActionBar
|
||||
actions={[
|
||||
{
|
||||
icon: 'vm-start',
|
||||
label: 'startVmLabel',
|
||||
handler: startVm,
|
||||
pending: includes(vm.current_operations, 'start')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-fast-clone',
|
||||
label: 'fastCloneVmLabel',
|
||||
handler: cloneVm,
|
||||
pending: includes(vm.current_operations, 'clone')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-migrate',
|
||||
label: 'migrateVmLabel',
|
||||
handler: migrateVm,
|
||||
pending: includes(vm.current_operations, 'pool_migrate')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-snapshot',
|
||||
label: 'snapshotVmLabel',
|
||||
handler: snapshotVm,
|
||||
pending: includes(vm.current_operations, 'snapshot')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'export',
|
||||
label: 'exportVmLabel',
|
||||
handler: exportVm,
|
||||
pending: includes(vm.current_operations, 'export')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-copy',
|
||||
label: 'copyVmLabel',
|
||||
handler: copyVm,
|
||||
pending: includes(vm.current_operations, 'copy')
|
||||
}
|
||||
]}
|
||||
display='icon'
|
||||
handlerParam={vm}
|
||||
>
|
||||
<Action
|
||||
handler={resumeVm}
|
||||
icon='vm-start'
|
||||
label={_('resumeVmLabel')}
|
||||
pending={includes(vm.current_operations, 'start')}
|
||||
/>
|
||||
{!isSelfUser && <Action
|
||||
handler={snapshotVm}
|
||||
icon='vm-snapshot'
|
||||
label={_('snapshotVmLabel')}
|
||||
pending={includes(vm.current_operations, 'snapshot')}
|
||||
/>}
|
||||
{!isSelfUser && canAdministrate && <Action
|
||||
handler={exportVm}
|
||||
icon='export'
|
||||
label={_('exportVmLabel')}
|
||||
pending={includes(vm.current_operations, 'export')}
|
||||
/>}
|
||||
{!isSelfUser && canAdministrate && <Action
|
||||
handler={copyVm}
|
||||
icon='vm-copy'
|
||||
label={_('copyVmLabel')}
|
||||
pending={includes(vm.current_operations, 'copy')}
|
||||
/>}
|
||||
</ActionBar>
|
||||
param={vm}
|
||||
/>
|
||||
),
|
||||
Suspended: ({ isAdmin, vm }) => (
|
||||
<ActionBar
|
||||
actions={[
|
||||
{
|
||||
icon: 'vm-start',
|
||||
label: 'resumeVmLabel',
|
||||
handler: resumeVm,
|
||||
pending: includes(vm.current_operations, 'start')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-snapshot',
|
||||
label: 'snapshotVmLabel',
|
||||
handler: snapshotVm,
|
||||
pending: includes(vm.current_operations, 'snapshot')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'export',
|
||||
label: 'exportVmLabel',
|
||||
handler: exportVm,
|
||||
pending: includes(vm.current_operations, 'export')
|
||||
},
|
||||
(isAdmin || !vm.resourceSet) && {
|
||||
icon: 'vm-copy',
|
||||
label: 'copyVmLabel',
|
||||
handler: copyVm,
|
||||
pending: includes(vm.current_operations, 'copy')
|
||||
}
|
||||
]}
|
||||
display='icon'
|
||||
param={vm}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const VmActionBar = addSubscriptions(() => ({
|
||||
resourceSets: subscribeResourceSets
|
||||
}))(connectStore(() => ({
|
||||
checkPermissions: getCheckPermissions,
|
||||
userId: createSelector(getUser, user => user.id)
|
||||
}))(({ checkPermissions, vm, userId, resourceSets }) => {
|
||||
// Is the user in the same resource set as the VM
|
||||
const _getIsSelfUser = createSelector(
|
||||
() => resourceSets,
|
||||
resourceSets =>
|
||||
vm.resourceSet && includes(
|
||||
find(resourceSets, { id: vm.resourceSet }).subjects,
|
||||
userId
|
||||
)
|
||||
)
|
||||
|
||||
const _getCanAdministrate = createSelector(
|
||||
() => checkPermissions,
|
||||
() => vm.id,
|
||||
(check, vmId) => check(vmId, 'administrate')
|
||||
)
|
||||
|
||||
const VmActionBar = connectStore({
|
||||
isAdmin
|
||||
})(({ isAdmin, vm }) => {
|
||||
const ActionBar = vmActionBarByState[vm.power_state]
|
||||
if (!ActionBar) {
|
||||
return <p>No action bar for state {vm.power_state}</p>
|
||||
}
|
||||
|
||||
return <ActionBar vm={vm} isSelfUser={_getIsSelfUser()} canAdministrate={_getCanAdministrate()} />
|
||||
}))
|
||||
return <ActionBar isAdmin={isAdmin} vm={vm} />
|
||||
})
|
||||
export default VmActionBar
|
||||
|
||||
@@ -230,6 +230,7 @@ export default class Vm extends BaseComponent {
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row>
|
||||
<Col>
|
||||
<NavTabs>
|
||||
|
||||
@@ -145,21 +145,19 @@ class CoresPerSocket extends Component {
|
||||
onChange={this._onChange}
|
||||
value={selectedCoresPerSocket || ''}
|
||||
>
|
||||
{_('vmChooseCoresPerSocket', message => <option key='none' value=''>{message}</option>)}
|
||||
{_('vmChooseCoresPerSocket', message => <option value=''>{message}</option>)}
|
||||
{this._selectedValueIsNotInOptions() &&
|
||||
_('vmCoresPerSocketIncorrectValue', message => <option key='incorrect' value={selectedCoresPerSocket}> {message}</option>)
|
||||
_('vmCoresPerSocketIncorrectValue', message => <option value={selectedCoresPerSocket}> {message}</option>)
|
||||
}
|
||||
{map(
|
||||
options,
|
||||
coresPerSocket => <option
|
||||
key={coresPerSocket}
|
||||
value={coresPerSocket}
|
||||
>
|
||||
{_('vmCoresPerSocket', {
|
||||
coresPerSocket => _(
|
||||
'vmCoresPerSocket', {
|
||||
nSockets: vm.CPUs.number / coresPerSocket,
|
||||
nCores: coresPerSocket
|
||||
})}
|
||||
</option>
|
||||
},
|
||||
message => <option key={coresPerSocket} value={coresPerSocket}>{message}</option>
|
||||
)
|
||||
)}
|
||||
</select>
|
||||
{' '}
|
||||
@@ -394,7 +392,7 @@ export default ({
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{_('osKernel')}</th>
|
||||
<td>{(vm.os_version && vm.os_version.uname) || _('unknownOsKernel')}</td>
|
||||
<td>{vm.os_version ? vm.os_version.uname ? vm.os_version.uname : _('unknownOsKernel') : _('unknownOsKernel')}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -150,6 +150,7 @@ export default class TabConsole extends Component {
|
||||
scale={scale}
|
||||
url={resolveUrl(`consoles/${vm.id}`)}
|
||||
/>
|
||||
{!minimalLayout && <p><em><Icon icon='info' /> <a href='https://bugs.xenserver.org/browse/XSO-650' target='_blank'>{_('tipLabel')} {_('tipConsoleLabel')}</a></em></p>}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -2,9 +2,8 @@ import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import Component from 'base-component'
|
||||
import DragNDropOrder from 'drag-n-drop-order'
|
||||
import forEach from 'lodash/forEach'
|
||||
import HTML5Backend from 'react-dnd-html5-backend'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import IsoDevice from 'iso-device'
|
||||
import Link from 'link'
|
||||
@@ -18,7 +17,6 @@ import TabButton from 'tab-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createSelector } from 'selectors'
|
||||
import { DragDropContext, DragSource, DropTarget } from 'react-dnd'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { noop } from 'utils'
|
||||
import { SelectSr, SelectVdi } from 'select-objects'
|
||||
@@ -41,26 +39,6 @@ import {
|
||||
setVmBootOrder
|
||||
} from 'xo'
|
||||
|
||||
const parseBootOrder = bootOrder => {
|
||||
// FIXME missing translation
|
||||
const bootOptions = {
|
||||
c: 'Hard-Drive',
|
||||
d: 'DVD-Drive',
|
||||
n: 'Network'
|
||||
}
|
||||
const order = []
|
||||
if (bootOrder) {
|
||||
for (const id of bootOrder) {
|
||||
if (id in bootOptions) {
|
||||
order.push({id, text: bootOptions[id], active: true})
|
||||
delete bootOptions[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
forEach(bootOptions, (text, id) => { order.push({id, text, active: false}) })
|
||||
return order
|
||||
}
|
||||
|
||||
@injectIntl
|
||||
@propTypes({
|
||||
onClose: propTypes.func,
|
||||
@@ -196,129 +174,6 @@ class AttachDisk extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const orderItemSource = {
|
||||
beginDrag: props => ({
|
||||
id: props.id,
|
||||
index: props.index
|
||||
})
|
||||
}
|
||||
|
||||
const orderItemTarget = {
|
||||
hover: (props, monitor, component) => {
|
||||
const dragIndex = monitor.getItem().index
|
||||
const hoverIndex = props.index
|
||||
|
||||
if (dragIndex === hoverIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
props.move(dragIndex, hoverIndex)
|
||||
monitor.getItem().index = hoverIndex
|
||||
}
|
||||
}
|
||||
|
||||
@DropTarget('orderItem', orderItemTarget, connect => ({
|
||||
connectDropTarget: connect.dropTarget()
|
||||
}))
|
||||
@DragSource('orderItem', orderItemSource, (connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging()
|
||||
}))
|
||||
@propTypes({
|
||||
connectDragSource: propTypes.func.isRequired,
|
||||
connectDropTarget: propTypes.func.isRequired,
|
||||
index: propTypes.number.isRequired,
|
||||
isDragging: propTypes.bool.isRequired,
|
||||
id: propTypes.any.isRequired,
|
||||
item: propTypes.object.isRequired,
|
||||
move: propTypes.func.isRequired
|
||||
})
|
||||
class OrderItem extends Component {
|
||||
_toggle = checked => {
|
||||
const { item } = this.props
|
||||
item.active = checked
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { item, connectDragSource, connectDropTarget } = this.props
|
||||
return connectDragSource(connectDropTarget(
|
||||
<li className='list-group-item'>
|
||||
<Icon icon='grab' />
|
||||
{' '}
|
||||
<Icon icon='grab' />
|
||||
{' '}
|
||||
{item.text}
|
||||
<span className='pull-right'>
|
||||
<Toggle value={item.active} onChange={this._toggle} />
|
||||
</span>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
onClose: propTypes.func,
|
||||
vm: propTypes.object.isRequired
|
||||
})
|
||||
@DragDropContext(HTML5Backend)
|
||||
class BootOrder extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
const { vm } = props
|
||||
const order = parseBootOrder(vm.boot && vm.boot.order)
|
||||
this.state = {order}
|
||||
}
|
||||
|
||||
_moveOrderItem = (dragIndex, hoverIndex) => {
|
||||
const order = this.state.order.slice()
|
||||
const dragItem = order.splice(dragIndex, 1)
|
||||
if (dragItem.length) {
|
||||
order.splice(hoverIndex, 0, dragItem.pop())
|
||||
this.setState({order})
|
||||
}
|
||||
}
|
||||
|
||||
_reset = () => {
|
||||
const { vm } = this.props
|
||||
const order = parseBootOrder(vm.boot && vm.boot.order)
|
||||
this.setState({order})
|
||||
}
|
||||
|
||||
_save = () => {
|
||||
const { vm, onClose = noop } = this.props
|
||||
const { order: newOrder } = this.state
|
||||
let order = ''
|
||||
forEach(newOrder, item => { item.active && (order += item.id) })
|
||||
return setVmBootOrder(vm, order)
|
||||
.then(onClose)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { order } = this.state
|
||||
|
||||
return <form>
|
||||
<ul>
|
||||
{map(order, (item, index) => <OrderItem
|
||||
key={index}
|
||||
index={index}
|
||||
id={item.id}
|
||||
// FIXME missing translation
|
||||
item={item}
|
||||
move={this._moveOrderItem}
|
||||
/>)}
|
||||
</ul>
|
||||
<fieldset className='form-inline'>
|
||||
<span className='pull-right'>
|
||||
<ActionButton icon='save' btnStyle='primary' handler={this._save}>{_('saveBootOption')}</ActionButton>
|
||||
{' '}
|
||||
<ActionButton icon='reset' handler={this._reset}>{_('resetBootOption')}</ActionButton>
|
||||
</span>
|
||||
</fieldset>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
||||
class MigrateVdiModalBody extends Component {
|
||||
get value () {
|
||||
return this.state
|
||||
@@ -360,6 +215,34 @@ export default class TabDisks extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
parseBootOrder = bootOrder => {
|
||||
// FIXME missing translation
|
||||
const bootOptions = {
|
||||
c: 'Hard-Drive',
|
||||
d: 'DVD-Drive',
|
||||
n: 'Network'
|
||||
}
|
||||
const order = []
|
||||
if (bootOrder) {
|
||||
for (const id of bootOrder) {
|
||||
if (id in bootOptions) {
|
||||
order.push({id, text: bootOptions[id], active: true})
|
||||
delete bootOptions[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
forEach(bootOptions, (text, id) => { order.push({id, text, active: false}) })
|
||||
return {order}
|
||||
}
|
||||
|
||||
setVmBootOrder = (_, newOrder) => {
|
||||
const { vm } = this.props
|
||||
let order = ''
|
||||
forEach(newOrder, item => { item.active && (order += item.id) })
|
||||
return setVmBootOrder(vm, order)
|
||||
.then(noop)
|
||||
}
|
||||
|
||||
_toggleNewDisk = () => this.setState({
|
||||
newDisk: !this.state.newDisk,
|
||||
attachDisk: false,
|
||||
@@ -432,8 +315,8 @@ export default class TabDisks extends Component {
|
||||
<Row>
|
||||
<Col>
|
||||
{newDisk && <div><NewDisk vm={vm} onClose={this._toggleNewDisk} /><hr /></div>}
|
||||
{attachDisk && <div><AttachDisk vm={vm} vbds={vbds} onClose={this._toggleAttachDisk} /><hr /></div>}
|
||||
{bootOrder && <div><BootOrder vm={vm} onClose={this._toggleBootOrder} /><hr /></div>}
|
||||
{attachDisk && <div><AttachDisk vm={vm} vbds={vbds} /><hr /></div>}
|
||||
{bootOrder && <div><DragNDropOrder parseOrderParam={vm.boot && vm.boot.order} parseOrder={this.parseBootOrder} setOrder={this.setVmBootOrder} toggleItems onClose={this._toggleBootOrder} /><hr /></div>}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
|
||||
@@ -83,12 +83,12 @@ export default connectStore(() => {
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<BlockLink to={`/vms/${vm.id}/network`}>
|
||||
{vm.addresses && vm.addresses['0/ip']
|
||||
? <Copiable tagName='p'>
|
||||
{vm.addresses['0/ip']}
|
||||
</Copiable>
|
||||
: <p>{_('noIpv4Record')}</p>
|
||||
}
|
||||
<Copiable tagName='p'>
|
||||
{vm.addresses && vm.addresses['0/ip']
|
||||
? vm.addresses['0/ip']
|
||||
: _('noIpv4Record')
|
||||
}
|
||||
</Copiable>
|
||||
</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
|
||||
@@ -403,7 +403,7 @@ export default class TabNetwork extends BaseComponent {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(vm.VIFs, vif => <VifItem key={vif} vifId={vif} isVmRunning={isVmRunning(vm)} resourceSet={vm.resourceSet} />)}
|
||||
{map(vm.VIFs, vif => <VifItem vifId={vif} isVmRunning={isVmRunning(vm)} resourceSet={vm.resourceSet} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
{vm.addresses && !isEmpty(vm.addresses)
|
||||
|
||||
@@ -148,9 +148,9 @@ export class XosanVolumesTable extends Component {
|
||||
const _findLatestTemplate = templates => {
|
||||
let latestTemplate = templates[0]
|
||||
|
||||
forEach(templates, template => {
|
||||
if (compareVersions(template.version, latestTemplate.version) > 0) {
|
||||
latestTemplate = template
|
||||
forEach(templates, pack => {
|
||||
if (compareVersions(pack.version, latestTemplate.version) > 0) {
|
||||
latestTemplate = pack
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
21
yarn.lock
21
yarn.lock
@@ -19,12 +19,6 @@
|
||||
normalize-path "^2.0.1"
|
||||
through2 "^2.0.3"
|
||||
|
||||
"@nraynaud/novnc@^0.6.1-1":
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@nraynaud/novnc/-/novnc-0.6.1.tgz#995459bb6f7bd5dd9bd7899b021f68f7744cf1b3"
|
||||
dependencies:
|
||||
pako "^1.0.3"
|
||||
|
||||
JSONStream@^1.0.3:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.1.tgz#707f761e01dae9e16f1bcf93703b78c70966579a"
|
||||
@@ -1210,6 +1204,10 @@ bootstrap@4.0.0-alpha.5:
|
||||
jquery "1.9.1 - 3"
|
||||
tether "^1.3.7"
|
||||
|
||||
bowser@^0.7.2:
|
||||
version "0.7.3"
|
||||
resolved "https://registry.yarnpkg.com/bowser/-/bowser-0.7.3.tgz#4fc0cb4e0e2bdd9b394df0d2038c32c2cc2712c8"
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.8"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
|
||||
@@ -5281,6 +5279,13 @@ notifyjs@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/notifyjs/-/notifyjs-3.0.0.tgz#7418c9d6c0533aebaa643414214af53b521d1b28"
|
||||
|
||||
novnc-node@^0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/novnc-node/-/novnc-node-0.5.3.tgz#69f20a4a2ff0c54ca2f942ff59553f523eb61fa7"
|
||||
dependencies:
|
||||
bowser "^0.7.2"
|
||||
debug "^2.2.0"
|
||||
|
||||
now-and-later@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-1.0.0.tgz#23e798ccaaf0e8acbef0687f82086274746e0893"
|
||||
@@ -5515,10 +5520,6 @@ p-reduce@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
|
||||
|
||||
pako@^1.0.3:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.5.tgz#d2205dfe5b9da8af797e7c163db4d1f84e4600bc"
|
||||
|
||||
pako@~0.2.0:
|
||||
version "0.2.9"
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
|
||||
|
||||
Reference in New Issue
Block a user