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 &&
}
-
+
}
+// 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
})