Compare commits
14 Commits
fix-iso-sm
...
fix-refs-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76bdc05320 | ||
|
|
49da3481a8 | ||
|
|
1c3e879da8 | ||
|
|
d82d640c3d | ||
|
|
ffff7e7762 | ||
|
|
4b218e5ee2 | ||
|
|
4917223c2d | ||
|
|
a8d37f1a43 | ||
|
|
88970f3d27 | ||
|
|
6bd6045b6a | ||
|
|
7c3464edd0 | ||
|
|
ee8699893e | ||
|
|
c1aca3e37a | ||
|
|
05cc7c64b4 |
@@ -2598,8 +2598,8 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: 'al SR:',
|
||||
|
||||
// Original text: "VMs to import"
|
||||
vmsToImport: 'VMs para importar',
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
vmsToImport: 'VM{nVms, plural, one {} other {s}} para importar',
|
||||
|
||||
// Original text: "Reset"
|
||||
importVmsCleanList: 'Reiniciar',
|
||||
|
||||
@@ -2657,8 +2657,8 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: 'Sur le SR:',
|
||||
|
||||
// Original text: "VMs to import"
|
||||
vmsToImport: 'VMs à importer',
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
vmsToImport: 'VM{nVms, plural, one {} other {s}} à importer',
|
||||
|
||||
// Original text: "Reset"
|
||||
importVmsCleanList: 'Réinitialiser',
|
||||
|
||||
@@ -2276,7 +2276,7 @@ export default {
|
||||
// Original text: 'To SR:'
|
||||
vmImportToSr: undefined,
|
||||
|
||||
// Original text: 'VMs to import'
|
||||
// Original text: 'VM{nVms, plural, one {} other {s}} to import'
|
||||
vmsToImport: undefined,
|
||||
|
||||
// Original text: 'Reset'
|
||||
|
||||
@@ -2490,7 +2490,7 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: 'Adattárolóra:',
|
||||
|
||||
// Original text: "VMs to import"
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
vmsToImport: 'Importálandó VPS-el',
|
||||
|
||||
// Original text: "Reset"
|
||||
|
||||
@@ -3746,8 +3746,8 @@ export default {
|
||||
// Original text: 'To SR:'
|
||||
vmImportToSr: 'Per SR:',
|
||||
|
||||
// Original text: 'VMs to import'
|
||||
vmsToImport: 'VMs da importare',
|
||||
// Original text: 'VM{nVms, plural, one {} other {s}} to import'
|
||||
vmsToImport: 'VM{nVms, plural, one {} other {s}} da importare',
|
||||
|
||||
// Original text: 'Reset'
|
||||
importVmsCleanList: 'Ripristina',
|
||||
|
||||
@@ -2280,8 +2280,8 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: 'To SR:',
|
||||
|
||||
// Original text: "VMs to import"
|
||||
vmsToImport: 'VMs to import',
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
vmsToImport: 'VM{nVms, plural, one {} other {s}} to import',
|
||||
|
||||
// Original text: "Reset"
|
||||
importVmsCleanList: 'Reset',
|
||||
|
||||
@@ -2279,8 +2279,8 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: 'Enviar para SR:',
|
||||
|
||||
// Original text: "VMs to import"
|
||||
vmsToImport: 'Importar VMs',
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
vmsToImport: 'Importar VM{nVms, plural, one {} other {s}} ',
|
||||
|
||||
// Original text: "Reset"
|
||||
importVmsCleanList: 'Reiniciar',
|
||||
|
||||
@@ -2600,7 +2600,7 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: 'В SR:',
|
||||
|
||||
// Original text: "VMs to import"
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
vmsToImport: 'ВМ для импорта',
|
||||
|
||||
// Original text: "Reset"
|
||||
|
||||
@@ -3223,7 +3223,7 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: 'SR:',
|
||||
|
||||
// Original text: "VMs to import"
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
vmsToImport: "içe aktarılacak VM'ler",
|
||||
|
||||
// Original text: "Reset"
|
||||
|
||||
@@ -1739,7 +1739,7 @@ export default {
|
||||
// Original text: "To SR:"
|
||||
vmImportToSr: '到存储库',
|
||||
|
||||
// Original text: "VMs to import"
|
||||
// Original text: "VM{nVms, plural, one {} other {s}} to import"
|
||||
vmsToImport: '导入虚拟机',
|
||||
|
||||
// Original text: "Reset"
|
||||
|
||||
@@ -9,7 +9,10 @@ const messages = {
|
||||
description: 'Description',
|
||||
deleteSourceVm: 'Delete source VM',
|
||||
expiration: 'Expiration',
|
||||
hostIp: 'Host IP',
|
||||
keyValue: '{key}: {value}',
|
||||
sslCertificate: 'SSL certificate',
|
||||
vmSrUsage: '{used} used of {total} ({free} free)',
|
||||
|
||||
notDefined: 'Not defined',
|
||||
statusConnecting: 'Connecting',
|
||||
@@ -1609,12 +1612,13 @@ const messages = {
|
||||
// ---- VM import ---
|
||||
fileType: 'File type:',
|
||||
fromUrl: 'From URL',
|
||||
fromVmware: 'From VMware',
|
||||
importVmsList: 'Drop OVA or XVA files here to import Virtual Machines.',
|
||||
noSelectedVms: 'No selected VMs.',
|
||||
url: 'URL:',
|
||||
vmImportToPool: 'To Pool:',
|
||||
vmImportToSr: 'To SR:',
|
||||
vmsToImport: 'VMs to import',
|
||||
vmsToImport: 'VM{nVms, plural, one {} other {s}} to import',
|
||||
importVmsCleanList: 'Reset',
|
||||
vmImportSuccess: 'VM import success',
|
||||
vmImportFailed: 'VM import failed',
|
||||
|
||||
@@ -3468,3 +3468,10 @@ export const synchronizeNetbox = pools =>
|
||||
body: _('syncNetboxWarning'),
|
||||
icon: 'refresh',
|
||||
}).then(() => _call('netbox.synchronize', { pools: resolveIds(pools) }))
|
||||
|
||||
// ESXi import ---------------------------------------------------------------
|
||||
|
||||
export const esxiConnect = (host, user, password, sslVerify) =>
|
||||
_call('esxi.connect', { host, user, password, sslVerify })
|
||||
|
||||
export const importVmFromEsxi = params => _call('vm.importFromEsxi', params)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { NavLink, NavTabs } from 'nav'
|
||||
import { routes } from 'utils'
|
||||
|
||||
import DiskImport from '../disk-import'
|
||||
import EsxiImport from '../vm-import/esxi-import'
|
||||
import VmImport from '../vm-import'
|
||||
|
||||
const HEADER = (
|
||||
@@ -25,6 +26,9 @@ const HEADER = (
|
||||
<NavLink to='/import/disk'>
|
||||
<Icon icon='disk' /> {_('labelDisk')}
|
||||
</NavLink>
|
||||
<NavLink to='/import/vmware'>
|
||||
<Icon icon='vm' /> {_('fromVmware')}
|
||||
</NavLink>
|
||||
</NavTabs>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -34,6 +38,7 @@ const HEADER = (
|
||||
const Import = routes('vm', {
|
||||
disk: DiskImport,
|
||||
vm: VmImport,
|
||||
vmware: EsxiImport,
|
||||
})(({ children }) => (
|
||||
<Page header={HEADER} title='newImport' formatTitle>
|
||||
{children}
|
||||
|
||||
@@ -494,6 +494,11 @@ export default class Menu extends Component {
|
||||
icon: 'disk',
|
||||
label: 'labelDisk',
|
||||
},
|
||||
{
|
||||
to: '/import/vmware',
|
||||
icon: 'vm',
|
||||
label: 'fromVmware',
|
||||
},
|
||||
],
|
||||
},
|
||||
!(noOperatablePools && noResourceSets) && {
|
||||
|
||||
251
packages/xo-web/src/xo-app/vm-import/esxi-import.js
Normal file
251
packages/xo-web/src/xo-app/vm-import/esxi-import.js
Normal file
@@ -0,0 +1,251 @@
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import decorate from 'apply-decorators'
|
||||
import React from 'react'
|
||||
import { esxiConnect, importVmFromEsxi, isSrWritable } from 'xo'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { Input } from 'debounce-input-decorator'
|
||||
import { InputCol, LabelCol, Row } from 'form-grid'
|
||||
import { isEmpty, map } from 'lodash'
|
||||
import { linkState } from 'reaclette-utils'
|
||||
import { Password, Select } from 'form'
|
||||
import { SelectNetwork, SelectPool, SelectSr } from 'select-objects'
|
||||
|
||||
import VmData from './vm-data'
|
||||
|
||||
const getInitialState = () => ({
|
||||
hasCertificate: true,
|
||||
hostIp: '',
|
||||
isConnected: false,
|
||||
network: undefined,
|
||||
password: '',
|
||||
pool: undefined,
|
||||
sr: undefined,
|
||||
user: '',
|
||||
vm: undefined,
|
||||
vmData: undefined,
|
||||
vmsById: undefined,
|
||||
})
|
||||
|
||||
const EsxiImport = decorate([
|
||||
provideState({
|
||||
initialState: getInitialState,
|
||||
effects: {
|
||||
importVm:
|
||||
() =>
|
||||
({ hasCertificate, hostIp, network, password, sr, user, vmData }) => {
|
||||
importVmFromEsxi({
|
||||
host: hostIp,
|
||||
network: network.id,
|
||||
password,
|
||||
sr,
|
||||
sslVerify: hasCertificate,
|
||||
user,
|
||||
vm: vmData.id,
|
||||
})
|
||||
},
|
||||
connect:
|
||||
() =>
|
||||
async ({ hostIp, hasCertificate, password, user }) => {
|
||||
const vms = await esxiConnect(hostIp, user, password, hasCertificate)
|
||||
return { isConnected: true, vmsById: vms.reduce((vms, vm) => ({ ...vms, [vm.id]: vm }), {}) }
|
||||
},
|
||||
linkState,
|
||||
onChangeVm: (_, vm) => state => ({ vm, vmData: state.vmsById[vm.value] }),
|
||||
onChangeVmData: (_, vmData) => ({ vmData }),
|
||||
onChangeNetwork: (_, network) => ({ network }),
|
||||
onChangePool: (_, pool) => ({ pool, sr: pool.default_SR }),
|
||||
onChangeSr: (_, sr) => ({ sr }),
|
||||
toggleCertificateCheck:
|
||||
(_, { target: { checked, name } }) =>
|
||||
state => ({
|
||||
...state,
|
||||
[name]: checked,
|
||||
}),
|
||||
reset: getInitialState,
|
||||
},
|
||||
computed: {
|
||||
selectVmOptions: ({ vmsById }) =>
|
||||
map(vmsById, vm => ({
|
||||
label: vm.nameLabel,
|
||||
value: vm.id,
|
||||
})),
|
||||
networkpredicate:
|
||||
({ pool }) =>
|
||||
network =>
|
||||
network.$poolId === pool?.uuid,
|
||||
srPredicate:
|
||||
({ pool }) =>
|
||||
sr =>
|
||||
isSrWritable(sr) && sr.$poolId === pool?.uuid,
|
||||
},
|
||||
}),
|
||||
injectIntl,
|
||||
injectState,
|
||||
({
|
||||
effects: {
|
||||
connect,
|
||||
importVm,
|
||||
linkState,
|
||||
onChangeVm,
|
||||
onChangeVmData,
|
||||
onChangeNetwork,
|
||||
onChangePool,
|
||||
onChangeSr,
|
||||
reset,
|
||||
srPredicate,
|
||||
toggleCertificateCheck,
|
||||
},
|
||||
intl: { formatMessage },
|
||||
state: {
|
||||
hasCertificate,
|
||||
hostIp,
|
||||
isConnected,
|
||||
network,
|
||||
networkPredicate,
|
||||
password,
|
||||
pool,
|
||||
selectVmOptions,
|
||||
sr,
|
||||
user,
|
||||
vm,
|
||||
vmsById,
|
||||
vmData,
|
||||
},
|
||||
}) => (
|
||||
<div>
|
||||
{!isConnected && (
|
||||
<form id='esxi-connect-form'>
|
||||
<Row>
|
||||
<LabelCol>{_('hostIp')}</LabelCol>
|
||||
<InputCol>
|
||||
<Input
|
||||
className='form-control'
|
||||
name='hostIp'
|
||||
onChange={linkState}
|
||||
placeholder='192.168.2.20'
|
||||
required
|
||||
value={hostIp}
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('user')}</LabelCol>
|
||||
<InputCol>
|
||||
<Input
|
||||
className='form-control'
|
||||
name='user'
|
||||
onChange={linkState}
|
||||
placeholder={formatMessage(messages.user)}
|
||||
required
|
||||
value={user}
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('password')}</LabelCol>
|
||||
<InputCol>
|
||||
<Password
|
||||
name='password'
|
||||
onChange={linkState}
|
||||
placeholder={formatMessage(messages.password)}
|
||||
required
|
||||
value={password}
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('sslCertificate')}</LabelCol>
|
||||
<InputCol>
|
||||
<input
|
||||
checked={hasCertificate}
|
||||
name='hasCertificate'
|
||||
onChange={toggleCertificateCheck}
|
||||
type='checkbox'
|
||||
value={hasCertificate}
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
<div className='form-group pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='mr-1'
|
||||
form='esxi-connect-form'
|
||||
handler={connect}
|
||||
icon='connect'
|
||||
type='submit'
|
||||
>
|
||||
{_('serverConnect')}
|
||||
</ActionButton>
|
||||
<Button onClick={reset}>{_('formReset')}</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{isConnected && (
|
||||
<form id='esxi-migrate-form'>
|
||||
<Row>
|
||||
<LabelCol>{_('vm')}</LabelCol>
|
||||
<InputCol>
|
||||
<Select disabled={isEmpty(vmsById)} onChange={onChangeVm} options={selectVmOptions} required value={vm} />
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('vmImportToPool')}</LabelCol>
|
||||
<InputCol>
|
||||
<SelectPool onChange={onChangePool} required value={pool} />
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('vmImportToSr')}</LabelCol>
|
||||
<InputCol>
|
||||
<SelectSr
|
||||
disabled={pool === undefined}
|
||||
onChange={onChangeSr}
|
||||
predicate={srPredicate}
|
||||
required
|
||||
value={sr}
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('network')}</LabelCol>
|
||||
<InputCol>
|
||||
<SelectNetwork
|
||||
disabled={pool === undefined}
|
||||
onChange={onChangeNetwork}
|
||||
predicate={networkPredicate}
|
||||
required
|
||||
value={network}
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
{vm !== undefined && (
|
||||
<div>
|
||||
<hr />
|
||||
<h5>{_('vmsToImport', { nVms: 1 })}</h5>
|
||||
<VmData data={vmData} onChange={onChangeVmData} pool={pool} />
|
||||
</div>
|
||||
)}
|
||||
<div className='form-group pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='mr-1'
|
||||
disabled={vm === undefined}
|
||||
form='esxi-migrate-form'
|
||||
handler={importVm}
|
||||
icon='import'
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
</ActionButton>
|
||||
<Button onClick={reset}>{_('formReset')}</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
])
|
||||
|
||||
export default EsxiImport
|
||||
@@ -1,302 +1,29 @@
|
||||
import * as CM from 'complex-matcher'
|
||||
import * as FormGrid from 'form-grid'
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import Component from 'base-component'
|
||||
import Dropzone from 'dropzone'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { importVm, importVms, isSrWritable } from 'xo'
|
||||
import { Select, SizeInput, Toggle } from 'form'
|
||||
import { createFinder, createGetObject, createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { connectStore, formatSize, mapPlus, noop } from 'utils'
|
||||
import { Input } from 'debounce-input-decorator'
|
||||
import { Container } from 'grid'
|
||||
import { Toggle } from 'form'
|
||||
|
||||
import { SelectNetwork, SelectPool, SelectSr } from 'select-objects'
|
||||
|
||||
import parseOvaFile from './ova'
|
||||
|
||||
import styles from './index.css'
|
||||
import XvaImport from './xva-import'
|
||||
import UrlImport from './url-import'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const FILE_TYPES = [
|
||||
{
|
||||
label: 'XVA',
|
||||
value: 'xva',
|
||||
},
|
||||
]
|
||||
|
||||
const FORMAT_TO_HANDLER = {
|
||||
ova: parseOvaFile,
|
||||
xva: noop,
|
||||
const RENDER_BY_TYPE = {
|
||||
xva: <XvaImport />,
|
||||
url: <UrlImport />,
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@connectStore(
|
||||
() => {
|
||||
const getHostMaster = createGetObject((_, props) => props.pool.master)
|
||||
const getPifs = createGetObjectsOfType('PIF').pick((state, props) => getHostMaster(state, props).$PIFs)
|
||||
const getDefaultNetworkId = createSelector(createFinder(getPifs, [pif => pif.management]), pif => pif.$network)
|
||||
|
||||
return {
|
||||
defaultNetwork: getDefaultNetworkId,
|
||||
}
|
||||
},
|
||||
{ withRef: true }
|
||||
)
|
||||
class VmData extends Component {
|
||||
static propTypes = {
|
||||
descriptionLabel: PropTypes.string,
|
||||
disks: PropTypes.objectOf(
|
||||
PropTypes.shape({
|
||||
capacity: PropTypes.number.isRequired,
|
||||
descriptionLabel: PropTypes.string.isRequired,
|
||||
nameLabel: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
compression: PropTypes.string,
|
||||
})
|
||||
),
|
||||
memory: PropTypes.number,
|
||||
nameLabel: PropTypes.string,
|
||||
nCpus: PropTypes.number,
|
||||
networks: PropTypes.array,
|
||||
pool: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
get value() {
|
||||
const { props, refs } = this
|
||||
return {
|
||||
descriptionLabel: refs.descriptionLabel.value,
|
||||
disks: map(props.disks, ({ capacity, path, compression, position }, diskId) => ({
|
||||
capacity,
|
||||
descriptionLabel: refs[`disk-description-${diskId}`].value,
|
||||
nameLabel: refs[`disk-name-${diskId}`].value,
|
||||
path,
|
||||
position,
|
||||
compression,
|
||||
})),
|
||||
memory: +refs.memory.value,
|
||||
nameLabel: refs.nameLabel.value,
|
||||
networks: map(props.networks, (_, networkId) => {
|
||||
const network = refs[`network-${networkId}`].value
|
||||
return network.id ? network.id : network
|
||||
}),
|
||||
nCpus: +refs.nCpus.value,
|
||||
tables: props.tables,
|
||||
}
|
||||
}
|
||||
|
||||
_getNetworkPredicate = createSelector(
|
||||
() => this.props.pool.id,
|
||||
id => network => network.$pool === id
|
||||
)
|
||||
|
||||
render() {
|
||||
const { descriptionLabel, defaultNetwork, disks, memory, nameLabel, nCpus, networks } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmNameLabel')}</label>
|
||||
<input className='form-control' ref='nameLabel' defaultValue={nameLabel} type='text' required />
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmNameDescription')}</label>
|
||||
<input className='form-control' ref='descriptionLabel' defaultValue={descriptionLabel} type='text' />
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('nCpus')}</label>
|
||||
<input className='form-control' ref='nCpus' defaultValue={nCpus} type='number' required />
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmMemory')}</label>
|
||||
<SizeInput defaultValue={memory} ref='memory' required />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
{!isEmpty(disks)
|
||||
? map(disks, (disk, diskId) => (
|
||||
<Row key={diskId}>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>
|
||||
{_('diskInfo', {
|
||||
position: `${disk.position}`,
|
||||
capacity: formatSize(disk.capacity),
|
||||
})}
|
||||
</label>
|
||||
<input
|
||||
className='form-control'
|
||||
ref={`disk-name-${diskId}`}
|
||||
defaultValue={disk.nameLabel}
|
||||
type='text'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('diskDescription')}</label>
|
||||
<input
|
||||
className='form-control'
|
||||
ref={`disk-description-${diskId}`}
|
||||
defaultValue={disk.descriptionLabel}
|
||||
type='text'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
))
|
||||
: _('noDisks')}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
{networks.length > 0
|
||||
? map(networks, (name, networkId) => (
|
||||
<div className='form-group' key={networkId}>
|
||||
<label>{_('networkInfo', { name })}</label>
|
||||
<SelectNetwork
|
||||
defaultValue={defaultNetwork}
|
||||
ref={`network-${networkId}`}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
: _('noNetworks')}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const parseFile = async (file, type, func) => {
|
||||
try {
|
||||
return {
|
||||
data: await func(file),
|
||||
file,
|
||||
type,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return { error, file, type }
|
||||
}
|
||||
}
|
||||
|
||||
const getRedirectionUrl = vms =>
|
||||
vms.length === 0
|
||||
? undefined // no redirect
|
||||
: vms.length === 1
|
||||
? `/vms/${vms[0]}`
|
||||
: `/home?s=${encodeURIComponent(new CM.Property('id', new CM.Or(vms.map(_ => new CM.String(_)))).toString())}&t=VM`
|
||||
|
||||
export default class Import extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isFromUrl: false,
|
||||
type: {
|
||||
label: 'XVA',
|
||||
value: 'xva',
|
||||
},
|
||||
url: '',
|
||||
vms: [],
|
||||
}
|
||||
}
|
||||
|
||||
_import = () => {
|
||||
const { state } = this
|
||||
return importVms(
|
||||
mapPlus(state.vms, (vm, push, vmIndex) => {
|
||||
if (!vm.error) {
|
||||
const ref = this.refs[`vm-data-${vmIndex}`]
|
||||
push({
|
||||
...vm,
|
||||
data: ref && ref.value,
|
||||
})
|
||||
}
|
||||
}),
|
||||
state.sr
|
||||
)
|
||||
}
|
||||
|
||||
_importVmFromUrl = () => {
|
||||
const { type, url } = this.state
|
||||
const file = {
|
||||
name: decodeURIComponent(url.slice(url.lastIndexOf('/') + 1)),
|
||||
}
|
||||
return importVm(file, type.value, undefined, this.state.sr, url)
|
||||
}
|
||||
|
||||
_handleDrop = async files => {
|
||||
this.setState({
|
||||
vms: [],
|
||||
})
|
||||
|
||||
const vms = await Promise.all(
|
||||
mapPlus(files, (file, push) => {
|
||||
const { name } = file
|
||||
const extIndex = name.lastIndexOf('.')
|
||||
|
||||
let func
|
||||
let type
|
||||
|
||||
if (extIndex >= 0 && (type = name.slice(extIndex + 1).toLowerCase()) && (func = FORMAT_TO_HANDLER[type])) {
|
||||
push(parseFile(file, type, func))
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
this.setState({
|
||||
vms: orderBy(vms, vm => [vm.error != null, vm.type, vm.file.name]),
|
||||
})
|
||||
}
|
||||
|
||||
_handleCleanSelectedVms = () => {
|
||||
this.setState({
|
||||
vms: [],
|
||||
})
|
||||
}
|
||||
|
||||
_handleSelectedPool = pool => {
|
||||
if (pool === '') {
|
||||
this.setState({
|
||||
pool: undefined,
|
||||
sr: undefined,
|
||||
srPredicate: undefined,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
pool,
|
||||
sr: pool.default_SR,
|
||||
srPredicate: sr => sr.$pool === this.state.pool.id && isSrWritable(sr),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_handleSelectedSr = sr => {
|
||||
this.setState({
|
||||
sr: sr === '' ? undefined : sr,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isFromUrl, pool, sr, srPredicate, type, url, vms } = this.state
|
||||
const { isFromUrl } = this.state
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -304,113 +31,7 @@ export default class Import extends Component {
|
||||
<p>
|
||||
<Toggle value={isFromUrl} onChange={this.toggleState('isFromUrl')} /> {_('fromUrl')}
|
||||
</p>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('vmImportToPool')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<SelectPool value={pool} onChange={this._handleSelectedPool} required />
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('vmImportToSr')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<SelectSr
|
||||
disabled={!pool}
|
||||
onChange={this._handleSelectedSr}
|
||||
predicate={srPredicate}
|
||||
required
|
||||
value={sr}
|
||||
/>
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
{sr &&
|
||||
(!isFromUrl ? (
|
||||
<div>
|
||||
<Dropzone onDrop={this._handleDrop} message={_('importVmsList')} />
|
||||
<hr />
|
||||
<h5>{_('vmsToImport')}</h5>
|
||||
{vms.length > 0 ? (
|
||||
<div>
|
||||
{map(vms, ({ data, error, file, type }, vmIndex) => (
|
||||
<div key={file.preview} className={styles.vmContainer}>
|
||||
<strong>{file.name}</strong>
|
||||
<span className='pull-right'>
|
||||
<strong>{`(${formatSize(file.size)})`}</strong>
|
||||
</span>
|
||||
{!error ? (
|
||||
data && (
|
||||
<div>
|
||||
<hr />
|
||||
<div className='alert alert-info' role='alert'>
|
||||
<strong>{_('vmImportFileType', { type })}</strong> {_('vmImportConfigAlert')}
|
||||
</div>
|
||||
<VmData {...data} ref={`vm-data-${vmIndex}`} pool={pool} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
<hr />
|
||||
<div className='alert alert-danger' role='alert'>
|
||||
<strong>{_('vmImportError')}</strong>{' '}
|
||||
{(error && error.message) || _('noVmImportErrorDescription')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p>{_('noSelectedVms')}</p>
|
||||
)}
|
||||
<hr />
|
||||
<div className='form-group pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={!vms.length}
|
||||
className='mr-1'
|
||||
form='import-form'
|
||||
handler={this._import}
|
||||
icon='import'
|
||||
redirectOnSuccess={getRedirectionUrl}
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
</ActionButton>
|
||||
<Button onClick={this._handleCleanSelectedVms}>{_('importVmsCleanList')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('url')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<Input
|
||||
className='form-control'
|
||||
onChange={this.linkState('url')}
|
||||
placeholder='https://my-company.net/vm.xva'
|
||||
type='url'
|
||||
/>
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('fileType')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<Select onChange={this.linkState('type')} options={FILE_TYPES} required value={type} />
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='mr-1 mt-1'
|
||||
disabled={isEmpty(url)}
|
||||
form='import-form'
|
||||
handler={this._importVmFromUrl}
|
||||
icon='import'
|
||||
redirectOnSuccess={getRedirectionUrl}
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
))}
|
||||
{RENDER_BY_TYPE[isFromUrl ? 'url' : 'xva']}
|
||||
</form>
|
||||
</Container>
|
||||
)
|
||||
|
||||
113
packages/xo-web/src/xo-app/vm-import/url-import.js
Normal file
113
packages/xo-web/src/xo-app/vm-import/url-import.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import decorate from 'apply-decorators'
|
||||
import React from 'react'
|
||||
import { importVm, isSrWritable } from 'xo'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { Input } from 'debounce-input-decorator'
|
||||
import { InputCol, LabelCol, Row } from 'form-grid'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { linkState } from 'reaclette-utils'
|
||||
import { Select } from 'form'
|
||||
import { SelectPool, SelectSr } from 'select-objects'
|
||||
|
||||
import { getRedirectionUrl } from './utils'
|
||||
|
||||
const FILE_TYPES = [
|
||||
{
|
||||
label: 'XVA',
|
||||
value: 'xva',
|
||||
},
|
||||
]
|
||||
|
||||
const getInitialState = () => ({
|
||||
pool: undefined,
|
||||
sr: undefined,
|
||||
type: {
|
||||
label: 'XVA',
|
||||
value: 'xva',
|
||||
},
|
||||
url: '',
|
||||
})
|
||||
|
||||
const UrlImport = decorate([
|
||||
provideState({
|
||||
initialState: getInitialState,
|
||||
effects: {
|
||||
handleImport() {
|
||||
const { type, url } = this.state
|
||||
const file = {
|
||||
name: decodeURIComponent(url.slice(url.lastIndexOf('/') + 1)),
|
||||
}
|
||||
importVm(file, type.value, undefined, this.state.sr, url)
|
||||
},
|
||||
linkState,
|
||||
onChangePool: (_, pool) => ({ pool, sr: pool.default_SR }),
|
||||
onChangeSr: (_, sr) => ({ sr }),
|
||||
reset: getInitialState,
|
||||
},
|
||||
computed: {
|
||||
srPredicate:
|
||||
({ pool }) =>
|
||||
sr =>
|
||||
isSrWritable(sr) && sr.$poolId === pool?.uuid,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({
|
||||
effects: { handleImport, linkState, onChangePool, onChangeSr, reset },
|
||||
state: { pool, sr, srPredicate, type, url },
|
||||
}) => (
|
||||
<div>
|
||||
<Row>
|
||||
<LabelCol>{_('vmImportToPool')}</LabelCol>
|
||||
<InputCol>
|
||||
<SelectPool value={pool} onChange={onChangePool} required />
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('vmImportToSr')}</LabelCol>
|
||||
<InputCol>
|
||||
<SelectSr disabled={pool === undefined} onChange={onChangeSr} predicate={srPredicate} required value={sr} />
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('url')}</LabelCol>
|
||||
<InputCol>
|
||||
<Input
|
||||
className='form-control'
|
||||
name='url'
|
||||
onChange={linkState}
|
||||
placeholder='https://my-company.net/vm.xva'
|
||||
type='url'
|
||||
/>
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('fileType')}</LabelCol>
|
||||
<InputCol>
|
||||
<Select name='type' onChange={linkState} options={FILE_TYPES} required value={type} />
|
||||
</InputCol>
|
||||
</Row>
|
||||
|
||||
<div className='form-group pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='mr-1'
|
||||
disabled={isEmpty(url)}
|
||||
form='import-form'
|
||||
handler={handleImport}
|
||||
icon='import'
|
||||
redirectOnSuccess={getRedirectionUrl}
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
</ActionButton>
|
||||
<Button onClick={reset}>{_('formReset')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
])
|
||||
|
||||
export default UrlImport
|
||||
8
packages/xo-web/src/xo-app/vm-import/utils.js
Normal file
8
packages/xo-web/src/xo-app/vm-import/utils.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as CM from 'complex-matcher'
|
||||
|
||||
const getRedirectionUrl = vms =>
|
||||
vms.length === 0
|
||||
? undefined // no redirect
|
||||
: vms.length === 1
|
||||
? `/vms/${vms[0]}`
|
||||
: `/home?s=${encodeURIComponent(new CM.Property('id', new CM.Or(vms.map(_ => new CM.String(_)))).toString())}&t=VM`
|
||||
187
packages/xo-web/src/xo-app/vm-import/vm-data.js
Normal file
187
packages/xo-web/src/xo-app/vm-import/vm-data.js
Normal file
@@ -0,0 +1,187 @@
|
||||
import _ from 'intl'
|
||||
import decorate from 'apply-decorators'
|
||||
import React from 'react'
|
||||
import { Col, Row } from 'grid'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { createFinder, createGetObject, createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { map } from 'lodash'
|
||||
import { SelectNetwork } from 'select-objects'
|
||||
import { SizeInput } from 'form'
|
||||
|
||||
const VmData = decorate([
|
||||
connectStore(() => {
|
||||
const getHostMaster = createGetObject((_, props) => props.pool.master)
|
||||
const getPifs = createGetObjectsOfType('PIF').pick((state, props) => getHostMaster(state, props).$PIFs)
|
||||
const getDefaultNetworkId = createSelector(createFinder(getPifs, [pif => pif.management]), pif => pif.$network)
|
||||
|
||||
return {
|
||||
defaultNetwork: getDefaultNetworkId,
|
||||
}
|
||||
}),
|
||||
provideState({
|
||||
effects: {
|
||||
onChangeDisks(_, { target: { name, value } }) {
|
||||
const { onChange, data: prevValue } = this.props
|
||||
// name: nameLabel-index or descriptionLabel-index
|
||||
const data = name.split('-')
|
||||
const index = data[1]
|
||||
onChange({
|
||||
...prevValue,
|
||||
disks: {
|
||||
...prevValue.disks,
|
||||
[index]: { ...prevValue.disks[index], [data[0]]: value },
|
||||
},
|
||||
})
|
||||
},
|
||||
onChangeMemory(_, memory) {
|
||||
const { onChange, data: prevData } = this.props
|
||||
onChange({
|
||||
...prevData,
|
||||
memory,
|
||||
})
|
||||
},
|
||||
onChangeNCpus(_, { target: { value } }) {
|
||||
const { onChange, data: prevData } = this.props
|
||||
onChange({
|
||||
...prevData,
|
||||
nCpus: +value,
|
||||
})
|
||||
},
|
||||
onChangeNetworks(_, network, networkIndex) {
|
||||
const { onChange, data } = this.props
|
||||
onChange({
|
||||
...data,
|
||||
networks: data.networks.map((prevNetwork, index) => (index === networkIndex ? network.id : prevNetwork)),
|
||||
})
|
||||
},
|
||||
onChangeValue(_, { target: { name, value } }) {
|
||||
const { onChange, data } = this.props
|
||||
onChange({
|
||||
...data,
|
||||
[name]: value,
|
||||
})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
networkPredicate:
|
||||
(_, { pool }) =>
|
||||
network =>
|
||||
pool.id === network.$pool,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({
|
||||
data,
|
||||
defaultNetwork,
|
||||
effects: { onChangeDisks, onChangeMemory, onChangeNCpus, onChangeNetworks, onChangeValue },
|
||||
state: { networkPredicate },
|
||||
}) => (
|
||||
<div>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmNameLabel')}</label>
|
||||
<input
|
||||
className='form-control'
|
||||
name='nameLabel'
|
||||
onChange={onChangeValue}
|
||||
type='text'
|
||||
required
|
||||
value={data.nameLabel}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmNameDescription')}</label>
|
||||
<input
|
||||
className='form-control'
|
||||
name='descriptionLabel'
|
||||
onChange={onChangeValue}
|
||||
type='text'
|
||||
value={data.descriptionLabel}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('nCpus')}</label>
|
||||
<input className='form-control' onChange={onChangeNCpus} type='number' required value={data.nCpus} />
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label>{_('vmMemory')}</label>
|
||||
<SizeInput onChange={onChangeMemory} required value={data.memory} />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
{map(data.disks, (disk, diskId) => (
|
||||
<Row key={diskId}>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>
|
||||
{_('diskInfo', {
|
||||
position: `${disk.position}`,
|
||||
capacity: formatSize(disk.capacity),
|
||||
})}
|
||||
</label>
|
||||
<input
|
||||
className='form-control'
|
||||
name={`nameLabel-${diskId}`}
|
||||
onChange={onChangeDisks}
|
||||
type='text'
|
||||
required
|
||||
value={disk.nameLabel}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('diskDescription')}</label>
|
||||
<input
|
||||
className='form-control'
|
||||
name={`descriptionLabel-${diskId}`}
|
||||
onChange={onChangeDisks}
|
||||
type='text'
|
||||
required
|
||||
value={disk.descriptionLabel}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
{map(data.networks, (networkId, index) => (
|
||||
<div className='form-group' key={networkId}>
|
||||
<label>{_('networkInfo', { name: index + 1 })}</label>
|
||||
<SelectNetwork
|
||||
onChange={network => onChangeNetworks(network, index)}
|
||||
predicate={networkPredicate}
|
||||
value={data.networks[index] ?? defaultNetwork}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Col>
|
||||
</Row>
|
||||
{data.storage !== undefined && (
|
||||
<Row className='mt-1'>
|
||||
<Col mediumSize={12}>
|
||||
<div className='form-group'>
|
||||
<label>{_('homeSrPage')}:</label>{' '}
|
||||
<span>
|
||||
{_('vmSrUsage', {
|
||||
free: formatSize(data.storage.free),
|
||||
total: formatSize(data.storage.used + data.storage.free),
|
||||
used: formatSize(data.storage.used),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
])
|
||||
|
||||
export default VmData
|
||||
202
packages/xo-web/src/xo-app/vm-import/xva-import.js
Normal file
202
packages/xo-web/src/xo-app/vm-import/xva-import.js
Normal file
@@ -0,0 +1,202 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import decorate from 'apply-decorators'
|
||||
import Dropzone from 'dropzone'
|
||||
import React from 'react'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { connectStore, formatSize, mapPlus, noop } from 'utils'
|
||||
import { importVms, isSrWritable } from 'xo'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { InputCol, LabelCol, Row } from 'form-grid'
|
||||
import { orderBy } from 'lodash'
|
||||
import { SelectPool, SelectSr } from 'select-objects'
|
||||
|
||||
import parseOvaFile from './ova'
|
||||
import styles from './index.css'
|
||||
import VmData from './vm-data'
|
||||
import { getRedirectionUrl } from './utils'
|
||||
|
||||
const FORMAT_TO_HANDLER = {
|
||||
ova: parseOvaFile,
|
||||
xva: noop,
|
||||
}
|
||||
|
||||
const parseFile = async (file, type, func) => {
|
||||
try {
|
||||
return {
|
||||
data: await func(file),
|
||||
file,
|
||||
type,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return { error, file, type }
|
||||
}
|
||||
}
|
||||
|
||||
const getInitialState = () => ({
|
||||
pool: undefined,
|
||||
sr: undefined,
|
||||
vms: [],
|
||||
})
|
||||
|
||||
const XvaImport = decorate([
|
||||
connectStore(() => ({
|
||||
networksByName: createGetObjectsOfType('network').groupBy('name_label'),
|
||||
})),
|
||||
provideState({
|
||||
initialState: getInitialState,
|
||||
effects: {
|
||||
handleImport:
|
||||
() =>
|
||||
({ sr, vms }) => {
|
||||
importVms(
|
||||
mapPlus(vms, (vm, push) => {
|
||||
if (!vm.error) {
|
||||
const { data } = vm
|
||||
push(
|
||||
data === undefined
|
||||
? { ...vm }
|
||||
: {
|
||||
...vm,
|
||||
data: {
|
||||
...vm.data,
|
||||
disks: Object.values(vm.data.disks),
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}),
|
||||
sr
|
||||
)
|
||||
},
|
||||
onChangePool: (_, pool) => ({ pool, sr: pool.default_SR }),
|
||||
onChangeSr: (_, sr) => ({ sr }),
|
||||
onChangeVmData: (_, data, vmIndex) => state => {
|
||||
const vms = [...state.vms]
|
||||
vms[vmIndex].data = data
|
||||
return { vms }
|
||||
},
|
||||
onDrop: (_, files) => async (_, props) => {
|
||||
const vms = (
|
||||
await Promise.all(
|
||||
mapPlus(files, (file, push) => {
|
||||
const { name } = file
|
||||
const extIndex = name.lastIndexOf('.')
|
||||
|
||||
let func
|
||||
let type
|
||||
|
||||
if (
|
||||
extIndex >= 0 &&
|
||||
(type = name.slice(extIndex + 1).toLowerCase()) &&
|
||||
(func = FORMAT_TO_HANDLER[type])
|
||||
) {
|
||||
push(parseFile(file, type, func))
|
||||
}
|
||||
})
|
||||
)
|
||||
).map(vm => {
|
||||
const { data } = vm
|
||||
return data === undefined
|
||||
? vm
|
||||
: {
|
||||
...vm,
|
||||
data: {
|
||||
...data,
|
||||
networks: data.networks.map(name => props.networksByName[name][0].id),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
vms: orderBy(vms, vm => [vm.error != null, vm.type, vm.file.name]),
|
||||
}
|
||||
},
|
||||
reset: getInitialState,
|
||||
},
|
||||
computed: {
|
||||
srPredicate:
|
||||
({ pool }) =>
|
||||
sr =>
|
||||
isSrWritable(sr) && sr.$poolId === pool?.uuid,
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({
|
||||
effects: { handleImport, onChangePool, onChangeSr, onChangeVmData, onDrop, reset },
|
||||
state: { pool, sr, srPredicate, vms },
|
||||
}) => (
|
||||
<div>
|
||||
<Row>
|
||||
<LabelCol>{_('vmImportToPool')}</LabelCol>
|
||||
<InputCol>
|
||||
<SelectPool value={pool} onChange={onChangePool} required />
|
||||
</InputCol>
|
||||
</Row>
|
||||
<Row>
|
||||
<LabelCol>{_('vmImportToSr')}</LabelCol>
|
||||
<InputCol>
|
||||
<SelectSr disabled={pool === undefined} onChange={onChangeSr} predicate={srPredicate} required value={sr} />
|
||||
</InputCol>
|
||||
</Row>
|
||||
<div>
|
||||
<Dropzone onDrop={onDrop} message={_('importVmsList')} />
|
||||
<hr />
|
||||
<h5>{_('vmsToImport', { nVms: vms.length })}</h5>
|
||||
{vms.length > 0 ? (
|
||||
<div>
|
||||
{vms.map(({ data, error, file, type }, vmIndex) => (
|
||||
<div key={file.preview} className={styles.vmContainer}>
|
||||
<strong>{file.name}</strong>
|
||||
<span className='pull-right'>
|
||||
<strong>{`(${formatSize(file.size)})`}</strong>
|
||||
</span>
|
||||
{!error ? (
|
||||
data && (
|
||||
<div>
|
||||
<hr />
|
||||
<div className='alert alert-info' role='alert'>
|
||||
<strong>{_('vmImportFileType', { type })}</strong> {_('vmImportConfigAlert')}
|
||||
</div>
|
||||
<VmData data={data} onChange={data => onChangeVmData(data, vmIndex)} pool={pool} />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
<hr />
|
||||
<div className='alert alert-danger' role='alert'>
|
||||
<strong>{_('vmImportError')}</strong>{' '}
|
||||
{(error && error.message) || _('noVmImportErrorDescription')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p>{_('noSelectedVms')}</p>
|
||||
)}
|
||||
<hr />
|
||||
<div className='form-group pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={vms.length === 0}
|
||||
className='mr-1'
|
||||
form='import-form'
|
||||
handler={handleImport}
|
||||
icon='import'
|
||||
redirectOnSuccess={getRedirectionUrl}
|
||||
type='submit'
|
||||
>
|
||||
{_('newImport')}
|
||||
</ActionButton>
|
||||
<Button onClick={reset}>{_('importVmsCleanList')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
])
|
||||
|
||||
export default XvaImport
|
||||
Reference in New Issue
Block a user