Compare commits

...

30 Commits

Author SHA1 Message Date
BCedric
1083aba33c add message for current operation 2017-06-23 16:51:46 +02:00
BCedric
d90156ff15 tabManagement, remove displaying vm list if the group is empty 2017-06-23 16:17:25 +02:00
BCedric
45b6e010df correct getMemoryTotal function 2017-06-23 16:14:43 +02:00
BCedric
1fbdb7799e display current operations 2017-06-23 16:12:45 +02:00
BCedric
3050fd7ce1 display an halted icon if the groupe is empty 2017-06-23 16:09:04 +02:00
BCedric
1988934f8a use proptypes decorator 2017-06-23 16:04:31 +02:00
BCedric
5331ced4fe add dragNDropOrder generic component 2017-06-22 15:57:56 +02:00
BCedric
74066f7d44 move NewVmGroup in new folder 2017-06-22 15:57:56 +02:00
BCedric
98e3aa89bb use intl in vm list 2017-06-22 15:57:56 +02:00
BCedric
070bb65740 use connectStore in VmGroup TabGeneral 2017-06-22 15:57:56 +02:00
BCedric
4e51959e3b add function removeAppliance 2017-06-22 15:57:56 +02:00
BCedric
b5ff695c74 add functions to calculate stats 2017-06-22 15:57:56 +02:00
BCedric
f879e2ace0 add VmGroup handlers in Home mainActions 2017-06-22 15:55:58 +02:00
BCedric
9aeabce4d8 add createVmGroup function 2017-06-22 15:55:58 +02:00
BCedric
2f17cd4ba9 allow to delete a vmGroup 2017-06-22 15:55:58 +02:00
BCedric
60e70a08c1 allow vms management 2017-06-22 15:55:58 +02:00
BCedric
6e0ba2bae3 add editVmGroup function 2017-06-22 15:55:58 +02:00
BCedric
feb996890e management of icon color 2017-06-22 15:55:58 +02:00
BCedric
a06bc85142 allow edition of a vmGroup 2017-06-22 15:55:58 +02:00
BCedric
f530aef92b remove vmGroup tag 2017-06-22 15:55:58 +02:00
BCedric
2eb7330335 add functions on action-bar 2017-06-22 15:55:58 +02:00
BCedric
89157e7b7e rename vm-groupe to vmGroup 2017-06-22 15:55:58 +02:00
BCedric
3834e2ef91 expose vmGroup, remove subscription 2017-06-22 15:55:58 +02:00
BCedric
a711231955 add creation of a vmGroup 2017-06-22 15:55:58 +02:00
BCedric
0a5e301b3e add stats tab 2017-06-22 15:55:58 +02:00
BCedric
c82b9893c5 add advanced tab 2017-06-22 15:55:58 +02:00
BCedric
f4dfabc34c add management tab 2017-06-22 15:55:58 +02:00
BCedric
059521aeda add general tab 2017-06-22 15:55:58 +02:00
BCedric
debca09e2c remove _getItems, rename VMGroupItem => VmGroupItem 2017-06-22 15:55:58 +02:00
BCedric
0699cfc449 display VM-Groups list 2017-06-22 15:55:58 +02:00
18 changed files with 1287 additions and 159 deletions

View 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>
}
}

View File

@@ -31,3 +31,8 @@ export const SR = {
...common,
homeFilterTags: 'tags:'
}
export const vmGroup = {
...common,
homeFilterTags: 'tags:'
}

View File

@@ -38,6 +38,7 @@ var messages = {
// ----- Titles -----
homePage: 'Home',
homeVmPage: 'VMs',
homeVmGroupPage: 'VM-Groups',
homeHostPage: 'Hosts',
homePoolPage: 'Pools',
homeTemplatePage: 'Templates',
@@ -66,6 +67,7 @@ var messages = {
taskMenu: 'Tasks',
taskPage: 'Tasks',
newVmPage: 'VM',
newVmGroupPage: 'VM-Group',
newSrPage: 'Storage',
newServerPage: 'Server',
newImport: 'Import',
@@ -121,6 +123,7 @@ var messages = {
homeTypePool: 'Pool',
homeTypeHost: 'Host',
homeTypeVm: 'VM',
homeTypeVmGroup: 'VM group',
homeTypeSr: 'SR',
homeTypeVmTemplate: 'Template',
homeSort: 'Sort',
@@ -624,6 +627,7 @@ var messages = {
advancedTabName: 'Advanced',
networkTabName: 'Network',
disksTabName: 'Disk{disks, plural, one {} other {s}}',
managementTabName: 'Management',
powerStateHalted: 'halted',
powerStateRunning: 'running',
@@ -934,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.',
@@ -1086,6 +1116,8 @@ var messages = {
migrateVmNetwork: 'Network',
migrateVmNoTargetHost: 'No target host',
migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
deleteVmGroupModalTitle: 'Delete VM-Group',
deleteVmGroupModalMessage: 'Are you sure you want to delete this VMGroup ?',
deleteVdiModalTitle: 'Delete VDI',
deleteVdiModalMessage: 'Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST',
revertVmModalTitle: 'Revert your VM',

View File

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

View File

@@ -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)
@@ -1035,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) => (
@@ -1951,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}))
}

View File

@@ -42,12 +42,15 @@ import {
forgetSrs,
isSrShared,
migrateVms,
rebootVmGroups,
reconnectAllHostsSrs,
rescanSrs,
restartHosts,
restartHostsAgents,
restartVms,
shutdownVmGroups,
snapshotVms,
startVmGroups,
startVms,
stopHosts,
stopVms,
@@ -86,6 +89,7 @@ import {
import styles from './index.css'
import HostItem from './host-item'
import PoolItem from './pool-item'
import VmGroupItem from './vm-group-item'
import VmItem from './vm-item'
import TemplateItem from './template-item'
import SrItem from './sr-item'
@@ -150,6 +154,20 @@ const OPTIONS = {
{ labelId: 'homeSortByCpus', sortBy: 'CPUs.number', sortOrder: 'desc' }
]
},
'VmGroup': {
defaultFilter: '',
filters: homeFilters.vmGroup,
mainActions: [
{ handler: shutdownVmGroups, icon: 'vm-stop', tooltip: _('stopVmLabel') },
{ handler: startVmGroups, icon: 'vm-start', tooltip: _('startVmLabel') },
{ handler: rebootVmGroups, icon: 'vm-reboot', tooltip: _('rebootVmLabel') }
],
Item: VmGroupItem,
sortOptions: [
{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
{ labelId: 'homeSortByPowerstate', sortBy: 'power_state', sortOrder: 'desc' }
]
},
pool: {
defaultFilter: '',
filters: homeFilters.pool,
@@ -196,6 +214,7 @@ const OPTIONS = {
const TYPES = {
VM: _('homeTypeVm'),
VmGroup: _('homeTypeVmGroup'),
'VM-template': _('homeTypeVmTemplate'),
host: _('homeTypeHost'),
pool: _('homeTypePool'),
@@ -212,14 +231,18 @@ const DEFAULT_TYPE = 'VM'
createGetObjectsOfType('host'),
hosts => state => isEmpty(hosts(state))
)
const type = (_, props) => props.location.query.t || DEFAULT_TYPE
const getType = (_, props) => props.location.query.t || DEFAULT_TYPE
const getObjectsByType = createGetObjectsOfType(getType)
return {
areObjectsFetched,
items: createGetObjectsOfType(type),
noServersConnected,
type,
user: getUser
return (state, props) => {
const type = getType(state, props)
return {
areObjectsFetched: areObjectsFetched(state, props),
items: getObjectsByType(state, props),
noServersConnected: noServersConnected(state, props),
type,
user: getUser(state, props)
}
}
})
export default class Home extends Component {
@@ -806,7 +829,7 @@ export default class Home extends Component {
</OverlayTrigger>
)}
{' '}
<OverlayTrigger
{type !== 'VmGroup' && <OverlayTrigger
autoFocus
trigger='click'
rootClose
@@ -824,7 +847,7 @@ export default class Home extends Component {
}
>
<Button btnStyle='link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
</OverlayTrigger>
</OverlayTrigger> }
{' '}
<DropdownButton bsStyle='link' id='sort' title={_('homeSortBy')}>
{map(options.sortOptions, ({ labelId, sortBy: _sortBy, sortOrder }, key) => (

View 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} />
&nbsp;&nbsp;
<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>
&nbsp;&nbsp;
</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>
}
}

View File

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

View File

@@ -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' },
@@ -167,6 +168,7 @@ export default class Menu extends Component {
isAdmin && { to: '/xosan', icon: 'menu-xosan', label: 'xosan' },
!(noOperatablePools && noResourceSets) && { to: '/vms/new', icon: 'menu-new', label: 'newMenu', subMenu: [
{ to: '/vms/new', icon: 'menu-new-vm', label: 'newVmPage' },
{ to: '/new/vm-group', icon: 'menu-new-vm', label: 'newVmGroupPage' },
isAdmin && { to: '/new/sr', icon: 'menu-new-sr', label: 'newSrPage' },
isAdmin && { to: '/settings/servers', icon: 'menu-settings-servers', label: 'newServerPage' },
!noOperatablePools && { to: '/vms/import', icon: 'menu-new-import', label: 'newImport' }

View File

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

View 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}&nbsp;</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>
)
}
}

View 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

View 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>
}
}

View 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>
)
}
}

View 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)}
&nbsp;<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>
)
})

View 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>
&nbsp;
<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>
)
}
}

View 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>
}
}

View File

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