diff --git a/src/common/intl/locales/fr.js b/src/common/intl/locales/fr.js index 0ebb5b921d1..fcb81b51426 100644 --- a/src/common/intl/locales/fr.js +++ b/src/common/intl/locales/fr.js @@ -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', diff --git a/src/common/intl/messages.js b/src/common/intl/messages.js index 6fd8f5a7f97..91a5d4160f0 100644 --- a/src/common/intl/messages.js +++ b/src/common/intl/messages.js @@ -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', diff --git a/src/common/render-xo-item.js b/src/common/render-xo-item.js index 810a077fd49..6d946b8a56d 100644 --- a/src/common/render-xo-item.js +++ b/src/common/render-xo-item.js @@ -108,6 +108,11 @@ const xoItemToRender = { {user.email} ), + resourceSet: resourceSet => ( + + {resourceSet.name} + + ), // XO objects. pool: pool => ( diff --git a/src/common/select-objects.js b/src/common/select-objects.js index 380aae86e29..39a959e1454 100644 --- a/src/common/select-objects.js +++ b/src/common/select-objects.js @@ -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 ( + + ) + } +} + +// =================================================================== + +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 ( + + ) + } +} + +// =================================================================== + +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 ( + + ) + } +} + +// =================================================================== + +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 ( + + ) + } +} diff --git a/src/common/selectors.js b/src/common/selectors.js index d9f86964f89..a9cc786ea99 100644 --- a/src/common/selectors.js +++ b/src/common/selectors.js @@ -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 diff --git a/src/common/utils.js b/src/common/utils.js index 47d58d96caf..cb1b96b65b2 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -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 diff --git a/src/xo-app/menu/index.js b/src/xo-app/menu/index.js index 679a81f5178..c9f0a2e7abd 100644 --- a/src/xo-app/menu/index.js +++ b/src/xo-app/menu/index.js @@ -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' } ]} ] diff --git a/src/xo-app/new-vm/index.js b/src/xo-app/new-vm/index.js index 67df3946d27..1453bc2ea49 100644 --- a/src/xo-app/new-vm/index.js +++ b/src/xo-app/new-vm/index.js @@ -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 }) => (
( {children}
) - const LineItem = ({ children }) => (
{children}
) - const Item = ({ label, children }) => ( {label && {_(label)} } @@ -77,10 +92,17 @@ const Item = ({ label, children }) => ( ) +// ----------------------------------------------------------------------------- + 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 = + + + const selectResourceSet = + + + return + + +

+ {showSelectPool && showSelectResourceSet + ? _('newVmCreateNewVmOn2', { + select1: selectPool, + select2: selectResourceSet + }) + : showSelectPool || showSelectResourceSet + ? _('newVmCreateNewVmOn', { + select: isEmpty(this._getOperatablePools()) ? selectResourceSet : selectPool + }) + : _('newVmCreateNewVmNoPermission') + } +

+ +
+
+ } + render () { - return
-

- {_('newVmCreateNewVmOn', { - pool: - - - })} -

- {this.state.state.pool &&
+ const { resourceSet, pool } = this.state + return + {(pool || resourceSet) && {this._renderInfo()} {this._renderPerformances()} @@ -440,9 +587,11 @@ export default class NewVm extends BaseComponent {
} - + } +// INFO ------------------------------------------------------------------------ + _renderInfo = () => { const { multipleVms, @@ -456,12 +605,18 @@ export default class NewVm extends BaseComponent { - + : } @@ -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 { {_('newVmIsoDvdLabel')}   - sr.$pool === pool.id && sr.SR_type === 'iso'} + srPredicate={this._getIsoPredicate()} value={installIso} /> + : } @@ -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
{map(VIFs, (vif, index) =>
@@ -750,11 +916,16 @@ export default class NewVm extends BaseComponent { - + : } @@ -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
{/* Existing disks */} - {map(toArray(existingDisks), (disk, index) =>
+ {map(existingDisks, (disk, index) =>
- + : } {' '} @@ -827,7 +1008,7 @@ export default class NewVm extends BaseComponent { /> - {index < size(existingDisks) + VDIs.length - 1 &&
} + {i++ < size(existingDisks) + VDIs.length - 1 &&
}
)} {/* VDIs */} @@ -835,11 +1016,17 @@ export default class NewVm extends BaseComponent { - + : } {' '} @@ -902,6 +1089,8 @@ export default class NewVm extends BaseComponent { vdi.$SR && vdi.name_label && vdi.size !== undefined ) +// SUMMARY --------------------------------------------------------------------- + _renderSummary = () => { const { bootAfterCreate, diff --git a/src/xo-app/self/admin/index.js b/src/xo-app/self/admin/index.js index 784e8552998..a81a9b1b9b3 100644 --- a/src/xo-app/self/admin/index.js +++ b/src/xo-app/self/admin/index.js @@ -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' // =================================================================== diff --git a/src/xo-app/self/dashboard/index.js b/src/xo-app/self/dashboard/index.js index 86fd87074b2..9fb7197382c 100644 --- a/src/xo-app/self/dashboard/index.js +++ b/src/xo-app/self/dashboard/index.js @@ -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' // =================================================================== diff --git a/src/xo-app/self/helpers.js b/src/xo-app/self/helpers.js index 9372bb02550..d26dc3df7a6 100644 --- a/src/xo-app/self/helpers.js +++ b/src/xo-app/self/helpers.js @@ -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 })