feat(new-vm): self service with resource sets

Fixes #1155
This commit is contained in:
Pierre Donias 2016-07-13 09:59:11 +02:00 committed by Olivier Lambert
parent 539d136936
commit 0d47332526
11 changed files with 618 additions and 188 deletions

View File

@ -310,7 +310,8 @@ export default {
alarmPool: 'Pool',
alarmRemoveAll: 'Supprimer toutes les alarmes',
// ----- 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',
newVmNameLabel: 'Nom',
newVmTemplateLabel: 'Modèle',

View File

@ -126,6 +126,11 @@ var messages = {
selectPifs: 'Select PIF(s)…',
selectPools: 'Select Pool(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)…',
selectVms: 'Select VM(s)…',
selectVmTemplates: 'Select VM template(s)…',
@ -583,7 +588,9 @@ var messages = {
alarmRemoveAll: 'Remove all alarms',
// ----- 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',
newVmNameLabel: 'Name',
newVmTemplateLabel: 'Template',
@ -622,6 +629,7 @@ var messages = {
newVmCreateVms: 'Create VMs',
newVmCreateVmsConfirm: 'Are you sure you want to create {nbVms} VMs?',
newVmMultipleVms: 'Multiple VMs:',
newVmSelectResourceSet: 'Select a resource set:',
// ----- Self -----
resourceSets: 'Resource sets',

View File

@ -108,6 +108,11 @@ const xoItemToRender = {
<Icon icon='user' /> {user.email}
</span>
),
resourceSet: resourceSet => (
<span>
<Icon icon='resource-set' /> {resourceSet.name}
</span>
),
// XO objects.
pool: pool => (

View File

@ -2,12 +2,14 @@ import React from 'react'
import assign from 'lodash/assign'
import classNames from 'classnames'
import filter from 'lodash/filter'
import flatten from 'lodash/flatten'
import forEach from 'lodash/forEach'
import groupBy from 'lodash/groupBy'
import keyBy from 'lodash/keyBy'
import keys from 'lodash/keys'
import map from 'lodash/map'
import sortBy from 'lodash/sortBy'
import store from 'store'
import { parse as parseRemote } from 'xo-remote-parser'
import _ from './intl'
@ -19,16 +21,19 @@ import {
createFilter,
createGetObjectsOfType,
createGetTags,
createSelector
createSelector,
getObject
} from './selectors'
import {
connectStore,
mapPlus
mapPlus,
resolveResourceSets
} from './utils'
import {
isSrWritable,
subscribeGroups,
subscribeRemotes,
subscribeResourceSets,
subscribeRoles,
subscribeUsers
} from './xo'
@ -580,3 +585,184 @@ export const SelectRemote = makeSubscriptionSelect(subscriber => {
return unsubscribeRemotes
}, { 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()}
/>
)
}
}

View File

@ -245,13 +245,18 @@ const _getPermissionsPredicate = invoke(() => {
// Creates an object selector from an id selector.
export const createGetObject = (idSelector = _getId) =>
(state, props) => {
(state, props, useResourceSet) => {
const object = state.objects.all[idSelector(state, props)]
if (!object) {
return
}
if (useResourceSet) {
return object
}
const predicate = _getPermissionsPredicate(state)
if (!predicate) {
if (predicate == null) {
return object // no filtering

View File

@ -14,7 +14,9 @@ import map from 'lodash/map'
import mapValues from 'lodash/mapValues'
import React from 'react'
import replace from 'lodash/replace'
import store from 'store'
import { connect } from 'react-redux'
import { getObject } from 'selectors'
import BaseComponent from './base-component'
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

View File

@ -2,15 +2,22 @@ import _ from 'intl'
import Component from 'base-component'
import classNames from 'classnames'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import Link from 'link'
import map from 'lodash/map'
import React from 'react'
import Tooltip from 'tooltip'
import { Button } from 'react-bootstrap-4/lib'
import { connectStore, noop, getXoaPlan } from 'utils'
import { createGetObjectsOfType, getLang, getUser } from 'selectors'
import { signOut } from 'xo'
import { signOut, subscribePermissions, subscribeResourceSets } from 'xo'
import { UpdateTag } from '../xoa-updates'
import {
createFilter,
createGetObjectsOfType,
createSelector,
getLang,
getUser
} from 'selectors'
import styles from './index.css'
@ -20,10 +27,10 @@ import styles from './index.css'
// There are currently issues between context updates (used by
// react-intl) and pure components.
lang: getLang,
nTasks: createGetObjectsOfType('task').count(
[ task => task.status === 'pending' ]
),
pools: createGetObjectsOfType('pool'),
user: getUser
}), {
withRef: true
@ -40,12 +47,37 @@ export default class Menu extends Component {
window.removeEventListener('resize', updateCollapsed)
this._removeListener = noop
}
this._unsubscribeResourceSets = subscribeResourceSets(resourceSets => {
this.setState({
resourceSets
})
})
this._unsubscribePermissions = subscribePermissions(permissions => {
this.setState({
permissions
})
})
}
componentWillUnmount () {
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 () {
return this.refs.content.offsetHeight
}
@ -58,6 +90,8 @@ export default class Menu extends Component {
render () {
const { nTasks, user } = this.props
const isAdmin = user && user.permission === 'admin'
const noOperatablePools = this._getNoOperatablePools()
const noResourceSets = isEmpty(this.state.resourceSets)
const items = [
{ 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: '/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' },
isAdmin && { to: '/new/sr', icon: 'menu-new-sr', label: 'newSrPage' },
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' }
]}
]

View File

@ -10,26 +10,37 @@ import find from 'lodash/find'
import forEach from 'lodash/forEach'
import getEventValue from 'get-event-value'
import Icon from 'icon'
import includes from 'lodash/includes'
import isArray from 'lodash/isArray'
import isEmpty from 'lodash/isEmpty'
import isObject from 'lodash/isObject'
import map from 'lodash/map'
import Page from '../page'
import React from 'react'
import size from 'lodash/size'
import slice from 'lodash/slice'
import store from 'store'
import toArray from 'lodash/toArray'
import Wizard, { Section } from 'wizard'
import { Button } from 'react-bootstrap-4/lib'
import { Container, Row, Col } from 'grid'
import { injectIntl } from 'react-intl'
import {
createVm,
createVms,
getCloudInitConfig
getCloudInitConfig,
subscribePermissions,
subscribeResourceSets
} from 'xo'
import {
SelectVdi,
SelectNetwork,
SelectPool,
SelectResourceSet,
SelectResourceSetsNetwork,
SelectResourceSetsSr,
SelectResourceSetsVdi,
SelectResourceSetsVmTemplate,
SelectSr,
SelectVdi,
SelectVmTemplate
} from 'select-objects'
import {
@ -39,12 +50,15 @@ import {
import {
connectStore,
formatSize,
noop
noop,
resolveResourceSets
} from 'utils'
import {
createFilter,
createSelector,
createGetObject,
createGetObjectsOfType
createGetObjectsOfType,
getUser
} from 'selectors'
import styles from './index.css'
@ -54,6 +68,9 @@ const NB_VMS_MIN = 2
const NB_VMS_MAX = 100
/* eslint-disable camelcase */
// Sub-components --------------------------------------------------------------
const SectionContent = ({ summary, column, children }) => (
<div className={classNames(
'form-inline',
@ -63,13 +80,11 @@ const SectionContent = ({ summary, column, children }) => (
{children}
</div>
)
const LineItem = ({ children }) => (
<div className={styles.lineItem}>
{children}
</div>
)
const Item = ({ label, children }) => (
<span className={styles.item}>
{label && <span>{_(label)}&nbsp;</span>}
@ -77,10 +92,17 @@ const Item = ({ label, children }) => (
</span>
)
// -----------------------------------------------------------------------------
const getObject = createGetObject((_, id) => id)
@connectStore(() => ({
isAdmin: createSelector(
getUser,
user => user && user.permission === 'admin'
),
networks: createGetObjectsOfType('network').sort(),
pools: createGetObjectsOfType('pool'),
templates: createGetObjectsOfType('VM-template').sort()
}))
@injectIntl
@ -89,8 +111,8 @@ export default class NewVm extends BaseComponent {
super()
this._uniqueId = 0
// NewVm's state is stored in this.state.state instead of this.state
// so it can be emptied easily with this.setState(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: {} })
this.state = { state: {} }
}
@ -98,49 +120,34 @@ export default class NewVm extends BaseComponent {
this._reset()
}
getPoolNetworks = createSelector(
() => this.props.networks,
() => {
const { pool } = this.state.state
return pool && pool.id
},
(networks, poolId) => filter(networks, network => network.$pool === poolId)
)
componentWillMount () {
this._unsubscribeResourceSets = subscribeResourceSets(resourceSets => {
this.setState({
resourceSets: resolveResourceSets(resourceSets)
})
})
this._unsubscribePermissions = subscribePermissions(permissions => {
this.setState({
permissions
})
})
}
componentWillUnmount () {
this._unsubscribeResourceSets()
this._unsubscribePermissions()
}
// Utils -----------------------------------------------------------------------
getUniqueId () {
return this._uniqueId++
}
get _isDiskTemplate () {
const { template } = this.state.state
return template &&
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) => {
this.setState({ state: {
...this.state.state,
@ -150,23 +157,30 @@ export default class NewVm extends BaseComponent {
_replaceState = (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({
bootAfterCreate: true,
configDrive: false,
CPUs: '',
cpuWeight: 1,
existingDisks: {},
fastClone: true,
multipleVms: false,
name_label: '',
name_description: '',
nameLabels: map(Array(NB_VMS_MIN), (_, index) => `VM_${index + 1}`),
nbVms: NB_VMS_MIN,
pool: pool || this.state.state.pool,
VDIs: [],
VIFs: []
})
}
_create = () => {
const { state } = this.state
const { resourceSet, state } = this.state
let installation
switch (state.installMethod) {
case 'ISO':
@ -205,13 +219,14 @@ export default class NewVm extends BaseComponent {
}
const data = {
clone: this._isDiskTemplate && state.fastClone,
clone: !this.isDiskTemplate && state.fastClone,
existingDisks: state.existingDisks,
installation,
name_label: state.name_label,
template: state.template.id,
VDIs: state.VDIs,
VIFs: state.VIFs,
resourceSet: resourceSet && resourceSet.id,
// TODO: To be added in xo-server
// vm.set parameters
CPUs: state.CPUs,
@ -227,28 +242,6 @@ export default class NewVm extends BaseComponent {
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 => {
if (!template) {
return this._reset()
@ -257,6 +250,8 @@ export default class NewVm extends BaseComponent {
this._setState({ template })
const storeState = store.getState()
const _isInResourceSet = this._getIsInResourceSet()
const { pool, resourceSet, state } = this.state
const existingDisks = {}
forEach(template.$VBDs, vbdId => {
@ -270,7 +265,9 @@ export default class NewVm extends BaseComponent {
name_label: vdi.name_label,
name_description: vdi.name_description,
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)
VIFs.push({
id: this.getUniqueId(),
network: vif.$network
network: pool || _isInResourceSet(vif.$network, 'network')
? vif.$network
: resourceSet.objectsByType['network'][0].id
})
})
if (VIFs.length === 0) {
@ -290,7 +289,6 @@ export default class NewVm extends BaseComponent {
network: networkId
})
}
const { state } = this.state
const name_label = state.name_label === '' || !state.name_labelHasChanged ? template.name_label : state.name_label
this._setState({
// infos
@ -316,55 +314,118 @@ export default class NewVm extends BaseComponent {
device,
name_description: disk.name_description || 'Created by XO',
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(
cloudConfig => this._setState({ cloudConfig }),
noop
)
if (template.name_label === 'CoreOS') {
getCloudInitConfig(template.id).then(
cloudConfig => this._setState({ cloudConfig }),
noop
)
}
}
_addVdi = () => {
const { 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: state.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) ] })
// Selectors -------------------------------------------------------------------
_getIsInPool = createSelector(
() => {
const { pool } = this.state
return pool && pool.id
},
poolId => ({ $pool }) =>
$pool === poolId
)
_getIsInResourceSet = createSelector(
() => {
const { resourceSet } = this.state
return resourceSet && resourceSet.objectsByType
},
objectsByType => (obj, objType) => {
const [id, type] = isObject(obj) ? [obj.id, obj.type] : [obj, objType]
return objectsByType && includes(map(objectsByType[type], object => object.id), id)
}
)
_getCanOperate = createSelector(
() => this.state.permissions,
permissions => ({ id }) =>
this.props.isAdmin || permissions && permissions[id] && permissions[id].operate
)
_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
// 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
// On change -------------------------------------------------------------------
/*
* 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) {
return event => {
let value
if (index !== undefined) {
if (index !== undefined) { // The element should be an array or an object
value = this.state.state[prop]
value = isArray(value) ? [ ...value ] : { ...value }
value = isArray(value) ? [ ...value ] : { ...value } // Clone the element
let eventValue = getEventValue(event)
eventValue = targetObjectProp ? eventValue[targetObjectProp] : eventValue
eventValue = targetObjectProp ? eventValue[targetObjectProp] : eventValue // Get the new value
if (value[index] && stateObjectProp) {
value[index][stateObjectProp] = eventValue
} else {
@ -390,20 +451,106 @@ export default class NewVm extends BaseComponent {
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 =>
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 () {
return <div>
<h1>
{_('newVmCreateNewVmOn', {
pool: <span className={styles.inlineSelect}>
<SelectPool value={this.state.state.pool} onChange={this._selectPool} />
</span>
})}
</h1>
{this.state.state.pool && <form id='vmCreation'>
const { resourceSet, pool } = this.state
return <Page header={this._renderHeader()}>
{(pool || resourceSet) && <form id='vmCreation'>
<Wizard>
{this._renderInfo()}
{this._renderPerformances()}
@ -440,9 +587,11 @@ export default class NewVm extends BaseComponent {
</ActionButton>
</div>
</form>}
</div>
</Page>
}
// INFO ------------------------------------------------------------------------
_renderInfo = () => {
const {
multipleVms,
@ -456,12 +605,18 @@ export default class NewVm extends BaseComponent {
<SectionContent>
<Item label='newVmTemplateLabel'>
<span className={styles.inlineSelect}>
<SelectVmTemplate
{this.state.pool ? <SelectVmTemplate
onChange={this._initTemplate}
placeholder={_('newVmSelectTemplate')}
predicate={this._getIsInPool()}
predicate={this._getVmPredicate()}
value={template}
/>
: <SelectResourceSetsVmTemplate
onChange={this._initTemplate}
placeholder={_('newVmSelectTemplate')}
resourceSet={this.state.resourceSet}
value={template}
/>}
</span>
</Item>
<Item label='newVmNameLabel'>
@ -558,6 +713,8 @@ export default class NewVm extends BaseComponent {
return CPUs && memory !== undefined
}
// INSTALL SETTINGS ------------------------------------------------------------
_renderInstallSettings = () => {
const { template } = this.state.state
if (!template) {
@ -570,7 +727,6 @@ export default class NewVm extends BaseComponent {
installIso,
installMethod,
installNetwork,
pool,
pv_args,
sshKey
} = this.state.state
@ -644,12 +800,19 @@ export default class NewVm extends BaseComponent {
<span>{_('newVmIsoDvdLabel')}</span>
&nbsp;
<span className={styles.inlineSelect}>
<SelectVdi
{this.state.pool ? <SelectVdi
disabled={installMethod !== 'ISO'}
onChange={this._getOnChange('installIso')}
srPredicate={sr => sr.$pool === pool.id && sr.SR_type === 'iso'}
srPredicate={this._getIsoPredicate()}
value={installIso}
/>
: <SelectResourceSetsVdi
disabled={installMethod !== 'ISO'}
onChange={this._getOnChange('installIso')}
resourceSet={this.state.resourceSet}
srPredicate={this._getIsoPredicate()}
value={installIso}
/>}
</span>
</span>
</Item>
@ -729,11 +892,14 @@ export default class NewVm extends BaseComponent {
}
}
// INTERFACES ------------------------------------------------------------------
_renderInterfaces = () => {
const { formatMessage } = this.props.intl
const {
VIFs
} = this.state.state
state: { VIFs },
pool
} = this.state
return <Section icon='new-vm-interfaces' title='newVmInterfacesPanel' done={this._isInterfacesDone()}>
<SectionContent column>
{map(VIFs, (vif, index) => <div key={index}>
@ -750,11 +916,16 @@ export default class NewVm extends BaseComponent {
</Item>
<Item label='newVmNetworkLabel'>
<span className={styles.inlineSelect}>
<SelectNetwork
{pool ? <SelectNetwork
onChange={this._getOnChange('VIFs', index, 'network', 'id')}
predicate={this._getIsInPool()}
predicate={this._getNetworkPredicate()}
value={vif.network}
/>
: <SelectResourceSetsNetwork
onChange={this._getOnChange('VIFs', index, 'network', 'id')}
resourceSet={this.state.resourceSet}
value={vif.network}
/>}
</span>
</Item>
<Item>
@ -780,25 +951,35 @@ export default class NewVm extends BaseComponent {
vif.network
)
// DISKS -----------------------------------------------------------------------
_renderDisks = () => {
const {
configDrive,
state: { configDrive,
existingDisks,
VDIs
} = this.state.state
VDIs },
pool
} = this.state
let i = 0
return <Section icon='new-vm-disks' title='newVmDisksPanel' done={this._isDisksDone()}>
<SectionContent column>
{/* Existing disks */}
{map(toArray(existingDisks), (disk, index) => <div key={index}>
{map(existingDisks, (disk, index) => <div key={i}>
<LineItem>
<Item label='newVmSrLabel'>
<span className={styles.inlineSelect}>
<SelectSr
{pool ? <SelectSr
onChange={this._getOnChange('existingDisks', index, '$SR', 'id')}
predicate={this._getSrPredicate()}
value={disk.$SR}
/>
: <SelectResourceSetsSr
onChange={this._getOnChange('existingDisks', index, '$SR', 'id')}
predicate={this._getSrPredicate()}
resourceSet={this.state.resourceSet}
value={disk.$SR}
/>}
</span>
</Item>
{' '}
@ -827,7 +1008,7 @@ export default class NewVm extends BaseComponent {
/>
</Item>
</LineItem>
{index < size(existingDisks) + VDIs.length - 1 && <hr />}
{i++ < size(existingDisks) + VDIs.length - 1 && <hr />}
</div>)}
{/* VDIs */}
@ -835,11 +1016,17 @@ export default class NewVm extends BaseComponent {
<LineItem>
<Item label='newVmSrLabel'>
<span className={styles.inlineSelect}>
<SelectSr
{pool ? <SelectSr
onChange={this._getOnChange('VDIs', index, 'SR', 'id')}
predicate={this._getSrPredicate()}
value={vdi.SR}
/>
: <SelectResourceSetsSr
onChange={this._getOnChange('VDIs', index, 'SR', 'id')}
predicate={this._getSrPredicate()}
resourceSet={this.state.resourceSet}
value={vdi.SR}
/>}
</span>
</Item>
{' '}
@ -902,6 +1089,8 @@ export default class NewVm extends BaseComponent {
vdi.$SR && vdi.name_label && vdi.size !== undefined
)
// SUMMARY ---------------------------------------------------------------------
_renderSummary = () => {
const {
bootAfterCreate,

View File

@ -32,7 +32,8 @@ import {
} from 'select-objects'
import {
connectStore,
formatSize
formatSize,
resolveResourceSets
} from 'utils'
import {
createResourceSet,
@ -42,8 +43,7 @@ import {
} from 'xo'
import {
Subjects,
resolveResourceSets
Subjects
} from '../helpers'
// ===================================================================

View File

@ -8,9 +8,13 @@ import _ from 'intl'
import map from 'lodash/map'
import renderXoItem from 'render-xo-item'
import { Container, Row, Col } from 'grid'
import { formatSize } from 'utils'
import { subscribeResourceSets } from 'xo'
import {
formatSize,
resolveResourceSets
} from 'utils'
import {
Card,
CardBlock,
@ -18,8 +22,7 @@ import {
} from 'card'
import {
Subjects,
resolveResourceSets
Subjects
} from '../helpers'
// ===================================================================

View File

@ -1,12 +1,9 @@
import _ from 'intl'
import forEach from 'lodash/forEach'
import keyBy from 'lodash/keyBy'
import map from 'lodash/map'
import propTypes from 'prop-types'
import React, { Component } from 'react'
import renderXoItem from 'render-xo-item'
import store from 'store'
import { getObject } from 'selectors'
import {
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({
subjects: propTypes.array.isRequired
})