parent
539d136936
commit
0d47332526
@ -310,7 +310,8 @@ export default {
|
|||||||
alarmPool: 'Pool',
|
alarmPool: 'Pool',
|
||||||
alarmRemoveAll: 'Supprimer toutes les alarmes',
|
alarmRemoveAll: 'Supprimer toutes les alarmes',
|
||||||
// ----- New VM -----
|
// ----- New VM -----
|
||||||
newVmCreateNewVmOn: 'Créer une nouvelle VM sur {pool}',
|
newVmCreateNewVmOn: 'Créer une nouvelle VM sur {select}',
|
||||||
|
newVmCreateNewVmOn2: 'Créer une nouvelle VM sur {select1} ou {select2}',
|
||||||
newVmInfoPanel: 'Informations',
|
newVmInfoPanel: 'Informations',
|
||||||
newVmNameLabel: 'Nom',
|
newVmNameLabel: 'Nom',
|
||||||
newVmTemplateLabel: 'Modèle',
|
newVmTemplateLabel: 'Modèle',
|
||||||
|
@ -126,6 +126,11 @@ var messages = {
|
|||||||
selectPifs: 'Select PIF(s)…',
|
selectPifs: 'Select PIF(s)…',
|
||||||
selectPools: 'Select Pool(s)…',
|
selectPools: 'Select Pool(s)…',
|
||||||
selectRemotes: 'Select Remote(s)…',
|
selectRemotes: 'Select Remote(s)…',
|
||||||
|
selectResourceSets: 'Select resource set(s)…',
|
||||||
|
selectResourceSetsVmTemplate: 'Select template(s)…',
|
||||||
|
selectResourceSetsSr: 'Select SR(s)…',
|
||||||
|
selectResourceSetsNetwork: 'Select network(s)…',
|
||||||
|
selectResourceSetsVdi: 'Select disk(s)…',
|
||||||
selectSrs: 'Select SR(s)…',
|
selectSrs: 'Select SR(s)…',
|
||||||
selectVms: 'Select VM(s)…',
|
selectVms: 'Select VM(s)…',
|
||||||
selectVmTemplates: 'Select VM template(s)…',
|
selectVmTemplates: 'Select VM template(s)…',
|
||||||
@ -583,7 +588,9 @@ var messages = {
|
|||||||
alarmRemoveAll: 'Remove all alarms',
|
alarmRemoveAll: 'Remove all alarms',
|
||||||
|
|
||||||
// ----- New VM -----
|
// ----- New VM -----
|
||||||
newVmCreateNewVmOn: 'Create a new VM on {pool}',
|
newVmCreateNewVmOn: 'Create a new VM on {select}',
|
||||||
|
newVmCreateNewVmOn2: 'Create a new VM on {select1} or {select2}',
|
||||||
|
newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
|
||||||
newVmInfoPanel: 'Infos',
|
newVmInfoPanel: 'Infos',
|
||||||
newVmNameLabel: 'Name',
|
newVmNameLabel: 'Name',
|
||||||
newVmTemplateLabel: 'Template',
|
newVmTemplateLabel: 'Template',
|
||||||
@ -622,6 +629,7 @@ var messages = {
|
|||||||
newVmCreateVms: 'Create VMs',
|
newVmCreateVms: 'Create VMs',
|
||||||
newVmCreateVmsConfirm: 'Are you sure you want to create {nbVms} VMs?',
|
newVmCreateVmsConfirm: 'Are you sure you want to create {nbVms} VMs?',
|
||||||
newVmMultipleVms: 'Multiple VMs:',
|
newVmMultipleVms: 'Multiple VMs:',
|
||||||
|
newVmSelectResourceSet: 'Select a resource set:',
|
||||||
|
|
||||||
// ----- Self -----
|
// ----- Self -----
|
||||||
resourceSets: 'Resource sets',
|
resourceSets: 'Resource sets',
|
||||||
|
@ -108,6 +108,11 @@ const xoItemToRender = {
|
|||||||
<Icon icon='user' /> {user.email}
|
<Icon icon='user' /> {user.email}
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
|
resourceSet: resourceSet => (
|
||||||
|
<span>
|
||||||
|
<Icon icon='resource-set' /> {resourceSet.name}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
|
||||||
// XO objects.
|
// XO objects.
|
||||||
pool: pool => (
|
pool: pool => (
|
||||||
|
@ -2,12 +2,14 @@ import React from 'react'
|
|||||||
import assign from 'lodash/assign'
|
import assign from 'lodash/assign'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import filter from 'lodash/filter'
|
import filter from 'lodash/filter'
|
||||||
|
import flatten from 'lodash/flatten'
|
||||||
import forEach from 'lodash/forEach'
|
import forEach from 'lodash/forEach'
|
||||||
import groupBy from 'lodash/groupBy'
|
import groupBy from 'lodash/groupBy'
|
||||||
import keyBy from 'lodash/keyBy'
|
import keyBy from 'lodash/keyBy'
|
||||||
import keys from 'lodash/keys'
|
import keys from 'lodash/keys'
|
||||||
import map from 'lodash/map'
|
import map from 'lodash/map'
|
||||||
import sortBy from 'lodash/sortBy'
|
import sortBy from 'lodash/sortBy'
|
||||||
|
import store from 'store'
|
||||||
import { parse as parseRemote } from 'xo-remote-parser'
|
import { parse as parseRemote } from 'xo-remote-parser'
|
||||||
|
|
||||||
import _ from './intl'
|
import _ from './intl'
|
||||||
@ -19,16 +21,19 @@ import {
|
|||||||
createFilter,
|
createFilter,
|
||||||
createGetObjectsOfType,
|
createGetObjectsOfType,
|
||||||
createGetTags,
|
createGetTags,
|
||||||
createSelector
|
createSelector,
|
||||||
|
getObject
|
||||||
} from './selectors'
|
} from './selectors'
|
||||||
import {
|
import {
|
||||||
connectStore,
|
connectStore,
|
||||||
mapPlus
|
mapPlus,
|
||||||
|
resolveResourceSets
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import {
|
import {
|
||||||
isSrWritable,
|
isSrWritable,
|
||||||
subscribeGroups,
|
subscribeGroups,
|
||||||
subscribeRemotes,
|
subscribeRemotes,
|
||||||
|
subscribeResourceSets,
|
||||||
subscribeRoles,
|
subscribeRoles,
|
||||||
subscribeUsers
|
subscribeUsers
|
||||||
} from './xo'
|
} from './xo'
|
||||||
@ -580,3 +585,184 @@ export const SelectRemote = makeSubscriptionSelect(subscriber => {
|
|||||||
|
|
||||||
return unsubscribeRemotes
|
return unsubscribeRemotes
|
||||||
}, { placeholder: _('selectRemotes') })
|
}, { placeholder: _('selectRemotes') })
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
export const SelectResourceSet = makeSubscriptionSelect(subscriber => {
|
||||||
|
const unsubscribeResourceSets = subscribeResourceSets(resourceSets => {
|
||||||
|
const xoObjects = map(sortBy(resolveResourceSets(resourceSets), 'name'), resourceSet => ({...resourceSet, type: 'resourceSet'}))
|
||||||
|
|
||||||
|
subscriber({xoObjects})
|
||||||
|
})
|
||||||
|
|
||||||
|
return unsubscribeResourceSets
|
||||||
|
}, { placeholder: _('selectResourceSets') })
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
export class SelectResourceSetsVmTemplate extends Component {
|
||||||
|
get value () {
|
||||||
|
return this.refs.select.value
|
||||||
|
}
|
||||||
|
|
||||||
|
set value (value) {
|
||||||
|
this.refs.select.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||||
|
this.setState({
|
||||||
|
resourceSets: resolveResourceSets(resourceSets)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_getTemplates = createSelector(
|
||||||
|
() => this.props.resourceSet,
|
||||||
|
({ objectsByType }) => {
|
||||||
|
const { predicate } = this.props
|
||||||
|
const templates = objectsByType['VM-template']
|
||||||
|
return sortBy(predicate ? filter(templates, predicate) : templates, 'name_label')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<GenericSelect
|
||||||
|
ref='select'
|
||||||
|
placeholder={_('selectResourceSetsVmTemplate')}
|
||||||
|
{...this.props}
|
||||||
|
xoObjects={this._getTemplates()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
export class SelectResourceSetsSr extends Component {
|
||||||
|
get value () {
|
||||||
|
return this.refs.select.value
|
||||||
|
}
|
||||||
|
|
||||||
|
set value (value) {
|
||||||
|
this.refs.select.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||||
|
this.setState({
|
||||||
|
resourceSets: resolveResourceSets(resourceSets)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSrs = createSelector(
|
||||||
|
() => this.props.resourceSet,
|
||||||
|
({ objectsByType }) => {
|
||||||
|
const { predicate } = this.props
|
||||||
|
const srs = objectsByType['SR']
|
||||||
|
return sortBy(predicate ? filter(srs, predicate) : srs, 'name_label')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<GenericSelect
|
||||||
|
ref='select'
|
||||||
|
placeholder={_('selectResourceSetsSr')}
|
||||||
|
{...this.props}
|
||||||
|
xoObjects={this._getSrs()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
export class SelectResourceSetsVdi extends Component {
|
||||||
|
get value () {
|
||||||
|
return this.refs.select.value
|
||||||
|
}
|
||||||
|
|
||||||
|
set value (value) {
|
||||||
|
this.refs.select.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||||
|
this.setState({
|
||||||
|
resourceSets: resolveResourceSets(resourceSets)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_getObject (id) {
|
||||||
|
return getObject(store.getState(), id, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSrs = createSelector(
|
||||||
|
() => this.props.resourceSet,
|
||||||
|
({ objectsByType }) => {
|
||||||
|
const { srPredicate } = this.props
|
||||||
|
const srs = objectsByType['SR']
|
||||||
|
return srPredicate ? filter(srs, srPredicate) : srs
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
_getVdis = createSelector(
|
||||||
|
this._getSrs,
|
||||||
|
srs => sortBy(map(flatten(map(srs, sr => sr.VDIs)), this._getObject), 'name_label')
|
||||||
|
)
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<GenericSelect
|
||||||
|
ref='select'
|
||||||
|
placeholder={_('selectResourceSetsVdi')}
|
||||||
|
{...this.props}
|
||||||
|
xoObjects={this._getVdis()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
export class SelectResourceSetsNetwork extends Component {
|
||||||
|
get value () {
|
||||||
|
return this.refs.select.value
|
||||||
|
}
|
||||||
|
|
||||||
|
set value (value) {
|
||||||
|
this.refs.select.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||||
|
this.setState({
|
||||||
|
resourceSets: resolveResourceSets(resourceSets)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_getNetworks = createSelector(
|
||||||
|
() => this.props.resourceSet,
|
||||||
|
({ objectsByType }) => {
|
||||||
|
const { predicate } = this.props
|
||||||
|
const networks = objectsByType['network']
|
||||||
|
return sortBy(predicate ? filter(networks, predicate) : networks, 'name_label')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<GenericSelect
|
||||||
|
ref='select'
|
||||||
|
placeholder={_('selectResourceSetsNetwork')}
|
||||||
|
{...this.props}
|
||||||
|
xoObjects={this._getNetworks()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -245,13 +245,18 @@ const _getPermissionsPredicate = invoke(() => {
|
|||||||
|
|
||||||
// Creates an object selector from an id selector.
|
// Creates an object selector from an id selector.
|
||||||
export const createGetObject = (idSelector = _getId) =>
|
export const createGetObject = (idSelector = _getId) =>
|
||||||
(state, props) => {
|
(state, props, useResourceSet) => {
|
||||||
const object = state.objects.all[idSelector(state, props)]
|
const object = state.objects.all[idSelector(state, props)]
|
||||||
if (!object) {
|
if (!object) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (useResourceSet) {
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
const predicate = _getPermissionsPredicate(state)
|
const predicate = _getPermissionsPredicate(state)
|
||||||
|
|
||||||
if (!predicate) {
|
if (!predicate) {
|
||||||
if (predicate == null) {
|
if (predicate == null) {
|
||||||
return object // no filtering
|
return object // no filtering
|
||||||
|
@ -14,7 +14,9 @@ import map from 'lodash/map'
|
|||||||
import mapValues from 'lodash/mapValues'
|
import mapValues from 'lodash/mapValues'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import replace from 'lodash/replace'
|
import replace from 'lodash/replace'
|
||||||
|
import store from 'store'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
|
import { getObject } from 'selectors'
|
||||||
|
|
||||||
import BaseComponent from './base-component'
|
import BaseComponent from './base-component'
|
||||||
import invoke from './invoke'
|
import invoke from './invoke'
|
||||||
@ -355,6 +357,41 @@ export function rethrow (cb) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
export const resolveResourceSets = resourceSets => (
|
||||||
|
map(resourceSets, resourceSet => {
|
||||||
|
const { objects, ...attrs } = resourceSet
|
||||||
|
const resolvedObjects = {}
|
||||||
|
const resolvedSet = {
|
||||||
|
...attrs,
|
||||||
|
missingObjects: [],
|
||||||
|
objectsByType: resolvedObjects
|
||||||
|
}
|
||||||
|
const state = store.getState()
|
||||||
|
|
||||||
|
forEach(objects, id => {
|
||||||
|
const object = getObject(state, id, true) // true: useResourceSet to bypass permissions
|
||||||
|
|
||||||
|
// Error, missing resource.
|
||||||
|
if (!object) {
|
||||||
|
resolvedSet.missingObjects.push(id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type } = object
|
||||||
|
|
||||||
|
if (!resolvedObjects[type]) {
|
||||||
|
resolvedObjects[type] = [ object ]
|
||||||
|
} else {
|
||||||
|
resolvedObjects[type].push(object)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return resolvedSet
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
// Creates a string replacer based on a pattern and a list of rules
|
// Creates a string replacer based on a pattern and a list of rules
|
||||||
|
@ -2,15 +2,22 @@ import _ from 'intl'
|
|||||||
import Component from 'base-component'
|
import Component from 'base-component'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import Icon from 'icon'
|
import Icon from 'icon'
|
||||||
|
import isEmpty from 'lodash/isEmpty'
|
||||||
import Link from 'link'
|
import Link from 'link'
|
||||||
import map from 'lodash/map'
|
import map from 'lodash/map'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Tooltip from 'tooltip'
|
import Tooltip from 'tooltip'
|
||||||
import { Button } from 'react-bootstrap-4/lib'
|
import { Button } from 'react-bootstrap-4/lib'
|
||||||
import { connectStore, noop, getXoaPlan } from 'utils'
|
import { connectStore, noop, getXoaPlan } from 'utils'
|
||||||
import { createGetObjectsOfType, getLang, getUser } from 'selectors'
|
import { signOut, subscribePermissions, subscribeResourceSets } from 'xo'
|
||||||
import { signOut } from 'xo'
|
|
||||||
import { UpdateTag } from '../xoa-updates'
|
import { UpdateTag } from '../xoa-updates'
|
||||||
|
import {
|
||||||
|
createFilter,
|
||||||
|
createGetObjectsOfType,
|
||||||
|
createSelector,
|
||||||
|
getLang,
|
||||||
|
getUser
|
||||||
|
} from 'selectors'
|
||||||
|
|
||||||
import styles from './index.css'
|
import styles from './index.css'
|
||||||
|
|
||||||
@ -20,10 +27,10 @@ import styles from './index.css'
|
|||||||
// There are currently issues between context updates (used by
|
// There are currently issues between context updates (used by
|
||||||
// react-intl) and pure components.
|
// react-intl) and pure components.
|
||||||
lang: getLang,
|
lang: getLang,
|
||||||
|
|
||||||
nTasks: createGetObjectsOfType('task').count(
|
nTasks: createGetObjectsOfType('task').count(
|
||||||
[ task => task.status === 'pending' ]
|
[ task => task.status === 'pending' ]
|
||||||
),
|
),
|
||||||
|
pools: createGetObjectsOfType('pool'),
|
||||||
user: getUser
|
user: getUser
|
||||||
}), {
|
}), {
|
||||||
withRef: true
|
withRef: true
|
||||||
@ -40,12 +47,37 @@ export default class Menu extends Component {
|
|||||||
window.removeEventListener('resize', updateCollapsed)
|
window.removeEventListener('resize', updateCollapsed)
|
||||||
this._removeListener = noop
|
this._removeListener = noop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._unsubscribeResourceSets = subscribeResourceSets(resourceSets => {
|
||||||
|
this.setState({
|
||||||
|
resourceSets
|
||||||
|
})
|
||||||
|
})
|
||||||
|
this._unsubscribePermissions = subscribePermissions(permissions => {
|
||||||
|
this.setState({
|
||||||
|
permissions
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
this._removeListener()
|
this._removeListener()
|
||||||
|
this._unsubscribeResourceSets()
|
||||||
|
this._unsubscribePermissions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getNoOperatablePools = createSelector(
|
||||||
|
createFilter(
|
||||||
|
() => this.props.pools,
|
||||||
|
() => this.permissions,
|
||||||
|
[ ({ id }, permissions) => {
|
||||||
|
const { user } = this.props
|
||||||
|
return user && user.permission === 'admin' || permissions && permissions[id] && permissions[id].operate
|
||||||
|
} ]
|
||||||
|
),
|
||||||
|
isEmpty
|
||||||
|
)
|
||||||
|
|
||||||
get height () {
|
get height () {
|
||||||
return this.refs.content.offsetHeight
|
return this.refs.content.offsetHeight
|
||||||
}
|
}
|
||||||
@ -58,6 +90,8 @@ export default class Menu extends Component {
|
|||||||
render () {
|
render () {
|
||||||
const { nTasks, user } = this.props
|
const { nTasks, user } = this.props
|
||||||
const isAdmin = user && user.permission === 'admin'
|
const isAdmin = user && user.permission === 'admin'
|
||||||
|
const noOperatablePools = this._getNoOperatablePools()
|
||||||
|
const noResourceSets = isEmpty(this.state.resourceSets)
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{ to: '/home', icon: 'menu-home', label: 'homePage' },
|
{ to: '/home', icon: 'menu-home', label: 'homePage' },
|
||||||
@ -92,11 +126,11 @@ export default class Menu extends Component {
|
|||||||
]},
|
]},
|
||||||
{ to: '/about', icon: 'menu-about', label: 'aboutPage' },
|
{ to: '/about', icon: 'menu-about', label: 'aboutPage' },
|
||||||
{ to: '/tasks', icon: 'task', label: 'taskMenu', pill: nTasks },
|
{ to: '/tasks', icon: 'task', label: 'taskMenu', pill: nTasks },
|
||||||
{ to: '/vms/new', icon: 'menu-new', label: 'newMenu', subMenu: [
|
!(noOperatablePools && noResourceSets) && { to: '/vms/new', icon: 'menu-new', label: 'newMenu', subMenu: [
|
||||||
{ to: '/vms/new', icon: 'menu-new-vm', label: 'newVmPage' },
|
{ to: '/vms/new', icon: 'menu-new-vm', label: 'newVmPage' },
|
||||||
isAdmin && { to: '/new/sr', icon: 'menu-new-sr', label: 'newSrPage' },
|
isAdmin && { to: '/new/sr', icon: 'menu-new-sr', label: 'newSrPage' },
|
||||||
isAdmin && { to: '/settings/servers', icon: 'menu-settings-servers', label: 'newServerPage' },
|
isAdmin && { to: '/settings/servers', icon: 'menu-settings-servers', label: 'newServerPage' },
|
||||||
{ to: '/vms/import', icon: 'menu-new-import', label: 'newImport' }
|
!noOperatablePools && { to: '/vms/import', icon: 'menu-new-import', label: 'newImport' }
|
||||||
]}
|
]}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -10,26 +10,37 @@ import find from 'lodash/find'
|
|||||||
import forEach from 'lodash/forEach'
|
import forEach from 'lodash/forEach'
|
||||||
import getEventValue from 'get-event-value'
|
import getEventValue from 'get-event-value'
|
||||||
import Icon from 'icon'
|
import Icon from 'icon'
|
||||||
|
import includes from 'lodash/includes'
|
||||||
import isArray from 'lodash/isArray'
|
import isArray from 'lodash/isArray'
|
||||||
|
import isEmpty from 'lodash/isEmpty'
|
||||||
|
import isObject from 'lodash/isObject'
|
||||||
import map from 'lodash/map'
|
import map from 'lodash/map'
|
||||||
|
import Page from '../page'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import size from 'lodash/size'
|
import size from 'lodash/size'
|
||||||
import slice from 'lodash/slice'
|
import slice from 'lodash/slice'
|
||||||
import store from 'store'
|
import store from 'store'
|
||||||
import toArray from 'lodash/toArray'
|
|
||||||
import Wizard, { Section } from 'wizard'
|
import Wizard, { Section } from 'wizard'
|
||||||
import { Button } from 'react-bootstrap-4/lib'
|
import { Button } from 'react-bootstrap-4/lib'
|
||||||
|
import { Container, Row, Col } from 'grid'
|
||||||
import { injectIntl } from 'react-intl'
|
import { injectIntl } from 'react-intl'
|
||||||
import {
|
import {
|
||||||
createVm,
|
createVm,
|
||||||
createVms,
|
createVms,
|
||||||
getCloudInitConfig
|
getCloudInitConfig,
|
||||||
|
subscribePermissions,
|
||||||
|
subscribeResourceSets
|
||||||
} from 'xo'
|
} from 'xo'
|
||||||
import {
|
import {
|
||||||
SelectVdi,
|
|
||||||
SelectNetwork,
|
SelectNetwork,
|
||||||
SelectPool,
|
SelectPool,
|
||||||
|
SelectResourceSet,
|
||||||
|
SelectResourceSetsNetwork,
|
||||||
|
SelectResourceSetsSr,
|
||||||
|
SelectResourceSetsVdi,
|
||||||
|
SelectResourceSetsVmTemplate,
|
||||||
SelectSr,
|
SelectSr,
|
||||||
|
SelectVdi,
|
||||||
SelectVmTemplate
|
SelectVmTemplate
|
||||||
} from 'select-objects'
|
} from 'select-objects'
|
||||||
import {
|
import {
|
||||||
@ -39,12 +50,15 @@ import {
|
|||||||
import {
|
import {
|
||||||
connectStore,
|
connectStore,
|
||||||
formatSize,
|
formatSize,
|
||||||
noop
|
noop,
|
||||||
|
resolveResourceSets
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import {
|
import {
|
||||||
|
createFilter,
|
||||||
createSelector,
|
createSelector,
|
||||||
createGetObject,
|
createGetObject,
|
||||||
createGetObjectsOfType
|
createGetObjectsOfType,
|
||||||
|
getUser
|
||||||
} from 'selectors'
|
} from 'selectors'
|
||||||
|
|
||||||
import styles from './index.css'
|
import styles from './index.css'
|
||||||
@ -54,6 +68,9 @@ const NB_VMS_MIN = 2
|
|||||||
const NB_VMS_MAX = 100
|
const NB_VMS_MAX = 100
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
// Sub-components --------------------------------------------------------------
|
||||||
|
|
||||||
const SectionContent = ({ summary, column, children }) => (
|
const SectionContent = ({ summary, column, children }) => (
|
||||||
<div className={classNames(
|
<div className={classNames(
|
||||||
'form-inline',
|
'form-inline',
|
||||||
@ -63,13 +80,11 @@ const SectionContent = ({ summary, column, children }) => (
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const LineItem = ({ children }) => (
|
const LineItem = ({ children }) => (
|
||||||
<div className={styles.lineItem}>
|
<div className={styles.lineItem}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const Item = ({ label, children }) => (
|
const Item = ({ label, children }) => (
|
||||||
<span className={styles.item}>
|
<span className={styles.item}>
|
||||||
{label && <span>{_(label)} </span>}
|
{label && <span>{_(label)} </span>}
|
||||||
@ -77,10 +92,17 @@ const Item = ({ label, children }) => (
|
|||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
const getObject = createGetObject((_, id) => id)
|
const getObject = createGetObject((_, id) => id)
|
||||||
|
|
||||||
@connectStore(() => ({
|
@connectStore(() => ({
|
||||||
|
isAdmin: createSelector(
|
||||||
|
getUser,
|
||||||
|
user => user && user.permission === 'admin'
|
||||||
|
),
|
||||||
networks: createGetObjectsOfType('network').sort(),
|
networks: createGetObjectsOfType('network').sort(),
|
||||||
|
pools: createGetObjectsOfType('pool'),
|
||||||
templates: createGetObjectsOfType('VM-template').sort()
|
templates: createGetObjectsOfType('VM-template').sort()
|
||||||
}))
|
}))
|
||||||
@injectIntl
|
@injectIntl
|
||||||
@ -89,8 +111,8 @@ export default class NewVm extends BaseComponent {
|
|||||||
super()
|
super()
|
||||||
|
|
||||||
this._uniqueId = 0
|
this._uniqueId = 0
|
||||||
// NewVm's state is stored in this.state.state instead of this.state
|
// NewVm's form's state is stored in this.state.state instead of this.state
|
||||||
// so it can be emptied easily with this.setState(state: {})
|
// so it can be emptied easily with this.setState({ state: {} })
|
||||||
this.state = { state: {} }
|
this.state = { state: {} }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,49 +120,34 @@ export default class NewVm extends BaseComponent {
|
|||||||
this._reset()
|
this._reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
getPoolNetworks = createSelector(
|
componentWillMount () {
|
||||||
() => this.props.networks,
|
this._unsubscribeResourceSets = subscribeResourceSets(resourceSets => {
|
||||||
() => {
|
this.setState({
|
||||||
const { pool } = this.state.state
|
resourceSets: resolveResourceSets(resourceSets)
|
||||||
return pool && pool.id
|
})
|
||||||
},
|
})
|
||||||
(networks, poolId) => filter(networks, network => network.$pool === poolId)
|
this._unsubscribePermissions = subscribePermissions(permissions => {
|
||||||
)
|
this.setState({
|
||||||
|
permissions
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
this._unsubscribeResourceSets()
|
||||||
|
this._unsubscribePermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utils -----------------------------------------------------------------------
|
||||||
|
|
||||||
getUniqueId () {
|
getUniqueId () {
|
||||||
return this._uniqueId++
|
return this._uniqueId++
|
||||||
}
|
}
|
||||||
|
|
||||||
get _isDiskTemplate () {
|
get _isDiskTemplate () {
|
||||||
const { template } = this.state.state
|
const { template } = this.state.state
|
||||||
return template &&
|
return template &&
|
||||||
template.template_info.disks.length === 0 && template.name_label !== 'Other install media'
|
template.template_info.disks.length === 0 && template.name_label !== 'Other install media'
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateNbVms = () => {
|
|
||||||
const { nbVms, name_label, nameLabels } = this.state.state
|
|
||||||
const nbVmsClamped = clamp(nbVms, NB_VMS_MIN, NB_VMS_MAX)
|
|
||||||
const newNameLabels = [ ...nameLabels ]
|
|
||||||
if (nbVmsClamped < nameLabels.length) {
|
|
||||||
this._setState({ nameLabels: slice(newNameLabels, 0, nbVmsClamped) })
|
|
||||||
} else {
|
|
||||||
for (let i = nameLabels.length + 1; i <= nbVmsClamped; i++) {
|
|
||||||
newNameLabels.push(`${name_label || 'VM'}_${i}`)
|
|
||||||
}
|
|
||||||
this._setState({ nameLabels: newNameLabels })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateNameLabels = () => {
|
|
||||||
const { name_label, nameLabels } = this.state.state
|
|
||||||
const nbVms = nameLabels.length
|
|
||||||
const newNameLabels = []
|
|
||||||
for (let i = 1; i <= nbVms; i++) {
|
|
||||||
newNameLabels.push(`${name_label || 'VM'}_${i}`)
|
|
||||||
}
|
|
||||||
this._setState({ nameLabels: newNameLabels })
|
|
||||||
}
|
|
||||||
|
|
||||||
_setState = (newValues, callback) => {
|
_setState = (newValues, callback) => {
|
||||||
this.setState({ state: {
|
this.setState({ state: {
|
||||||
...this.state.state,
|
...this.state.state,
|
||||||
@ -150,23 +157,30 @@ export default class NewVm extends BaseComponent {
|
|||||||
_replaceState = (state, callback) =>
|
_replaceState = (state, callback) =>
|
||||||
this.setState({ state }, callback)
|
this.setState({ state }, callback)
|
||||||
|
|
||||||
_reset = pool =>
|
// Actions ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
_reset = ({ pool, resourceSet } = { pool: this.state.pool, resourceSet: this.state.resourceSet }) => {
|
||||||
|
this.setState({ pool, resourceSet })
|
||||||
this._replaceState({
|
this._replaceState({
|
||||||
bootAfterCreate: true,
|
bootAfterCreate: true,
|
||||||
configDrive: false,
|
configDrive: false,
|
||||||
|
CPUs: '',
|
||||||
cpuWeight: 1,
|
cpuWeight: 1,
|
||||||
existingDisks: {},
|
existingDisks: {},
|
||||||
fastClone: true,
|
fastClone: true,
|
||||||
multipleVms: false,
|
multipleVms: false,
|
||||||
|
name_label: '',
|
||||||
|
name_description: '',
|
||||||
nameLabels: map(Array(NB_VMS_MIN), (_, index) => `VM_${index + 1}`),
|
nameLabels: map(Array(NB_VMS_MIN), (_, index) => `VM_${index + 1}`),
|
||||||
|
|
||||||
nbVms: NB_VMS_MIN,
|
nbVms: NB_VMS_MIN,
|
||||||
pool: pool || this.state.state.pool,
|
|
||||||
VDIs: [],
|
VDIs: [],
|
||||||
VIFs: []
|
VIFs: []
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
_create = () => {
|
_create = () => {
|
||||||
const { state } = this.state
|
const { resourceSet, state } = this.state
|
||||||
let installation
|
let installation
|
||||||
switch (state.installMethod) {
|
switch (state.installMethod) {
|
||||||
case 'ISO':
|
case 'ISO':
|
||||||
@ -205,13 +219,14 @@ export default class NewVm extends BaseComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
clone: this._isDiskTemplate && state.fastClone,
|
clone: !this.isDiskTemplate && state.fastClone,
|
||||||
existingDisks: state.existingDisks,
|
existingDisks: state.existingDisks,
|
||||||
installation,
|
installation,
|
||||||
name_label: state.name_label,
|
name_label: state.name_label,
|
||||||
template: state.template.id,
|
template: state.template.id,
|
||||||
VDIs: state.VDIs,
|
VDIs: state.VDIs,
|
||||||
VIFs: state.VIFs,
|
VIFs: state.VIFs,
|
||||||
|
resourceSet: resourceSet && resourceSet.id,
|
||||||
// TODO: To be added in xo-server
|
// TODO: To be added in xo-server
|
||||||
// vm.set parameters
|
// vm.set parameters
|
||||||
CPUs: state.CPUs,
|
CPUs: state.CPUs,
|
||||||
@ -227,28 +242,6 @@ export default class NewVm extends BaseComponent {
|
|||||||
|
|
||||||
return state.multipleVms ? createVms(data, state.nameLabels) : createVm(data)
|
return state.multipleVms ? createVms(data, state.nameLabels) : createVm(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
_selectPool = pool =>
|
|
||||||
this._reset(pool)
|
|
||||||
|
|
||||||
_getIsInPool = createSelector(
|
|
||||||
() => this.state.state.pool.id,
|
|
||||||
poolId => object => object.$pool === poolId
|
|
||||||
)
|
|
||||||
|
|
||||||
_getSrPredicate = createSelector(
|
|
||||||
() => this.state.state.pool.id,
|
|
||||||
poolId => disk => disk.$pool === poolId && disk.content_type !== 'iso' && disk.size > 0
|
|
||||||
)
|
|
||||||
|
|
||||||
_getDefaultNetworkId = () => {
|
|
||||||
const network = find(this.getPoolNetworks(), network => {
|
|
||||||
const pif = getObject(store.getState(), network.PIFs[0])
|
|
||||||
return pif && pif.management
|
|
||||||
})
|
|
||||||
return network && network.id
|
|
||||||
}
|
|
||||||
|
|
||||||
_initTemplate = template => {
|
_initTemplate = template => {
|
||||||
if (!template) {
|
if (!template) {
|
||||||
return this._reset()
|
return this._reset()
|
||||||
@ -257,6 +250,8 @@ export default class NewVm extends BaseComponent {
|
|||||||
this._setState({ template })
|
this._setState({ template })
|
||||||
|
|
||||||
const storeState = store.getState()
|
const storeState = store.getState()
|
||||||
|
const _isInResourceSet = this._getIsInResourceSet()
|
||||||
|
const { pool, resourceSet, state } = this.state
|
||||||
|
|
||||||
const existingDisks = {}
|
const existingDisks = {}
|
||||||
forEach(template.$VBDs, vbdId => {
|
forEach(template.$VBDs, vbdId => {
|
||||||
@ -270,7 +265,9 @@ export default class NewVm extends BaseComponent {
|
|||||||
name_label: vdi.name_label,
|
name_label: vdi.name_label,
|
||||||
name_description: vdi.name_description,
|
name_description: vdi.name_description,
|
||||||
size: vdi.size,
|
size: vdi.size,
|
||||||
$SR: vdi.$SR
|
$SR: pool || _isInResourceSet(vdi.$SR, 'SR')
|
||||||
|
? vdi.$SR
|
||||||
|
: resourceSet.objectsByType['SR'][0].id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -280,7 +277,9 @@ export default class NewVm extends BaseComponent {
|
|||||||
const vif = getObject(storeState, vifId)
|
const vif = getObject(storeState, vifId)
|
||||||
VIFs.push({
|
VIFs.push({
|
||||||
id: this.getUniqueId(),
|
id: this.getUniqueId(),
|
||||||
network: vif.$network
|
network: pool || _isInResourceSet(vif.$network, 'network')
|
||||||
|
? vif.$network
|
||||||
|
: resourceSet.objectsByType['network'][0].id
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (VIFs.length === 0) {
|
if (VIFs.length === 0) {
|
||||||
@ -290,7 +289,6 @@ export default class NewVm extends BaseComponent {
|
|||||||
network: networkId
|
network: networkId
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const { state } = this.state
|
|
||||||
const name_label = state.name_label === '' || !state.name_labelHasChanged ? template.name_label : state.name_label
|
const name_label = state.name_label === '' || !state.name_labelHasChanged ? template.name_label : state.name_label
|
||||||
this._setState({
|
this._setState({
|
||||||
// infos
|
// infos
|
||||||
@ -316,55 +314,118 @@ export default class NewVm extends BaseComponent {
|
|||||||
device,
|
device,
|
||||||
name_description: disk.name_description || 'Created by XO',
|
name_description: disk.name_description || 'Created by XO',
|
||||||
name_label: (name_label || 'disk') + '_' + device,
|
name_label: (name_label || 'disk') + '_' + device,
|
||||||
SR: state.pool.default_SR
|
SR: pool
|
||||||
|
? pool.default_SR
|
||||||
|
: resourceSet.objectsByType['SR'][0].id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
getCloudInitConfig(template.id).then(
|
if (template.name_label === 'CoreOS') {
|
||||||
cloudConfig => this._setState({ cloudConfig }),
|
getCloudInitConfig(template.id).then(
|
||||||
noop
|
cloudConfig => this._setState({ cloudConfig }),
|
||||||
)
|
noop
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_addVdi = () => {
|
// Selectors -------------------------------------------------------------------
|
||||||
const { state } = this.state
|
|
||||||
const device = String(this.getUniqueId())
|
_getIsInPool = createSelector(
|
||||||
this._setState({ VDIs: [ ...state.VDIs, {
|
() => {
|
||||||
device,
|
const { pool } = this.state
|
||||||
name_description: 'Created by XO',
|
return pool && pool.id
|
||||||
name_label: (state.name_label || 'disk') + '_' + device,
|
},
|
||||||
SR: state.pool.default_SR,
|
poolId => ({ $pool }) =>
|
||||||
type: 'system'
|
$pool === poolId
|
||||||
}] })
|
)
|
||||||
}
|
_getIsInResourceSet = createSelector(
|
||||||
_removeVdi = index => {
|
() => {
|
||||||
const { VDIs } = this.state.state
|
const { resourceSet } = this.state
|
||||||
this._setState({ VDIs: [ ...VDIs.slice(0, index), ...VDIs.slice(index + 1) ] })
|
return resourceSet && resourceSet.objectsByType
|
||||||
}
|
},
|
||||||
_addInterface = () => {
|
objectsByType => (obj, objType) => {
|
||||||
const networkId = this._getDefaultNetworkId()
|
const [id, type] = isObject(obj) ? [obj.id, obj.type] : [obj, objType]
|
||||||
this._setState({ VIFs: [ ...this.state.state.VIFs, {
|
return objectsByType && includes(map(objectsByType[type], object => object.id), id)
|
||||||
id: this.getUniqueId(),
|
}
|
||||||
network: networkId
|
)
|
||||||
}] })
|
_getCanOperate = createSelector(
|
||||||
}
|
() => this.state.permissions,
|
||||||
_removeInterface = index => {
|
permissions => ({ id }) =>
|
||||||
const { VIFs } = this.state.state
|
this.props.isAdmin || permissions && permissions[id] && permissions[id].operate
|
||||||
this._setState({ VIFs: [ ...VIFs.slice(0, index), ...VIFs.slice(index + 1) ] })
|
)
|
||||||
|
_getVmPredicate = createSelector(
|
||||||
|
this._getIsInPool,
|
||||||
|
this._getIsInResourceSet,
|
||||||
|
(isInPool, isInResourceSet) => vm =>
|
||||||
|
isInResourceSet(vm) || isInPool(vm)
|
||||||
|
)
|
||||||
|
_getSrPredicate = createSelector(
|
||||||
|
this._getIsInPool,
|
||||||
|
this._getIsInResourceSet,
|
||||||
|
(isInPool, isInResourceSet) => disk =>
|
||||||
|
(isInResourceSet(disk) || isInPool(disk)) && disk.content_type !== 'iso' && disk.size > 0
|
||||||
|
)
|
||||||
|
_getIsoPredicate = () => disk =>
|
||||||
|
disk.content_type === 'iso'
|
||||||
|
_getNetworkPredicate = createSelector(
|
||||||
|
this._getIsInPool,
|
||||||
|
this._getIsInResourceSet,
|
||||||
|
(isInPool, isInResourceSet) => network =>
|
||||||
|
isInResourceSet(network) || isInPool(network)
|
||||||
|
)
|
||||||
|
_getPoolNetworks = createSelector(
|
||||||
|
() => this.props.networks,
|
||||||
|
() => {
|
||||||
|
const { pool } = this.state
|
||||||
|
return pool && pool.id
|
||||||
|
},
|
||||||
|
(networks, poolId) => filter(networks, network => network.$pool === poolId)
|
||||||
|
)
|
||||||
|
_getOperatablePools = createFilter(
|
||||||
|
() => this.props.pools,
|
||||||
|
this._getCanOperate,
|
||||||
|
[ (pool, canOperate) => canOperate(pool) ]
|
||||||
|
)
|
||||||
|
_getDefaultNetworkId = () => {
|
||||||
|
const { resourceSet } = this.state
|
||||||
|
if (resourceSet) {
|
||||||
|
return resourceSet.objectsByType['network'][0].id
|
||||||
|
}
|
||||||
|
const network = find(this._getPoolNetworks(), network => {
|
||||||
|
const pif = getObject(store.getState(), network.PIFs[0])
|
||||||
|
return pif && pif.management
|
||||||
|
})
|
||||||
|
return network && network.id
|
||||||
}
|
}
|
||||||
|
|
||||||
// if index: the element to be modified is an array/object
|
// On change -------------------------------------------------------------------
|
||||||
// if stateObjectProp: the array/object contains objects and stateObjectProp needs to be modified
|
/*
|
||||||
// if targetObjectProp: the event target value is an object and the new value is the targetObjectProp of this object
|
* if index: the element to be modified should be an array/object
|
||||||
|
* if stateObjectProp: the array/object contains objects and stateObjectProp needs to be modified
|
||||||
|
* if targetObjectProp: the event target value is an object and the new value is the targetObjectProp of this object
|
||||||
|
*
|
||||||
|
* SCHEMA: EXAMPLE:
|
||||||
|
*
|
||||||
|
* state: { this.state.state: {
|
||||||
|
* [prop]: { existingDisks: {
|
||||||
|
* [index]: { 0: {
|
||||||
|
* [stateObjectProp]: TO BE MODIFIED name_label: TO BE MODIFIED
|
||||||
|
* ... name_description
|
||||||
|
* } ...
|
||||||
|
* ... }
|
||||||
|
* } 1: {...}
|
||||||
|
* ... }
|
||||||
|
* } }
|
||||||
|
*/
|
||||||
_getOnChange (prop, index, stateObjectProp, targetObjectProp) {
|
_getOnChange (prop, index, stateObjectProp, targetObjectProp) {
|
||||||
return event => {
|
return event => {
|
||||||
let value
|
let value
|
||||||
if (index !== undefined) {
|
if (index !== undefined) { // The element should be an array or an object
|
||||||
value = this.state.state[prop]
|
value = this.state.state[prop]
|
||||||
value = isArray(value) ? [ ...value ] : { ...value }
|
value = isArray(value) ? [ ...value ] : { ...value } // Clone the element
|
||||||
let eventValue = getEventValue(event)
|
let eventValue = getEventValue(event)
|
||||||
eventValue = targetObjectProp ? eventValue[targetObjectProp] : eventValue
|
eventValue = targetObjectProp ? eventValue[targetObjectProp] : eventValue // Get the new value
|
||||||
if (value[index] && stateObjectProp) {
|
if (value[index] && stateObjectProp) {
|
||||||
value[index][stateObjectProp] = eventValue
|
value[index][stateObjectProp] = eventValue
|
||||||
} else {
|
} else {
|
||||||
@ -390,20 +451,106 @@ export default class NewVm extends BaseComponent {
|
|||||||
this._setState({ [prop]: value })
|
this._setState({ [prop]: value })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_updateNbVms = () => {
|
||||||
|
const { nbVms, name_label, nameLabels } = this.state.state
|
||||||
|
const nbVmsClamped = clamp(nbVms, NB_VMS_MIN, NB_VMS_MAX)
|
||||||
|
const newNameLabels = [ ...nameLabels ]
|
||||||
|
if (nbVmsClamped < nameLabels.length) {
|
||||||
|
this._setState({ nameLabels: slice(newNameLabels, 0, nbVmsClamped) })
|
||||||
|
} else {
|
||||||
|
for (let i = nameLabels.length + 1; i <= nbVmsClamped; i++) {
|
||||||
|
newNameLabels.push(`${name_label || 'VM'}_${i}`)
|
||||||
|
}
|
||||||
|
this._setState({ nameLabels: newNameLabels })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_updateNameLabels = () => {
|
||||||
|
const { name_label, nameLabels } = this.state.state
|
||||||
|
const nbVms = nameLabels.length
|
||||||
|
const newNameLabels = []
|
||||||
|
for (let i = 1; i <= nbVms; i++) {
|
||||||
|
newNameLabels.push(`${name_label || 'VM'}_${i}`)
|
||||||
|
}
|
||||||
|
this._setState({ nameLabels: newNameLabels })
|
||||||
|
}
|
||||||
|
_selectResourceSet = resourceSet =>
|
||||||
|
this._reset({ pool: undefined, resourceSet })
|
||||||
|
_selectPool = pool =>
|
||||||
|
this._reset({ pool, resourceSet: undefined })
|
||||||
|
_addVdi = () => {
|
||||||
|
const { pool, state } = this.state
|
||||||
|
const device = String(this.getUniqueId())
|
||||||
|
this._setState({ VDIs: [ ...state.VDIs, {
|
||||||
|
device,
|
||||||
|
name_description: 'Created by XO',
|
||||||
|
name_label: (state.name_label || 'disk') + '_' + device,
|
||||||
|
SR: pool && pool.default_SR,
|
||||||
|
type: 'system'
|
||||||
|
}] })
|
||||||
|
}
|
||||||
|
_removeVdi = index => {
|
||||||
|
const { VDIs } = this.state.state
|
||||||
|
this._setState({ VDIs: [ ...VDIs.slice(0, index), ...VDIs.slice(index + 1) ] })
|
||||||
|
}
|
||||||
|
_addInterface = () => {
|
||||||
|
const networkId = this._getDefaultNetworkId()
|
||||||
|
this._setState({ VIFs: [ ...this.state.state.VIFs, {
|
||||||
|
id: this.getUniqueId(),
|
||||||
|
network: networkId
|
||||||
|
}] })
|
||||||
|
}
|
||||||
|
_removeInterface = index => {
|
||||||
|
const { VIFs } = this.state.state
|
||||||
|
this._setState({ VIFs: [ ...VIFs.slice(0, index), ...VIFs.slice(index + 1) ] })
|
||||||
|
}
|
||||||
|
|
||||||
_getRedirectionUrl = id =>
|
_getRedirectionUrl = id =>
|
||||||
this.state.state.multipleVms ? '/home' : `/vms/${id}`
|
this.state.state.multipleVms ? '/home' : `/vms/${id}`
|
||||||
|
|
||||||
|
// MAIN ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_renderHeader = () => {
|
||||||
|
const { pool, resourceSet, resourceSets } = this.state
|
||||||
|
const showSelectPool = !isEmpty(this._getOperatablePools())
|
||||||
|
const showSelectResourceSet = !this.props.isAdmin && !isEmpty(resourceSets)
|
||||||
|
const selectPool = <span className={styles.inlineSelect}>
|
||||||
|
<SelectPool
|
||||||
|
onChange={this._selectPool}
|
||||||
|
predicate={this._getCanOperate()}
|
||||||
|
value={pool}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
const selectResourceSet = <span className={styles.inlineSelect}>
|
||||||
|
<SelectResourceSet
|
||||||
|
onChange={this._selectResourceSet}
|
||||||
|
value={resourceSet}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
return <Container>
|
||||||
|
<Row>
|
||||||
|
<Col mediumSize={12}>
|
||||||
|
<h2>
|
||||||
|
{showSelectPool && showSelectResourceSet
|
||||||
|
? _('newVmCreateNewVmOn2', {
|
||||||
|
select1: selectPool,
|
||||||
|
select2: selectResourceSet
|
||||||
|
})
|
||||||
|
: showSelectPool || showSelectResourceSet
|
||||||
|
? _('newVmCreateNewVmOn', {
|
||||||
|
select: isEmpty(this._getOperatablePools()) ? selectResourceSet : selectPool
|
||||||
|
})
|
||||||
|
: _('newVmCreateNewVmNoPermission')
|
||||||
|
}
|
||||||
|
</h2>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return <div>
|
const { resourceSet, pool } = this.state
|
||||||
<h1>
|
return <Page header={this._renderHeader()}>
|
||||||
{_('newVmCreateNewVmOn', {
|
{(pool || resourceSet) && <form id='vmCreation'>
|
||||||
pool: <span className={styles.inlineSelect}>
|
|
||||||
<SelectPool value={this.state.state.pool} onChange={this._selectPool} />
|
|
||||||
</span>
|
|
||||||
})}
|
|
||||||
</h1>
|
|
||||||
{this.state.state.pool && <form id='vmCreation'>
|
|
||||||
<Wizard>
|
<Wizard>
|
||||||
{this._renderInfo()}
|
{this._renderInfo()}
|
||||||
{this._renderPerformances()}
|
{this._renderPerformances()}
|
||||||
@ -440,9 +587,11 @@ export default class NewVm extends BaseComponent {
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</form>}
|
</form>}
|
||||||
</div>
|
</Page>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// INFO ------------------------------------------------------------------------
|
||||||
|
|
||||||
_renderInfo = () => {
|
_renderInfo = () => {
|
||||||
const {
|
const {
|
||||||
multipleVms,
|
multipleVms,
|
||||||
@ -456,12 +605,18 @@ export default class NewVm extends BaseComponent {
|
|||||||
<SectionContent>
|
<SectionContent>
|
||||||
<Item label='newVmTemplateLabel'>
|
<Item label='newVmTemplateLabel'>
|
||||||
<span className={styles.inlineSelect}>
|
<span className={styles.inlineSelect}>
|
||||||
<SelectVmTemplate
|
{this.state.pool ? <SelectVmTemplate
|
||||||
onChange={this._initTemplate}
|
onChange={this._initTemplate}
|
||||||
placeholder={_('newVmSelectTemplate')}
|
placeholder={_('newVmSelectTemplate')}
|
||||||
predicate={this._getIsInPool()}
|
predicate={this._getVmPredicate()}
|
||||||
value={template}
|
value={template}
|
||||||
/>
|
/>
|
||||||
|
: <SelectResourceSetsVmTemplate
|
||||||
|
onChange={this._initTemplate}
|
||||||
|
placeholder={_('newVmSelectTemplate')}
|
||||||
|
resourceSet={this.state.resourceSet}
|
||||||
|
value={template}
|
||||||
|
/>}
|
||||||
</span>
|
</span>
|
||||||
</Item>
|
</Item>
|
||||||
<Item label='newVmNameLabel'>
|
<Item label='newVmNameLabel'>
|
||||||
@ -558,6 +713,8 @@ export default class NewVm extends BaseComponent {
|
|||||||
return CPUs && memory !== undefined
|
return CPUs && memory !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// INSTALL SETTINGS ------------------------------------------------------------
|
||||||
|
|
||||||
_renderInstallSettings = () => {
|
_renderInstallSettings = () => {
|
||||||
const { template } = this.state.state
|
const { template } = this.state.state
|
||||||
if (!template) {
|
if (!template) {
|
||||||
@ -570,7 +727,6 @@ export default class NewVm extends BaseComponent {
|
|||||||
installIso,
|
installIso,
|
||||||
installMethod,
|
installMethod,
|
||||||
installNetwork,
|
installNetwork,
|
||||||
pool,
|
|
||||||
pv_args,
|
pv_args,
|
||||||
sshKey
|
sshKey
|
||||||
} = this.state.state
|
} = this.state.state
|
||||||
@ -644,12 +800,19 @@ export default class NewVm extends BaseComponent {
|
|||||||
<span>{_('newVmIsoDvdLabel')}</span>
|
<span>{_('newVmIsoDvdLabel')}</span>
|
||||||
|
|
||||||
<span className={styles.inlineSelect}>
|
<span className={styles.inlineSelect}>
|
||||||
<SelectVdi
|
{this.state.pool ? <SelectVdi
|
||||||
disabled={installMethod !== 'ISO'}
|
disabled={installMethod !== 'ISO'}
|
||||||
onChange={this._getOnChange('installIso')}
|
onChange={this._getOnChange('installIso')}
|
||||||
srPredicate={sr => sr.$pool === pool.id && sr.SR_type === 'iso'}
|
srPredicate={this._getIsoPredicate()}
|
||||||
value={installIso}
|
value={installIso}
|
||||||
/>
|
/>
|
||||||
|
: <SelectResourceSetsVdi
|
||||||
|
disabled={installMethod !== 'ISO'}
|
||||||
|
onChange={this._getOnChange('installIso')}
|
||||||
|
resourceSet={this.state.resourceSet}
|
||||||
|
srPredicate={this._getIsoPredicate()}
|
||||||
|
value={installIso}
|
||||||
|
/>}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</Item>
|
</Item>
|
||||||
@ -729,11 +892,14 @@ export default class NewVm extends BaseComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// INTERFACES ------------------------------------------------------------------
|
||||||
|
|
||||||
_renderInterfaces = () => {
|
_renderInterfaces = () => {
|
||||||
const { formatMessage } = this.props.intl
|
const { formatMessage } = this.props.intl
|
||||||
const {
|
const {
|
||||||
VIFs
|
state: { VIFs },
|
||||||
} = this.state.state
|
pool
|
||||||
|
} = this.state
|
||||||
return <Section icon='new-vm-interfaces' title='newVmInterfacesPanel' done={this._isInterfacesDone()}>
|
return <Section icon='new-vm-interfaces' title='newVmInterfacesPanel' done={this._isInterfacesDone()}>
|
||||||
<SectionContent column>
|
<SectionContent column>
|
||||||
{map(VIFs, (vif, index) => <div key={index}>
|
{map(VIFs, (vif, index) => <div key={index}>
|
||||||
@ -750,11 +916,16 @@ export default class NewVm extends BaseComponent {
|
|||||||
</Item>
|
</Item>
|
||||||
<Item label='newVmNetworkLabel'>
|
<Item label='newVmNetworkLabel'>
|
||||||
<span className={styles.inlineSelect}>
|
<span className={styles.inlineSelect}>
|
||||||
<SelectNetwork
|
{pool ? <SelectNetwork
|
||||||
onChange={this._getOnChange('VIFs', index, 'network', 'id')}
|
onChange={this._getOnChange('VIFs', index, 'network', 'id')}
|
||||||
predicate={this._getIsInPool()}
|
predicate={this._getNetworkPredicate()}
|
||||||
value={vif.network}
|
value={vif.network}
|
||||||
/>
|
/>
|
||||||
|
: <SelectResourceSetsNetwork
|
||||||
|
onChange={this._getOnChange('VIFs', index, 'network', 'id')}
|
||||||
|
resourceSet={this.state.resourceSet}
|
||||||
|
value={vif.network}
|
||||||
|
/>}
|
||||||
</span>
|
</span>
|
||||||
</Item>
|
</Item>
|
||||||
<Item>
|
<Item>
|
||||||
@ -780,25 +951,35 @@ export default class NewVm extends BaseComponent {
|
|||||||
vif.network
|
vif.network
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DISKS -----------------------------------------------------------------------
|
||||||
|
|
||||||
_renderDisks = () => {
|
_renderDisks = () => {
|
||||||
const {
|
const {
|
||||||
configDrive,
|
state: { configDrive,
|
||||||
existingDisks,
|
existingDisks,
|
||||||
VDIs
|
VDIs },
|
||||||
} = this.state.state
|
pool
|
||||||
|
} = this.state
|
||||||
|
let i = 0
|
||||||
return <Section icon='new-vm-disks' title='newVmDisksPanel' done={this._isDisksDone()}>
|
return <Section icon='new-vm-disks' title='newVmDisksPanel' done={this._isDisksDone()}>
|
||||||
<SectionContent column>
|
<SectionContent column>
|
||||||
|
|
||||||
{/* Existing disks */}
|
{/* Existing disks */}
|
||||||
{map(toArray(existingDisks), (disk, index) => <div key={index}>
|
{map(existingDisks, (disk, index) => <div key={i}>
|
||||||
<LineItem>
|
<LineItem>
|
||||||
<Item label='newVmSrLabel'>
|
<Item label='newVmSrLabel'>
|
||||||
<span className={styles.inlineSelect}>
|
<span className={styles.inlineSelect}>
|
||||||
<SelectSr
|
{pool ? <SelectSr
|
||||||
onChange={this._getOnChange('existingDisks', index, '$SR', 'id')}
|
onChange={this._getOnChange('existingDisks', index, '$SR', 'id')}
|
||||||
predicate={this._getSrPredicate()}
|
predicate={this._getSrPredicate()}
|
||||||
value={disk.$SR}
|
value={disk.$SR}
|
||||||
/>
|
/>
|
||||||
|
: <SelectResourceSetsSr
|
||||||
|
onChange={this._getOnChange('existingDisks', index, '$SR', 'id')}
|
||||||
|
predicate={this._getSrPredicate()}
|
||||||
|
resourceSet={this.state.resourceSet}
|
||||||
|
value={disk.$SR}
|
||||||
|
/>}
|
||||||
</span>
|
</span>
|
||||||
</Item>
|
</Item>
|
||||||
{' '}
|
{' '}
|
||||||
@ -827,7 +1008,7 @@ export default class NewVm extends BaseComponent {
|
|||||||
/>
|
/>
|
||||||
</Item>
|
</Item>
|
||||||
</LineItem>
|
</LineItem>
|
||||||
{index < size(existingDisks) + VDIs.length - 1 && <hr />}
|
{i++ < size(existingDisks) + VDIs.length - 1 && <hr />}
|
||||||
</div>)}
|
</div>)}
|
||||||
|
|
||||||
{/* VDIs */}
|
{/* VDIs */}
|
||||||
@ -835,11 +1016,17 @@ export default class NewVm extends BaseComponent {
|
|||||||
<LineItem>
|
<LineItem>
|
||||||
<Item label='newVmSrLabel'>
|
<Item label='newVmSrLabel'>
|
||||||
<span className={styles.inlineSelect}>
|
<span className={styles.inlineSelect}>
|
||||||
<SelectSr
|
{pool ? <SelectSr
|
||||||
onChange={this._getOnChange('VDIs', index, 'SR', 'id')}
|
onChange={this._getOnChange('VDIs', index, 'SR', 'id')}
|
||||||
predicate={this._getSrPredicate()}
|
predicate={this._getSrPredicate()}
|
||||||
value={vdi.SR}
|
value={vdi.SR}
|
||||||
/>
|
/>
|
||||||
|
: <SelectResourceSetsSr
|
||||||
|
onChange={this._getOnChange('VDIs', index, 'SR', 'id')}
|
||||||
|
predicate={this._getSrPredicate()}
|
||||||
|
resourceSet={this.state.resourceSet}
|
||||||
|
value={vdi.SR}
|
||||||
|
/>}
|
||||||
</span>
|
</span>
|
||||||
</Item>
|
</Item>
|
||||||
{' '}
|
{' '}
|
||||||
@ -902,6 +1089,8 @@ export default class NewVm extends BaseComponent {
|
|||||||
vdi.$SR && vdi.name_label && vdi.size !== undefined
|
vdi.$SR && vdi.name_label && vdi.size !== undefined
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SUMMARY ---------------------------------------------------------------------
|
||||||
|
|
||||||
_renderSummary = () => {
|
_renderSummary = () => {
|
||||||
const {
|
const {
|
||||||
bootAfterCreate,
|
bootAfterCreate,
|
||||||
|
@ -32,7 +32,8 @@ import {
|
|||||||
} from 'select-objects'
|
} from 'select-objects'
|
||||||
import {
|
import {
|
||||||
connectStore,
|
connectStore,
|
||||||
formatSize
|
formatSize,
|
||||||
|
resolveResourceSets
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import {
|
import {
|
||||||
createResourceSet,
|
createResourceSet,
|
||||||
@ -42,8 +43,7 @@ import {
|
|||||||
} from 'xo'
|
} from 'xo'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Subjects,
|
Subjects
|
||||||
resolveResourceSets
|
|
||||||
} from '../helpers'
|
} from '../helpers'
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
@ -8,9 +8,13 @@ import _ from 'intl'
|
|||||||
import map from 'lodash/map'
|
import map from 'lodash/map'
|
||||||
import renderXoItem from 'render-xo-item'
|
import renderXoItem from 'render-xo-item'
|
||||||
import { Container, Row, Col } from 'grid'
|
import { Container, Row, Col } from 'grid'
|
||||||
import { formatSize } from 'utils'
|
|
||||||
import { subscribeResourceSets } from 'xo'
|
import { subscribeResourceSets } from 'xo'
|
||||||
|
|
||||||
|
import {
|
||||||
|
formatSize,
|
||||||
|
resolveResourceSets
|
||||||
|
} from 'utils'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardBlock,
|
CardBlock,
|
||||||
@ -18,8 +22,7 @@ import {
|
|||||||
} from 'card'
|
} from 'card'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Subjects,
|
Subjects
|
||||||
resolveResourceSets
|
|
||||||
} from '../helpers'
|
} from '../helpers'
|
||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import _ from 'intl'
|
import _ from 'intl'
|
||||||
import forEach from 'lodash/forEach'
|
|
||||||
import keyBy from 'lodash/keyBy'
|
import keyBy from 'lodash/keyBy'
|
||||||
import map from 'lodash/map'
|
import map from 'lodash/map'
|
||||||
import propTypes from 'prop-types'
|
import propTypes from 'prop-types'
|
||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
import renderXoItem from 'render-xo-item'
|
import renderXoItem from 'render-xo-item'
|
||||||
import store from 'store'
|
|
||||||
import { getObject } from 'selectors'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
subscribeGroups,
|
subscribeGroups,
|
||||||
@ -15,41 +12,6 @@ import {
|
|||||||
|
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
|
|
||||||
export const resolveResourceSets = resourceSets => (
|
|
||||||
map(resourceSets, resourceSet => {
|
|
||||||
const { objects, ...attrs } = resourceSet
|
|
||||||
const resolvedObjects = {}
|
|
||||||
const resolvedSet = {
|
|
||||||
...attrs,
|
|
||||||
missingObjects: [],
|
|
||||||
objectsByType: resolvedObjects
|
|
||||||
}
|
|
||||||
const state = store.getState()
|
|
||||||
|
|
||||||
forEach(objects, id => {
|
|
||||||
const object = getObject(state, id)
|
|
||||||
|
|
||||||
// Error, missing resource.
|
|
||||||
if (!object) {
|
|
||||||
resolvedSet.missingObjects.push(id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { type } = object
|
|
||||||
|
|
||||||
if (!resolvedObjects[type]) {
|
|
||||||
resolvedObjects[type] = [ object ]
|
|
||||||
} else {
|
|
||||||
resolvedObjects[type].push(object)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return resolvedSet
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// ===================================================================
|
|
||||||
|
|
||||||
@propTypes({
|
@propTypes({
|
||||||
subjects: propTypes.array.isRequired
|
subjects: propTypes.array.isRequired
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user