feat(xo-web/VM,SR): import VDI content (#3216)

See #2432
This commit is contained in:
Pierre Donias 2018-07-23 09:24:44 +02:00 committed by GitHub
parent d99555a4a8
commit 53477be12d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 114 additions and 5 deletions

View File

@ -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)) - 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)) - 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 ### Bug fixes

View File

@ -8,16 +8,18 @@ import styles from './index.css'
@propTypes({ @propTypes({
onDrop: propTypes.func, onDrop: propTypes.func,
message: propTypes.node, message: propTypes.node,
multiple: propTypes.bool,
}) })
export default class Dropzone extends Component { export default class Dropzone extends Component {
render () { render () {
const { onDrop, message } = this.props const { onDrop, message, multiple } = this.props
return ( return (
<ReactDropzone <ReactDropzone
onDrop={onDrop}
className={styles.dropzone}
activeClassName={styles.activeDropzone} activeClassName={styles.activeDropzone}
className={styles.dropzone}
multiple={multiple}
onDrop={onDrop}
> >
<div className={styles.dropzoneText}>{message}</div> <div className={styles.dropzoneText}>{message}</div>
</ReactDropzone> </ReactDropzone>

View File

@ -918,6 +918,9 @@ const messages = {
deleteSelectedVdis: 'Delete selected VDIs', deleteSelectedVdis: 'Delete selected VDIs',
deleteSelectedVdi: 'Delete selected VDI', deleteSelectedVdi: 'Delete selected VDI',
exportVdi: 'Export VDI content', exportVdi: 'Export VDI content',
importVdi: 'Import VDI content',
importVdiNoFile: 'No file selected',
selectVdiMessage: 'Drop VHD file here',
useQuotaWarning: useQuotaWarning:
'Creating this disk will use the disk space quota from the resource set {resourceSet} ({spaceLeft} left)', 'Creating this disk will use the disk space quota from the resource set {resourceSet} ({spaceLeft} left)',
notEnoughSpaceInResourceSet: notEnoughSpaceInResourceSet:
@ -1242,8 +1245,11 @@ const messages = {
importVmsCleanList: 'Reset', importVmsCleanList: 'Reset',
vmImportSuccess: 'VM import success', vmImportSuccess: 'VM import success',
vmImportFailed: 'VM import failed', vmImportFailed: 'VM import failed',
vdiImportSuccess: 'VDI import success',
vdiImportFailed: 'VDI import failed',
setVmFailed: 'Error on setting the VM: {vm}', setVmFailed: 'Error on setting the VM: {vm}',
startVmImport: 'Import starting…', startVmImport: 'Import starting…',
startVdiImport: 'VDI import starting…',
startVmExport: 'Export starting…', startVmExport: 'Export starting…',
startVdiExport: 'VDI export starting…', startVdiExport: 'VDI export starting…',
nCpus: 'N CPUs', nCpus: 'N CPUs',

View File

@ -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 (
<Dropzone
onDrop={this.linkState('file', '0')}
message={file === undefined ? _('selectVdiMessage') : file.name}
multiple={false}
/>
)
}
}

View File

@ -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: <ImportVdiModalBody />,
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) => export const importVms = (vms, sr) =>
Promise.all( Promise.all(
map(vms, ({ file, type, data }) => map(vms, ({ file, type, data }) =>

View File

@ -16,7 +16,7 @@ import { Text } from 'editable'
import { SizeInput, Toggle } from 'form' import { SizeInput, Toggle } from 'form'
import { Container, Row, Col } from 'grid' import { Container, Row, Col } from 'grid'
import { connectStore, formatSize, noop } from 'utils' import { connectStore, formatSize, noop } from 'utils'
import { concat, isEmpty, map, some } from 'lodash' import { concat, groupBy, isEmpty, map, mapValues, pick, some } from 'lodash'
import { import {
createGetObjectsOfType, createGetObjectsOfType,
createSelector, createSelector,
@ -31,6 +31,7 @@ import {
disconnectVbd, disconnectVbd,
editVdi, editVdi,
exportVdi, exportVdi,
importVdi,
isVmRunning, isVmRunning,
} from 'xo' } from 'xo'
@ -191,6 +192,12 @@ const INDIVIDUAL_ACTIONS = [
icon: 'export', icon: 'export',
label: _('exportVdi'), label: _('exportVdi'),
}, },
{
disabled: ({ id }, { isVdiAttached }) => isVdiAttached[id],
handler: importVdi,
icon: 'import',
label: _('importVdi'),
},
{ {
handler: vdi => copy(vdi.uuid), handler: vdi => copy(vdi.uuid),
icon: 'clipboard', icon: 'clipboard',
@ -276,6 +283,7 @@ class NewDisk extends Component {
@connectStore(() => ({ @connectStore(() => ({
checkPermissions: getCheckPermissions, checkPermissions: getCheckPermissions,
vbds: createGetObjectsOfType('VBD'),
})) }))
export default class SrDisks extends Component { export default class SrDisks extends Component {
_closeNewDiskForm = () => this.setState({ newDisk: false }) _closeNewDiskForm = () => this.setState({ newDisk: false })
@ -293,6 +301,15 @@ export default class SrDisks extends Component {
(check, id) => check(id, 'administrate') (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 () { render () {
const vdis = this._getAllVdis() const vdis = this._getAllVdis()
const { newDisk } = this.state const { newDisk } = this.state
@ -325,6 +342,7 @@ export default class SrDisks extends Component {
<SortedTable <SortedTable
collection={vdis} collection={vdis}
columns={COLUMNS} columns={COLUMNS}
data-isVdiAttached={this._getIsVdiAttached()}
defaultFilter='filterOnlyManaged' defaultFilter='filterOnlyManaged'
filters={FILTERS} filters={FILTERS}
groupedActions={GROUPED_ACTIONS} groupedActions={GROUPED_ACTIONS}

View File

@ -14,6 +14,7 @@ import SortedTable from 'sorted-table'
import TabButton from 'tab-button' import TabButton from 'tab-button'
import { Container, Row, Col } from 'grid' import { Container, Row, Col } from 'grid'
import { import {
createGetObjectsOfType,
createSelector, createSelector,
createFinder, createFinder,
getCheckPermissions, getCheckPermissions,
@ -33,7 +34,17 @@ import { SizeInput, Toggle } from 'form'
import { XoSelect, Size, Text } from 'editable' import { XoSelect, Size, Text } from 'editable'
import { confirm } from 'modal' import { confirm } from 'modal'
import { error } from 'notification' import { error } from 'notification'
import { filter, find, forEach, get, map, mapValues, some } from 'lodash' import {
filter,
find,
forEach,
get,
groupBy,
map,
mapValues,
pick,
some,
} from 'lodash'
import { import {
attachDiskToVm, attachDiskToVm,
createDisk, createDisk,
@ -45,6 +56,7 @@ import {
disconnectVbd, disconnectVbd,
editVdi, editVdi,
exportVdi, exportVdi,
importVdi,
isSrWritable, isSrWritable,
isVmRunning, isVmRunning,
migrateVdi, migrateVdi,
@ -585,6 +597,7 @@ class MigrateVdiModalBody extends Component {
@connectStore(() => ({ @connectStore(() => ({
checkPermissions: getCheckPermissions, checkPermissions: getCheckPermissions,
isAdmin, isAdmin,
allVbds: createGetObjectsOfType('VBD'),
})) }))
export default class TabDisks extends Component { export default class TabDisks extends Component {
constructor (props) { constructor (props) {
@ -652,12 +665,27 @@ export default class TabDisks extends Component {
(vdis, vbds, vm) => mapValues(vdis, vdi => find(vbds, { VDI: vdi.id })) (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 = [ individualActions = [
{ {
handler: exportVdi, handler: exportVdi,
icon: 'export', icon: 'export',
label: _('exportVdi'), label: _('exportVdi'),
}, },
{
disabled: ({ id }, { isVdiAttached }) => isVdiAttached[id],
handler: importVdi,
icon: 'import',
label: _('importVdi'),
},
{ {
handler: this._migrateVdi, handler: this._migrateVdi,
icon: 'vdi-migrate', icon: 'vdi-migrate',
@ -735,6 +763,7 @@ export default class TabDisks extends Component {
actions={ACTIONS} actions={ACTIONS}
collection={vdis} collection={vdis}
columns={vm.virtualizationMode === 'pv' ? COLUMNS_VM_PV : COLUMNS} columns={vm.virtualizationMode === 'pv' ? COLUMNS_VM_PV : COLUMNS}
data-isVdiAttached={this._getIsVdiAttached()}
data-srs={srs} data-srs={srs}
data-vbdsByVdi={this._getVbdsByVdi()} data-vbdsByVdi={this._getVbdsByVdi()}
data-vm={vm} data-vm={vm}