Compare commits
30 Commits
xen-api-v0
...
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 |
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>
|
||||
}
|
||||
}
|
||||
@@ -31,3 +31,8 @@ export const SR = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const vmGroup = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}))
|
||||
}
|
||||
|
||||
@@ -42,12 +42,15 @@ import {
|
||||
forgetSrs,
|
||||
isSrShared,
|
||||
migrateVms,
|
||||
rebootVmGroups,
|
||||
reconnectAllHostsSrs,
|
||||
rescanSrs,
|
||||
restartHosts,
|
||||
restartHostsAgents,
|
||||
restartVms,
|
||||
shutdownVmGroups,
|
||||
snapshotVms,
|
||||
startVmGroups,
|
||||
startVms,
|
||||
stopHosts,
|
||||
stopVms,
|
||||
@@ -86,6 +89,7 @@ import {
|
||||
import styles from './index.css'
|
||||
import HostItem from './host-item'
|
||||
import PoolItem from './pool-item'
|
||||
import VmGroupItem from './vm-group-item'
|
||||
import VmItem from './vm-item'
|
||||
import TemplateItem from './template-item'
|
||||
import SrItem from './sr-item'
|
||||
@@ -150,6 +154,20 @@ const OPTIONS = {
|
||||
{ labelId: 'homeSortByCpus', sortBy: 'CPUs.number', sortOrder: 'desc' }
|
||||
]
|
||||
},
|
||||
'VmGroup': {
|
||||
defaultFilter: '',
|
||||
filters: homeFilters.vmGroup,
|
||||
mainActions: [
|
||||
{ handler: shutdownVmGroups, icon: 'vm-stop', tooltip: _('stopVmLabel') },
|
||||
{ handler: startVmGroups, icon: 'vm-start', tooltip: _('startVmLabel') },
|
||||
{ handler: rebootVmGroups, icon: 'vm-reboot', tooltip: _('rebootVmLabel') }
|
||||
],
|
||||
Item: VmGroupItem,
|
||||
sortOptions: [
|
||||
{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
|
||||
{ labelId: 'homeSortByPowerstate', sortBy: 'power_state', sortOrder: 'desc' }
|
||||
]
|
||||
},
|
||||
pool: {
|
||||
defaultFilter: '',
|
||||
filters: homeFilters.pool,
|
||||
@@ -196,6 +214,7 @@ const OPTIONS = {
|
||||
|
||||
const TYPES = {
|
||||
VM: _('homeTypeVm'),
|
||||
VmGroup: _('homeTypeVmGroup'),
|
||||
'VM-template': _('homeTypeVmTemplate'),
|
||||
host: _('homeTypeHost'),
|
||||
pool: _('homeTypePool'),
|
||||
@@ -212,14 +231,18 @@ const DEFAULT_TYPE = 'VM'
|
||||
createGetObjectsOfType('host'),
|
||||
hosts => state => isEmpty(hosts(state))
|
||||
)
|
||||
const type = (_, props) => props.location.query.t || DEFAULT_TYPE
|
||||
const getType = (_, props) => props.location.query.t || DEFAULT_TYPE
|
||||
const getObjectsByType = createGetObjectsOfType(getType)
|
||||
|
||||
return {
|
||||
areObjectsFetched,
|
||||
items: createGetObjectsOfType(type),
|
||||
noServersConnected,
|
||||
type,
|
||||
user: getUser
|
||||
return (state, props) => {
|
||||
const type = getType(state, props)
|
||||
return {
|
||||
areObjectsFetched: areObjectsFetched(state, props),
|
||||
items: getObjectsByType(state, props),
|
||||
noServersConnected: noServersConnected(state, props),
|
||||
type,
|
||||
user: getUser(state, props)
|
||||
}
|
||||
}
|
||||
})
|
||||
export default class Home extends Component {
|
||||
@@ -806,7 +829,7 @@ export default class Home extends Component {
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
{' '}
|
||||
<OverlayTrigger
|
||||
{type !== 'VmGroup' && <OverlayTrigger
|
||||
autoFocus
|
||||
trigger='click'
|
||||
rootClose
|
||||
@@ -824,7 +847,7 @@ export default class Home extends Component {
|
||||
}
|
||||
>
|
||||
<Button btnStyle='link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
|
||||
</OverlayTrigger>
|
||||
</OverlayTrigger> }
|
||||
{' '}
|
||||
<DropdownButton bsStyle='link' id='sort' title={_('homeSortBy')}>
|
||||
{map(options.sortOptions, ({ labelId, sortBy: _sortBy, sortOrder }, key) => (
|
||||
|
||||
77
src/xo-app/home/vm-group-item.js
Normal file
77
src/xo-app/home/vm-group-item.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Ellipsis, { EllipsisContainer } from 'ellipsis'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import Tooltip from 'tooltip'
|
||||
import { BlockLink } from 'link'
|
||||
import { Col } from 'grid'
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObject } from 'selectors'
|
||||
import { editVmGroup } from 'xo'
|
||||
import { Text } from 'editable'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@connectStore(() => {
|
||||
return (state, props) => {
|
||||
const vms = {}
|
||||
forEach(props.item.$VMs, vmId => {
|
||||
const getVM = createGetObject(() => vmId)
|
||||
vms[vmId] = getVM(state, props)
|
||||
})
|
||||
return {
|
||||
vms
|
||||
}
|
||||
}
|
||||
})
|
||||
export default class VmGroupItem extends Component {
|
||||
toggleState = stateField => () => this.setState({ [stateField]: !this.state[stateField] })
|
||||
_onSelect = () => this.props.onSelect(this.props.item.id)
|
||||
_setNameDescription = description => editVmGroup(this.props.item, {name_description: description})
|
||||
_setNameLabel = label => editVmGroup(this.props.item, {name_label: label})
|
||||
_getVmGroupState = (vmGroup) => {
|
||||
const states = map(this.props.vms, vm => vm.power_state)
|
||||
return isEmpty(states)
|
||||
? 'Busy'
|
||||
: states.indexOf('Halted') === -1
|
||||
? states[0]
|
||||
: states.indexOf('Running') === -1
|
||||
? states[0]
|
||||
: 'Busy'
|
||||
}
|
||||
|
||||
render () {
|
||||
const { item: vmGroup, selected, vms } = this.props
|
||||
return <div className={styles.item}>
|
||||
<BlockLink to={`/vm-group/${vmGroup.id}`}>
|
||||
<SingleLineRow>
|
||||
<Col smallSize={10} mediumSize={9} largeSize={3}>
|
||||
<EllipsisContainer>
|
||||
<input type='checkbox' checked={selected} onChange={this._onSelect} value={vmGroup.id} />
|
||||
|
||||
<Ellipsis>
|
||||
<Tooltip content={_(`powerStateVmGroup${this._getVmGroupState(vmGroup)}`)}>
|
||||
<Icon icon={isEmpty(vms) ? 'halted' : `${this._getVmGroupState(vmGroup).toLowerCase()}`} />
|
||||
</Tooltip>
|
||||
<Text value={vmGroup.name_label} onChange={this._setNameLabel} useLongClick />
|
||||
</Ellipsis>
|
||||
|
||||
</EllipsisContainer>
|
||||
</Col>
|
||||
<Col mediumSize={4} className='hidden-md-down'>
|
||||
<EllipsisContainer>
|
||||
<Ellipsis>
|
||||
<Text value={vmGroup.name_description} onChange={this._setNameDescription} useLongClick />
|
||||
</Ellipsis>
|
||||
</EllipsisContainer>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</BlockLink>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
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>
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user