feat(migrate-vm-modal): controlled form (#2259)
This commit is contained in:
parent
495c97b44b
commit
4d69866532
@ -1,19 +1,14 @@
|
||||
import Collapse from 'collapse'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import { every, forEach, map } from 'lodash'
|
||||
import { map } from 'lodash'
|
||||
|
||||
import _ from '../../intl'
|
||||
import propTypes from '../../prop-types-decorator'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
import { createSelector } from '../../selectors'
|
||||
import { SelectSr } from '../../select-objects'
|
||||
import { isSrWritable } from 'xo'
|
||||
import { Container, Col } from 'grid'
|
||||
|
||||
// Can 2 SRs on the same pool have 2 VDIs used by the same VM
|
||||
const areSrsCompatible = (sr1, sr2) =>
|
||||
sr1.shared || sr2.shared || sr1.$container === sr2.$container
|
||||
import { isSrWritable } from 'xo'
|
||||
import { SelectSr } from '../../select-objects'
|
||||
|
||||
const Collapsible = ({ collapsible, children, ...props }) =>
|
||||
collapsible ? (
|
||||
@ -32,96 +27,49 @@ Collapsible.propTypes = {
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
vdis: propTypes.array.isRequired,
|
||||
predicate: propTypes.func,
|
||||
mainSrPredicate: propTypes.func,
|
||||
onChange: propTypes.func.isRequired,
|
||||
srPredicate: propTypes.func,
|
||||
value: propTypes.objectOf(
|
||||
propTypes.shape({
|
||||
mainSr: propTypes.object,
|
||||
mapVdisSrs: propTypes.object,
|
||||
})
|
||||
).isRequired,
|
||||
vdis: propTypes.object.isRequired,
|
||||
})
|
||||
export default class ChooseSrForEachVdisModal extends Component {
|
||||
state = {
|
||||
mapVdisSrs: {},
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
if (
|
||||
this.props.predicate !== undefined &&
|
||||
newProps.predicate !== this.props.predicate
|
||||
) {
|
||||
this.state = {
|
||||
mainSr: undefined,
|
||||
mapVdisSrs: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onChange = props => {
|
||||
this.setState(props)
|
||||
this.props.onChange(props)
|
||||
}
|
||||
|
||||
_onChangeMainSr = newSr => {
|
||||
const oldSr = this.state.mainSr
|
||||
|
||||
if (oldSr == null || newSr == null || oldSr.$pool !== newSr.$pool) {
|
||||
this.setState({
|
||||
mapVdisSrs: {},
|
||||
})
|
||||
} else if (!newSr.shared) {
|
||||
const mapVdisSrs = { ...this.state.mapVdisSrs }
|
||||
forEach(mapVdisSrs, (sr, vdi) => {
|
||||
if (
|
||||
sr != null &&
|
||||
newSr !== sr &&
|
||||
sr.$container !== newSr.$container &&
|
||||
!sr.shared
|
||||
) {
|
||||
delete mapVdisSrs[vdi]
|
||||
}
|
||||
})
|
||||
this._onChange({ mapVdisSrs })
|
||||
}
|
||||
|
||||
this._onChange({
|
||||
mainSr: newSr,
|
||||
_onChange = newValues => {
|
||||
this.props.onChange({
|
||||
...this.props.value,
|
||||
...newValues,
|
||||
})
|
||||
}
|
||||
|
||||
_getSrPredicate = createSelector(
|
||||
() => this.state.mainSr,
|
||||
() => this.state.mapVdisSrs,
|
||||
(mainSr, mapVdisSrs) => sr =>
|
||||
isSrWritable(sr) &&
|
||||
mainSr.$pool === sr.$pool &&
|
||||
areSrsCompatible(mainSr, sr) &&
|
||||
every(
|
||||
mapVdisSrs,
|
||||
selectedSr => selectedSr == null || areSrsCompatible(selectedSr, sr)
|
||||
)
|
||||
)
|
||||
_onChangeMainSr = mainSr => this._onChange({ mainSr })
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const { vdis } = props
|
||||
const { mainSr, mapVdisSrs } = state
|
||||
|
||||
const srPredicate = props.predicate || this._getSrPredicate()
|
||||
const { props } = this
|
||||
const {
|
||||
mainSrPredicate = isSrWritable,
|
||||
srPredicate = mainSrPredicate,
|
||||
value: { mainSr, mapVdisSrs },
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SelectSr
|
||||
onChange={mainSr =>
|
||||
props.predicate !== undefined
|
||||
? this._onChange({ mainSr })
|
||||
: this._onChangeMainSr(mainSr)
|
||||
}
|
||||
predicate={props.predicate || isSrWritable}
|
||||
onChange={this._onChangeMainSr}
|
||||
placeholder={_('chooseSrForEachVdisModalMainSr')}
|
||||
predicate={mainSrPredicate}
|
||||
value={mainSr}
|
||||
/>
|
||||
<br />
|
||||
{vdis != null &&
|
||||
{props.vdis != null &&
|
||||
mainSr != null && (
|
||||
<Collapsible
|
||||
collapsible={vdis.length >= 3}
|
||||
buttonText={_('chooseSrForEachVdisModalSelectSr')}
|
||||
collapsible={props.vdis.length >= 3}
|
||||
>
|
||||
<br />
|
||||
<Container>
|
||||
@ -133,7 +81,7 @@ export default class ChooseSrForEachVdisModal extends Component {
|
||||
<strong>{_('chooseSrForEachVdisModalSrLabel')}</strong>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
{map(vdis, vdi => (
|
||||
{map(props.vdis, vdi => (
|
||||
<SingleLineRow key={vdi.uuid}>
|
||||
<Col size={6}>{vdi.name_label || vdi.name}</Col>
|
||||
<Col size={6}>
|
||||
@ -143,8 +91,8 @@ export default class ChooseSrForEachVdisModal extends Component {
|
||||
mapVdisSrs: { ...mapVdisSrs, [vdi.uuid]: sr },
|
||||
})
|
||||
}
|
||||
value={mapVdisSrs[vdi.uuid]}
|
||||
predicate={srPredicate}
|
||||
value={mapVdisSrs !== undefined && mapVdisSrs[vdi.uuid]}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import every from 'lodash/every'
|
||||
import forEach from 'lodash/forEach'
|
||||
import find from 'lodash/find'
|
||||
import forEach from 'lodash/forEach'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import store from 'store'
|
||||
@ -11,18 +11,17 @@ import ChooseSrForEachVdisModal from '../choose-sr-for-each-vdis-modal'
|
||||
import invoke from '../../invoke'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
import { Col } from '../../grid'
|
||||
import { connectStore, mapPlus, resolveId, resolveIds } from '../../utils'
|
||||
import { getDefaultNetworkForVif } from '../utils'
|
||||
import { SelectHost, SelectNetwork } from '../../select-objects'
|
||||
import { connectStore, mapPlus, resolveIds } from '../../utils'
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
createPicker,
|
||||
createSelector,
|
||||
getObject,
|
||||
} from '../../selectors'
|
||||
import { isSrShared } from 'xo'
|
||||
|
||||
import { isSrWritable } from '../'
|
||||
import { isSrShared, isSrWritable } from '../'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@ -68,8 +67,8 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
mapVdisSrs: {},
|
||||
mapVifsNetworks: {},
|
||||
targetSrs: {},
|
||||
}
|
||||
|
||||
this._getHostPredicate = createSelector(
|
||||
@ -126,11 +125,11 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
|
||||
get value () {
|
||||
return {
|
||||
targetHost: this.state.host && this.state.host.id,
|
||||
sr: this.state.mainSr && this.state.mainSr.id,
|
||||
mapVdisSrs: resolveIds(this.state.mapVdisSrs),
|
||||
mapVdisSrs: resolveIds(this.state.targetSrs.mapVdisSrs),
|
||||
mapVifsNetworks: this.state.mapVifsNetworks,
|
||||
migrationNetwork: this.state.migrationNetworkId,
|
||||
sr: resolveId(this.state.targetSrs.mainSr),
|
||||
targetHost: this.state.host && this.state.host.id,
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,6 +173,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
intraPool,
|
||||
mapVifsNetworks: undefined,
|
||||
migrationNetwork: undefined,
|
||||
targetSrs: {},
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -205,6 +205,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
intraPool,
|
||||
mapVifsNetworks: defaultNetworksForVif,
|
||||
migrationNetworkId: defaultMigrationNetworkId,
|
||||
targetSrs: {},
|
||||
})
|
||||
}
|
||||
|
||||
@ -219,6 +220,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
intraPool,
|
||||
mapVifsNetworks,
|
||||
migrationNetworkId,
|
||||
targetSrs,
|
||||
} = this.state
|
||||
return (
|
||||
<div>
|
||||
@ -240,8 +242,9 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
<SingleLineRow>
|
||||
<Col size={12}>
|
||||
<ChooseSrForEachVdisModal
|
||||
onChange={props => this.setState(props)}
|
||||
predicate={this._getSrPredicate()}
|
||||
mainSrPredicate={this._getSrPredicate()}
|
||||
onChange={this.linkState('targetSrs')}
|
||||
value={targetSrs}
|
||||
vdis={vdis}
|
||||
/>
|
||||
</Col>
|
||||
|
@ -5,7 +5,6 @@ import every from 'lodash/every'
|
||||
import filter from 'lodash/filter'
|
||||
import find from 'lodash/find'
|
||||
import forEach from 'lodash/forEach'
|
||||
import getEventValue from 'get-event-value'
|
||||
import groupBy from 'lodash/groupBy'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
@ -125,8 +124,8 @@ const openImportModal = ({ backups }) =>
|
||||
body: <ImportModalBody vmName={backups[0].name} backups={backups} />,
|
||||
}).then(doImport)
|
||||
|
||||
const doImport = ({ backup, mainSr, start, mapVdisSrs }) => {
|
||||
if (!mainSr || !backup) {
|
||||
const doImport = ({ backup, targetSrs, start }) => {
|
||||
if (targetSrs.mainSr === undefined || backup === undefined) {
|
||||
error(_('backupRestoreErrorTitle'), _('backupRestoreErrorMessage'))
|
||||
return
|
||||
}
|
||||
@ -137,10 +136,10 @@ const doImport = ({ backup, mainSr, start, mapVdisSrs }) => {
|
||||
info(_('importBackupTitle'), _('importBackupMessage'))
|
||||
try {
|
||||
const importPromise = importMethods[backup.type]({
|
||||
remote: backup.remoteId,
|
||||
sr: mainSr,
|
||||
file: backup.path,
|
||||
mapVdisSrs,
|
||||
mapVdisSrs: targetSrs.mapVdisSrs,
|
||||
remote: backup.remoteId,
|
||||
sr: targetSrs.mainSr,
|
||||
}).then(id => {
|
||||
return id
|
||||
})
|
||||
@ -153,12 +152,8 @@ const doImport = ({ backup, mainSr, start, mapVdisSrs }) => {
|
||||
}
|
||||
|
||||
class _ModalBody extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
mapVdisSrs: {},
|
||||
}
|
||||
state = {
|
||||
targetSrs: {},
|
||||
}
|
||||
|
||||
get value () {
|
||||
@ -166,52 +161,52 @@ class _ModalBody extends Component {
|
||||
}
|
||||
|
||||
_getSrPredicate = createSelector(
|
||||
() => this.state.sr,
|
||||
() => this.state.mapVdisSrs,
|
||||
(defaultSr, mapVdisSrs) => sr =>
|
||||
sr !== defaultSr &&
|
||||
() => this.state.targetSrs.mainSr,
|
||||
() => this.state.targetSrs.mapVdisSrs,
|
||||
(mainSr, mapVdisSrs) => sr =>
|
||||
isSrWritable(sr) &&
|
||||
defaultSr.$pool === sr.$pool &&
|
||||
areSrsCompatible(defaultSr, sr) &&
|
||||
mainSr.$pool === sr.$pool &&
|
||||
areSrsCompatible(mainSr, sr) &&
|
||||
every(
|
||||
mapVdisSrs,
|
||||
selectedSr => selectedSr == null || areSrsCompatible(selectedSr, sr)
|
||||
)
|
||||
)
|
||||
|
||||
_onChangeDefaultSr = event => {
|
||||
const oldSr = this.state.sr
|
||||
const newSr = getEventValue(event)
|
||||
_onSrsChange = props => {
|
||||
const oldMainSr = this.state.targetSrs.mainSr
|
||||
const newMainSr = props.mainSr
|
||||
|
||||
if (oldSr == null || newSr == null || oldSr.$pool !== newSr.$pool) {
|
||||
this.setState({
|
||||
mapVdisSrs: {},
|
||||
})
|
||||
} else if (!newSr.shared) {
|
||||
const mapVdisSrs = { ...this.state.mapVdisSrs }
|
||||
forEach(mapVdisSrs, (sr, vdi) => {
|
||||
const targetSrs = { ...props }
|
||||
|
||||
// This code fixes the incompatibilities between the mapVdisSrs values
|
||||
if (oldMainSr !== newMainSr) {
|
||||
if (
|
||||
oldMainSr == null ||
|
||||
newMainSr == null ||
|
||||
oldMainSr.$pool !== newMainSr.$pool
|
||||
) {
|
||||
targetSrs.mapVdisSrs = {}
|
||||
} else if (!newMainSr.shared) {
|
||||
forEach(targetSrs.mapVdisSrs, (sr, vdi) => {
|
||||
if (
|
||||
sr != null &&
|
||||
newSr !== sr &&
|
||||
sr.$container !== newSr.$container &&
|
||||
newMainSr !== sr &&
|
||||
sr.$container !== newMainSr.$container &&
|
||||
!sr.shared
|
||||
) {
|
||||
delete mapVdisSrs[vdi]
|
||||
delete targetSrs.mapVdisSrs[vdi]
|
||||
}
|
||||
})
|
||||
this.setState({
|
||||
mapVdisSrs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
sr: newSr,
|
||||
})
|
||||
this.setState({ targetSrs })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { backups, intl } = this.props
|
||||
const vdis = this.state.backup && this.state.backup.vdis
|
||||
const { props, state } = this
|
||||
const vdis = state.backup && state.backup.vdis
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -219,15 +214,17 @@ class _ModalBody extends Component {
|
||||
onChange={this.linkState('backup')}
|
||||
optionKey='path'
|
||||
optionRenderer={backupOptionRenderer}
|
||||
options={backups}
|
||||
placeholder={intl.formatMessage(
|
||||
options={props.backups}
|
||||
placeholder={props.intl.formatMessage(
|
||||
messages.importBackupModalSelectBackup
|
||||
)}
|
||||
/>
|
||||
<br />
|
||||
<ChooseSrForEachVdisModal
|
||||
onChange={this._onSrsChange}
|
||||
srPredicate={this._getSrPredicate()}
|
||||
value={state.targetSrs}
|
||||
vdis={vdis}
|
||||
onChange={props => this.setState(props)}
|
||||
/>
|
||||
<br />
|
||||
<Toggle onChange={this.linkState('start')} />{' '}
|
||||
|
Loading…
Reference in New Issue
Block a user