diff --git a/CHANGELOG.md b/CHANGELOG.md index ffb0064be..70251f030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Export VDI content [#2432](https://github.com/vatesfr/xen-orchestra/issues/2432) (PR [#3194](https://github.com/vatesfr/xen-orchestra/pull/3194)) - Search syntax support wildcard (`*`) and regular expressions [#3190](https://github.com/vatesfr/xen-orchestra/issues/3190) (PRs [#3198](https://github.com/vatesfr/xen-orchestra/pull/3198) & [#3199](https://github.com/vatesfr/xen-orchestra/pull/3199)) +- Import VDI content [#2432](https://github.com/vatesfr/xen-orchestra/issues/2432) (PR [#3216](https://github.com/vatesfr/xen-orchestra/pull/3216)) ### Bug fixes diff --git a/packages/xo-web/src/common/dropzone/index.js b/packages/xo-web/src/common/dropzone/index.js index 0e18e7529..b2f8697dd 100644 --- a/packages/xo-web/src/common/dropzone/index.js +++ b/packages/xo-web/src/common/dropzone/index.js @@ -8,16 +8,18 @@ import styles from './index.css' @propTypes({ onDrop: propTypes.func, message: propTypes.node, + multiple: propTypes.bool, }) export default class Dropzone extends Component { render () { - const { onDrop, message } = this.props + const { onDrop, message, multiple } = this.props return (
{message}
diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js index 4ae3932ac..30a99f921 100644 --- a/packages/xo-web/src/common/intl/messages.js +++ b/packages/xo-web/src/common/intl/messages.js @@ -918,6 +918,9 @@ const messages = { deleteSelectedVdis: 'Delete selected VDIs', deleteSelectedVdi: 'Delete selected VDI', exportVdi: 'Export VDI content', + importVdi: 'Import VDI content', + importVdiNoFile: 'No file selected', + selectVdiMessage: 'Drop VHD file here', useQuotaWarning: 'Creating this disk will use the disk space quota from the resource set {resourceSet} ({spaceLeft} left)', notEnoughSpaceInResourceSet: @@ -1242,8 +1245,11 @@ const messages = { importVmsCleanList: 'Reset', vmImportSuccess: 'VM import success', vmImportFailed: 'VM import failed', + vdiImportSuccess: 'VDI import success', + vdiImportFailed: 'VDI import failed', setVmFailed: 'Error on setting the VM: {vm}', startVmImport: 'Import starting…', + startVdiImport: 'VDI import starting…', startVmExport: 'Export starting…', startVdiExport: 'VDI export starting…', nCpus: 'N CPUs', diff --git a/packages/xo-web/src/common/xo/import-vdi-modal/index.js b/packages/xo-web/src/common/xo/import-vdi-modal/index.js new file mode 100644 index 000000000..32cc0f5b0 --- /dev/null +++ b/packages/xo-web/src/common/xo/import-vdi-modal/index.js @@ -0,0 +1,21 @@ +import _ from 'intl' +import Component from 'base-component' +import Dropzone from 'dropzone' +import React from 'react' + +export default class ImportVdiModalBody extends Component { + get value () { + return this.state.file + } + + render () { + const { file } = this.state + return ( + + ) + } +} diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js index 86eba2d14..79606a324 100644 --- a/packages/xo-web/src/common/xo/index.js +++ b/packages/xo-web/src/common/xo/index.js @@ -1256,6 +1256,38 @@ export const importVm = (file, type = 'xva', data = undefined, sr) => { ) } +import ImportVdiModalBody from './import-vdi-modal' // eslint-disable-line import/first +export const importVdi = async vdi => { + const file = await confirm({ + body: , + icon: 'import', + title: _('importVdi'), + }) + + if (file === undefined) { + error(_('importVdi'), _('importVdiNoFile')) + return + } + + const { name } = file + info(_('startVdiImport'), name) + + return _call('disk.importContent', { id: resolveId(vdi) }).then( + ({ $sendTo }) => + post($sendTo, file) + .then(res => { + if (res.status !== 200) { + throw res.status + } + success(_('vdiImportSuccess'), name) + return res.json().then(body => body.result) + }) + .catch(err => { + error(_('vdiImportFailed'), err) + }) + ) +} + export const importVms = (vms, sr) => Promise.all( map(vms, ({ file, type, data }) => diff --git a/packages/xo-web/src/xo-app/sr/tab-disks.js b/packages/xo-web/src/xo-app/sr/tab-disks.js index 029e64ac5..3634920db 100644 --- a/packages/xo-web/src/xo-app/sr/tab-disks.js +++ b/packages/xo-web/src/xo-app/sr/tab-disks.js @@ -16,7 +16,7 @@ import { Text } from 'editable' import { SizeInput, Toggle } from 'form' import { Container, Row, Col } from 'grid' import { connectStore, formatSize, noop } from 'utils' -import { concat, isEmpty, map, some } from 'lodash' +import { concat, groupBy, isEmpty, map, mapValues, pick, some } from 'lodash' import { createGetObjectsOfType, createSelector, @@ -31,6 +31,7 @@ import { disconnectVbd, editVdi, exportVdi, + importVdi, isVmRunning, } from 'xo' @@ -191,6 +192,12 @@ const INDIVIDUAL_ACTIONS = [ icon: 'export', label: _('exportVdi'), }, + { + disabled: ({ id }, { isVdiAttached }) => isVdiAttached[id], + handler: importVdi, + icon: 'import', + label: _('importVdi'), + }, { handler: vdi => copy(vdi.uuid), icon: 'clipboard', @@ -276,6 +283,7 @@ class NewDisk extends Component { @connectStore(() => ({ checkPermissions: getCheckPermissions, + vbds: createGetObjectsOfType('VBD'), })) export default class SrDisks extends Component { _closeNewDiskForm = () => this.setState({ newDisk: false }) @@ -293,6 +301,15 @@ export default class SrDisks extends Component { (check, id) => check(id, 'administrate') ) + _getIsVdiAttached = createSelector( + createSelector( + () => this.props.vbds, + () => map(this.props.vdis, 'id'), + (vbds, vdis) => pick(groupBy(vbds, 'VDI'), vdis) + ), + vbdsByVdi => mapValues(vbdsByVdi, vbds => some(vbds, 'attached')) + ) + render () { const vdis = this._getAllVdis() const { newDisk } = this.state @@ -325,6 +342,7 @@ export default class SrDisks extends Component { ({ checkPermissions: getCheckPermissions, isAdmin, + allVbds: createGetObjectsOfType('VBD'), })) export default class TabDisks extends Component { constructor (props) { @@ -652,12 +665,27 @@ export default class TabDisks extends Component { (vdis, vbds, vm) => mapValues(vdis, vdi => find(vbds, { VDI: vdi.id })) ) + _getIsVdiAttached = createSelector( + createSelector( + () => this.props.allVbds, + () => Object.keys(this.props.vdis), + (vbds, vdis) => pick(groupBy(vbds, 'VDI'), vdis) + ), + vbdsByVdi => mapValues(vbdsByVdi, vbds => some(vbds, 'attached')) + ) + individualActions = [ { handler: exportVdi, icon: 'export', label: _('exportVdi'), }, + { + disabled: ({ id }, { isVdiAttached }) => isVdiAttached[id], + handler: importVdi, + icon: 'import', + label: _('importVdi'), + }, { handler: this._migrateVdi, icon: 'vdi-migrate', @@ -735,6 +763,7 @@ export default class TabDisks extends Component { actions={ACTIONS} collection={vdis} columns={vm.virtualizationMode === 'pv' ? COLUMNS_VM_PV : COLUMNS} + data-isVdiAttached={this._getIsVdiAttached()} data-srs={srs} data-vbdsByVdi={this._getVbdsByVdi()} data-vm={vm}