mirror of https://github.com/apache/cloudstack.git
ui: changes in migrate vm storage and migrate volume form (#5145)
Better forms in UI for migrating VMs and volumes. - Show option to migrate with storage while live migrating a VM - For VM storage migration (stopped VM), allow migrating volumes to specific primary storages - Show primary storage details in migrate volume form Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
parent
14f3b24975
commit
2df82d8188
|
|
@ -2565,13 +2565,21 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
|
|||
sc.setParameters("dataCenterId", zoneId);
|
||||
}
|
||||
if (pod != null) {
|
||||
sc.setParameters("podId", pod);
|
||||
SearchCriteria<StoragePoolJoinVO> ssc = _poolJoinDao.createSearchCriteria();
|
||||
ssc.addOr("podId", Op.EQ, pod);
|
||||
ssc.addOr("podId", Op.NULL);
|
||||
|
||||
sc.addAnd("podId", SearchCriteria.Op.SC, ssc);
|
||||
}
|
||||
if (address != null) {
|
||||
sc.setParameters("hostAddress", address);
|
||||
}
|
||||
if (cluster != null) {
|
||||
sc.setParameters("clusterId", cluster);
|
||||
SearchCriteria<StoragePoolJoinVO> ssc = _poolJoinDao.createSearchCriteria();
|
||||
ssc.addOr("clusterId", Op.EQ, cluster);
|
||||
ssc.addOr("clusterId", Op.NULL);
|
||||
|
||||
sc.addAnd("clusterId", SearchCriteria.Op.SC, ssc);
|
||||
}
|
||||
if (scopeType != null) {
|
||||
sc.setParameters("scope", scopeType.toString());
|
||||
|
|
|
|||
|
|
@ -470,6 +470,7 @@
|
|||
"label.asyncbackup": "Async Backup",
|
||||
"label.author.email": "Author e-mail",
|
||||
"label.author.name": "Author name",
|
||||
"label.auto.assign": "Automatically assign",
|
||||
"label.auto.assign.diskoffering.disk.size": "Automatically assign offering matching the disk size",
|
||||
"label.auto.assign.random.ip": "Automatically assign a random IP address",
|
||||
"label.autoscale": "AutoScale",
|
||||
|
|
@ -550,6 +551,7 @@
|
|||
"label.certificate.upload.failed": "Certificate Upload Failed",
|
||||
"label.certificate.upload.failed.description": "Failed to update SSL Certificate. Failed to pass certificate validation check",
|
||||
"label.certificateid": "Certificate ID",
|
||||
"label.change": "Change",
|
||||
"label.change.affinity": "Change Affinity",
|
||||
"label.change.ip.addess": "Change IP Address",
|
||||
"label.change.ipaddress": "Change IP address for NIC",
|
||||
|
|
@ -818,6 +820,7 @@
|
|||
"label.disksize": "Disk Size (in GB)",
|
||||
"label.disksizeallocated": "Disk Allocated",
|
||||
"label.disksizeallocatedgb": "Allocated",
|
||||
"label.disksizefree": "Disk Free",
|
||||
"label.disksizetotal": "Disk Total",
|
||||
"label.disksizetotalgb": "Total",
|
||||
"label.disksizeunallocatedgb": "Unallocated",
|
||||
|
|
@ -1425,6 +1428,8 @@
|
|||
"label.migrate.instance.to": "Migrate instance to",
|
||||
"label.migrate.instance.to.host": "Migrate instance to another host",
|
||||
"label.migrate.instance.to.ps": "Migrate instance to another primary storage",
|
||||
"label.migrate.instance.single.storage": "Migrate all volume(s) of the instance to a single primary storage",
|
||||
"label.migrate.instance.specific.storages": "Migrate volume(s) of the instance to specific primary storages",
|
||||
"label.migrate.lb.vm": "Migrate LB VM",
|
||||
"label.migrate.lb.vm.to.ps": "Migrate LB VM to another primary storage",
|
||||
"label.migrate.router.to": "Migrate Router to",
|
||||
|
|
@ -1434,6 +1439,7 @@
|
|||
"label.migrate.volume": "Migrate Volume",
|
||||
"label.migrate.volume.newdiskoffering.desc": "This option allows administrators to replace the old disk offering, using one that better suits the new placement of the volume.",
|
||||
"label.migrate.volume.to.primary.storage": "Migrate volume to another primary storage",
|
||||
"label.migrate.with.storage": "Migrate with storage",
|
||||
"label.migrating": "Migrating",
|
||||
"label.migrating.data": "Migrating Data",
|
||||
"label.min.balance": "Min Balance",
|
||||
|
|
@ -1977,6 +1983,7 @@
|
|||
"label.select.offering": "Select offering",
|
||||
"label.select.project": "Select Project",
|
||||
"label.select.projects": "Select Projects",
|
||||
"label.select.ps": "Select Primary Storage",
|
||||
"label.select.region": "Select region",
|
||||
"label.select.tier": "Select Tier",
|
||||
"label.select.vm.for.static.nat": "Select VM for static NAT",
|
||||
|
|
@ -3068,17 +3075,20 @@
|
|||
"message.lock.account": "Please confirm that you want to lock this account. By locking the account, all users for this account will no longer be able to manage their cloud resources. Existing resources can still be accessed.",
|
||||
"message.login.failed": "Login Failed",
|
||||
"message.migrate.instance.confirm": "Please confirm the host you wish to migrate the virtual instance to.",
|
||||
"message.migrate.instance.host.auto.assign": "Host for the instance will be automatically chosen based on the suitability within the same cluster",
|
||||
"message.migrate.instance.select.host": "Please select a host for migration",
|
||||
"message.migrate.instance.to.host": "Please confirm that you want to migrate instance to another host.",
|
||||
"message.migrate.instance.to.ps": "Please confirm that you want to migrate instance to another primary storage.",
|
||||
"message.migrate.instance.to.host": "Please confirm that you want to migrate this instance to another host. When migration is between hosts of different clusters volume(s) of the instance may get migrated to suitable storage pools.",
|
||||
"message.migrate.instance.to.ps": "Please confirm that you want to migrate this instance to another primary storage.",
|
||||
"message.migrate.lb.vm.to.ps": "Please confirm that you want to migrate LB VM to another primary storage.",
|
||||
"message.migrate.router.confirm": "Please confirm the host you wish to migrate the router to:",
|
||||
"message.migrate.router.to.ps": "Please confirm that you want to migrate router to another primary storage.",
|
||||
"message.migrate.system.vm.to.ps": "Please confirm that you want to migrate system VM to another primary storage.",
|
||||
"message.migrate.systemvm.confirm": "Please confirm the host you wish to migrate the system VM to:",
|
||||
"message.migrate.volume": "Please confirm that you want to migrate volume to another primary storage.",
|
||||
"message.migrate.volume": "Please confirm that you want to migrate this volume to another primary storage.",
|
||||
"message.migrate.volume.failed": "Migrating volume failed",
|
||||
"message.migrate.volume.pool.auto.assign": "Primary storage for the volume will be automatically chosen based on the suitability and VM destination",
|
||||
"message.migrate.volume.processing": "Migrating volume...",
|
||||
"message.migrate.with.storage": "Specify storage pool for volumes of the instance.",
|
||||
"message.migrating.failed": "Migration failed",
|
||||
"message.migrating.processing": "Migration in progress for",
|
||||
"message.migrating.vm.to.host.failed": "Failed to migrate VM to host",
|
||||
|
|
@ -3144,6 +3154,7 @@
|
|||
"message.pod.dedicated": "Pod Dedicated",
|
||||
"message.pod.dedication.released": "Pod dedication released",
|
||||
"message.portable.ip.delete.confirm": "Please confirm you want to delete Portable IP Range",
|
||||
"message.primary.storage.invalid.state": "Primary storage is not in Up state",
|
||||
"message.processing.complete": "Processing complete!",
|
||||
"message.project.invite.sent": "Invite sent to user; they will be added to the project once they accept the invitation",
|
||||
"message.protocol.description": "For XenServer, choose NFS, iSCSI, or PreSetup. For KVM, choose NFS, SharedMountPoint, RDB, CLVM or Gluster. For vSphere, choose NFS, PreSetup (VMFS or iSCSI or FiberChannel or vSAN or vVols) or DatastoreCluster. For Hyper-V, choose SMB/CIFS. For LXC, choose NFS or SharedMountPoint. For OVM, choose NFS or ocfs2.",
|
||||
|
|
@ -3419,6 +3430,7 @@
|
|||
"message.volume.state.uploaderror": "Volume upload encountered some error",
|
||||
"message.volume.state.uploadinprogress": "Volume upload is in progress",
|
||||
"message.volume.state.uploadop": "The volume upload operation is in progress or in short the volume is on secondary storage",
|
||||
"message.volume.state.primary.storage.suitability": "The suitability of a primary storage for a volume depends on the disk offering of the volume and on the virtual machine allocations if the volume is attached to a virtual machine.",
|
||||
"message.waiting.for.builtin.templates.to.load": "Waiting for builtin templates to load...",
|
||||
"message.warn.filetype": "jpg, jpeg, png, bmp and svg are the only supported image formats",
|
||||
"message.xstools61plus.update.failed": "Failed to update Original XS Version is 6.1+ field. Error:",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,248 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a-table
|
||||
class="top-spaced"
|
||||
size="small"
|
||||
style="max-height: 250px; overflow-y: auto"
|
||||
:loading="volumesLoading"
|
||||
:columns="volumeColumns"
|
||||
:dataSource="volumes"
|
||||
:pagination="false"
|
||||
:rowKey="record => record.id">
|
||||
<div slot="size" slot-scope="record">
|
||||
<span v-if="record.size">
|
||||
{{ $bytesToHumanReadableSize(record.size) }}
|
||||
</span>
|
||||
</div>
|
||||
<template slot="selectedstorage" slot-scope="record">
|
||||
<span>{{ record.selectedstoragename || '' }}</span>
|
||||
</template>
|
||||
<template slot="select" slot-scope="record">
|
||||
<div style="display: flex; justify-content: flex-end;"><a-button @click="openVolumeStoragePoolSelector(record)">{{ record.selectedstorageid ? $t('label.change') : $t('label.select') }}</a-button></div>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<a-modal
|
||||
:visible="!(!selectedVolumeForStoragePoolSelection.id)"
|
||||
:title="$t('label.select.ps')"
|
||||
:closable="true"
|
||||
:maskClosable="false"
|
||||
:footer="null"
|
||||
:cancelText="$t('label.cancel')"
|
||||
@cancel="closeVolumeStoragePoolSelector()"
|
||||
centered
|
||||
width="auto">
|
||||
<volume-storage-pool-select-form
|
||||
:resource="selectedVolumeForStoragePoolSelection"
|
||||
:clusterId="storagePoolsClusterId"
|
||||
:autoAssignAllowed="storagePoolsClusterId != null"
|
||||
:isOpen="!(!selectedVolumeForStoragePoolSelection.id)"
|
||||
@close-action="closeVolumeStoragePoolSelector()"
|
||||
@select="handleVolumeStoragePoolSelection" />
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from '@/api'
|
||||
import VolumeStoragePoolSelectForm from '@/components/view/VolumeStoragePoolSelectForm'
|
||||
|
||||
export default {
|
||||
name: 'InstanceVolumesStoragePoolSelectListView',
|
||||
components: {
|
||||
VolumeStoragePoolSelectForm
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
clusterId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
volumes: [],
|
||||
volumesLoading: false,
|
||||
volumeColumns: [
|
||||
{
|
||||
title: this.$t('label.volumeid'),
|
||||
dataIndex: 'name'
|
||||
},
|
||||
{
|
||||
title: this.$t('label.type'),
|
||||
dataIndex: 'type'
|
||||
},
|
||||
{
|
||||
title: this.$t('label.size'),
|
||||
scopedSlots: { customRender: 'size' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.storage'),
|
||||
scopedSlots: { customRender: 'selectedstorage' }
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
scopedSlots: { customRender: 'select' }
|
||||
}
|
||||
],
|
||||
selectedVolumeForStoragePoolSelection: {},
|
||||
selectedClusterId: null,
|
||||
volumesWithClusterStoragePool: []
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
this.apiParams = {}
|
||||
if (this.$route.meta.name === 'vm') {
|
||||
this.apiConfig = this.$store.getters.apis.migrateVirtualMachineWithVolume || {}
|
||||
this.apiConfig.params.forEach(param => {
|
||||
this.apiParams[param.name] = param
|
||||
})
|
||||
this.apiConfig = this.$store.getters.apis.migrateVirtualMachine || {}
|
||||
this.apiConfig.params.forEach(param => {
|
||||
if (!(param.name in this.apiParams)) {
|
||||
this.apiParams[param.name] = param
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.apiConfig = this.$store.getters.apis.migrateSystemVm || {}
|
||||
this.apiConfig.params.forEach(param => {
|
||||
if (!(param.name in this.apiParams)) {
|
||||
this.apiParams[param.name] = param
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchVolumes()
|
||||
},
|
||||
computed: {
|
||||
isSelectedVolumeOnlyClusterStoragePoolVolume () {
|
||||
if (this.volumesWithClusterStoragePool.length !== 1) {
|
||||
return false
|
||||
}
|
||||
for (const volume of this.volumesWithClusterStoragePool) {
|
||||
if (volume.id === this.selectedVolumeForStoragePoolSelection.id) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
storagePoolsClusterId () {
|
||||
if (this.clusterId) {
|
||||
return this.clusterId
|
||||
}
|
||||
return this.isSelectedVolumeOnlyClusterStoragePoolVolume ? null : this.selectedClusterId
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchVolumes () {
|
||||
this.volumesLoading = true
|
||||
this.volumes = []
|
||||
api('listVolumes', {
|
||||
listAll: true,
|
||||
virtualmachineid: this.resource.id
|
||||
}).then(response => {
|
||||
var volumes = response.listvolumesresponse.volume
|
||||
if (volumes && volumes.length > 0) {
|
||||
volumes.sort((a, b) => {
|
||||
return b.type.localeCompare(a.type)
|
||||
})
|
||||
this.volumes = volumes
|
||||
}
|
||||
}).finally(() => {
|
||||
this.resetSelection()
|
||||
this.volumesLoading = false
|
||||
})
|
||||
},
|
||||
resetSelection () {
|
||||
var volumes = this.volumes
|
||||
this.volumes = []
|
||||
for (var volume of volumes) {
|
||||
if (this.clusterId) {
|
||||
volume.selectedstorageid = -1
|
||||
volume.selectedstoragename = this.$t('label.auto.assign')
|
||||
} else {
|
||||
volume.selectedstorageid = null
|
||||
volume.selectedstoragename = ''
|
||||
}
|
||||
delete volume.selectedstorageclusterid
|
||||
}
|
||||
this.volumes = volumes
|
||||
this.updateVolumeToStoragePoolSelection()
|
||||
},
|
||||
openVolumeStoragePoolSelector (volume) {
|
||||
this.selectedVolumeForStoragePoolSelection = volume
|
||||
},
|
||||
closeVolumeStoragePoolSelector () {
|
||||
this.selectedVolumeForStoragePoolSelection = {}
|
||||
},
|
||||
handleVolumeStoragePoolSelection (volumeId, storagePool) {
|
||||
for (const volume of this.volumes) {
|
||||
if (volume.id === volumeId) {
|
||||
volume.selectedstorageid = storagePool.id
|
||||
volume.selectedstoragename = storagePool.name
|
||||
volume.selectedstorageclusterid = storagePool.clusterid
|
||||
break
|
||||
}
|
||||
}
|
||||
this.updateVolumeToStoragePoolSelection()
|
||||
},
|
||||
updateVolumeToStoragePoolSelection () {
|
||||
var clusterId = null
|
||||
this.volumeToPoolSelection = []
|
||||
this.volumesWithClusterStoragePool = []
|
||||
for (const volume of this.volumes) {
|
||||
if (volume.selectedstorageid && volume.selectedstorageid !== -1) {
|
||||
this.volumeToPoolSelection.push({ volume: volume.id, pool: volume.selectedstorageid })
|
||||
}
|
||||
if (!this.clusterId && volume.selectedstorageclusterid) {
|
||||
clusterId = volume.selectedstorageclusterid
|
||||
this.volumesWithClusterStoragePool.push(volume)
|
||||
}
|
||||
}
|
||||
if (!this.clusterId) {
|
||||
this.selectedClusterId = clusterId
|
||||
for (const volume of this.volumes) {
|
||||
if (this.selectedClusterId == null && volume.selectedstorageid === -1) {
|
||||
volume.selectedstorageid = null
|
||||
volume.selectedstoragename = ''
|
||||
}
|
||||
if (this.selectedClusterId && volume.selectedstorageid == null) {
|
||||
volume.selectedstorageid = -1
|
||||
volume.selectedstoragename = this.$t('label.auto.assign')
|
||||
}
|
||||
}
|
||||
}
|
||||
this.$emit('select', this.volumeToPoolSelection)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.top-spaced {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a-input-search
|
||||
class="top-spaced"
|
||||
:placeholder="$t('label.search')"
|
||||
v-model="searchQuery"
|
||||
style="margin-bottom: 10px;"
|
||||
@search="fetchStoragePools"
|
||||
autoFocus />
|
||||
<a-table
|
||||
size="small"
|
||||
style="overflow-y: auto"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:dataSource="storagePools"
|
||||
:pagination="false"
|
||||
:rowKey="record => record.id">
|
||||
<span slot="suitabilityCustomTitle">
|
||||
{{ $t('label.suitability') }}
|
||||
<a-tooltip :title="$t('message.volume.state.primary.storage.suitability')" placement="top">
|
||||
<a-icon type="info-circle" class="table-tooltip-icon" />
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<div slot="name" slot-scope="record">
|
||||
{{ record.name }}
|
||||
<a-tooltip v-if="record.name === $t('label.auto.assign')" :title="$t('message.migrate.volume.pool.auto.assign')" placement="top">
|
||||
<a-icon type="info-circle" class="table-tooltip-icon" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div slot="suitability" slot-scope="record">
|
||||
<a-icon
|
||||
class="host-item__suitability-icon"
|
||||
type="check-circle"
|
||||
theme="twoTone"
|
||||
twoToneColor="#52c41a"
|
||||
v-if="record.suitableformigration" />
|
||||
<a-icon
|
||||
class="host-item__suitability-icon"
|
||||
type="close-circle"
|
||||
theme="twoTone"
|
||||
twoToneColor="#f5222d"
|
||||
v-else />
|
||||
</div>
|
||||
<div slot="disksizetotal" slot-scope="record">
|
||||
<span v-if="record.disksizetotal">{{ $bytesToHumanReadableSize(record.disksizetotal) }}</span>
|
||||
</div>
|
||||
<div slot="disksizeused" slot-scope="record">
|
||||
<span v-if="record.disksizeused">{{ $bytesToHumanReadableSize(record.disksizeused) }}</span>
|
||||
</div>
|
||||
<div slot="disksizefree" slot-scope="record">
|
||||
<span v-if="record.disksizetotal && record.disksizeused">{{ $bytesToHumanReadableSize(record.disksizetotal * 1 - record.disksizeused * 1) }}</span>
|
||||
</div>
|
||||
<template slot="select" slot-scope="record">
|
||||
<a-tooltip placement="top" :title="record.state !== 'Up' ? $t('message.primary.storage.invalid.state') : ''">
|
||||
<a-radio
|
||||
:disabled="record.id !== -1 && record.state !== 'Up'"
|
||||
@click="updateSelection(record)"
|
||||
:checked="selectedStoragePool != null && record.id === selectedStoragePool.id">
|
||||
</a-radio>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-pagination
|
||||
class="top-spaced"
|
||||
size="small"
|
||||
:current="page"
|
||||
:pageSize="pageSize"
|
||||
:total="totalCount"
|
||||
:showTotal="total => `${$t('label.total')} ${total} ${$t('label.items')}`"
|
||||
:pageSizeOptions="['10', '20', '40', '80', '100']"
|
||||
@change="handleChangePage"
|
||||
@showSizeChange="handleChangePageSize"
|
||||
showSizeChanger>
|
||||
<template slot="buildOptionText" slot-scope="props">
|
||||
<span>{{ props.value }} / {{ $t('label.page') }}</span>
|
||||
</template>
|
||||
</a-pagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from '@/api'
|
||||
|
||||
export default {
|
||||
name: 'VolumeStoragePoolSelector',
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
clusterId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
suitabilityEnabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
autoAssignAllowed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
storagePools: [],
|
||||
searchQuery: '',
|
||||
totalCount: 0,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
selectedStoragePool: null,
|
||||
columns: [
|
||||
{
|
||||
title: this.$t('label.storageid'),
|
||||
scopedSlots: { customRender: 'name' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.clusterid'),
|
||||
dataIndex: 'clustername'
|
||||
},
|
||||
{
|
||||
title: this.$t('label.podid'),
|
||||
dataIndex: 'podname'
|
||||
},
|
||||
{
|
||||
title: this.$t('label.disksizetotal'),
|
||||
scopedSlots: { customRender: 'disksizetotal' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.disksizeused'),
|
||||
scopedSlots: { customRender: 'disksizeused' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.disksizefree'),
|
||||
scopedSlots: { customRender: 'disksizefree' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.select'),
|
||||
scopedSlots: { customRender: 'select' }
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.suitabilityEnabled) {
|
||||
this.columns.splice(1, 0, { slots: { title: 'suitabilityCustomTitle' }, scopedSlots: { customRender: 'suitability' } }
|
||||
)
|
||||
}
|
||||
this.preselectStoragePool()
|
||||
this.fetchStoragePools()
|
||||
},
|
||||
watch: {
|
||||
searchQuery (newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
this.page = 1
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchStoragePools () {
|
||||
this.loading = true
|
||||
if (this.suitabilityEnabled) {
|
||||
api('findStoragePoolsForMigration', {
|
||||
id: this.resource.id,
|
||||
keyword: this.searchQuery,
|
||||
page: this.page,
|
||||
pagesize: this.pageSize
|
||||
}).then(response => {
|
||||
this.storagePools = response.findstoragepoolsformigrationresponse.storagepool || []
|
||||
this.totalCount = response.findstoragepoolsformigrationresponse.count
|
||||
}).catch(error => {
|
||||
this.$notifyError(error)
|
||||
}).finally(() => {
|
||||
this.handleStoragePoolsFetchComplete()
|
||||
})
|
||||
} else {
|
||||
var params = {
|
||||
zoneid: this.resource.zoneid,
|
||||
keyword: this.searchQuery,
|
||||
page: this.page,
|
||||
pagesize: this.pageSize
|
||||
}
|
||||
if (this.clusterId) {
|
||||
params.clusterid = this.clusterId
|
||||
}
|
||||
api('listStoragePools', params).then(response => {
|
||||
this.storagePools = response.liststoragepoolsresponse.storagepool || []
|
||||
this.totalCount = response.liststoragepoolsresponse.count
|
||||
}).catch(error => {
|
||||
this.$notifyError(error)
|
||||
}).finally(() => {
|
||||
this.handleStoragePoolsFetchComplete()
|
||||
})
|
||||
}
|
||||
},
|
||||
handleStoragePoolsFetchComplete () {
|
||||
this.$emit('storagePoolsUpdated', this.storagePools)
|
||||
this.addAutoAssignOption()
|
||||
this.loading = false
|
||||
},
|
||||
addAutoAssignOption () {
|
||||
if (this.autoAssignAllowed && this.page === 1) {
|
||||
this.storagePools.unshift({ id: -1, name: this.$t('label.auto.assign'), clustername: '', podname: '' })
|
||||
}
|
||||
},
|
||||
handleChangePage (page, pageSize) {
|
||||
this.page = page
|
||||
this.pageSize = pageSize
|
||||
this.fetchStoragePools()
|
||||
},
|
||||
handleChangePageSize (currentPage, pageSize) {
|
||||
this.page = currentPage
|
||||
this.pageSize = pageSize
|
||||
this.fetchStoragePools()
|
||||
},
|
||||
preselectStoragePool () {
|
||||
if (this.resource && 'selectedstorageid' in this.resource) {
|
||||
this.selectedStoragePool = { id: this.resource.selectedstorageid }
|
||||
}
|
||||
},
|
||||
clearView () {
|
||||
this.storagePools = []
|
||||
this.searchQuery = ''
|
||||
this.totalCount = 0
|
||||
this.page = 1
|
||||
this.pageSize = 10
|
||||
this.selectedStoragePool = null
|
||||
},
|
||||
reset () {
|
||||
this.clearView()
|
||||
this.preselectStoragePool()
|
||||
this.fetchStoragePools()
|
||||
},
|
||||
updateSelection (storagePool) {
|
||||
this.selectedStoragePool = storagePool
|
||||
this.$emit('select', this.selectedStoragePool)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.top-spaced {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.table-tooltip-icon {
|
||||
color: rgba(0,0,0,.45);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
<template>
|
||||
<div class="form" v-ctrl-enter="handleKeyboardSubmit">
|
||||
<storage-pool-select-view
|
||||
ref="selectionView"
|
||||
:resource="resource"
|
||||
:clusterId="clusterId"
|
||||
:suitabilityEnabled="suitabilityEnabled"
|
||||
:autoAssignAllowed="autoAssignAllowed"
|
||||
@select="handleSelect" />
|
||||
|
||||
<a-divider />
|
||||
|
||||
<div class="actions">
|
||||
<a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
|
||||
<a-button type="primary" ref="submit" :disabled="!selectedStoragePool" @click="submitForm">{{ $t('label.ok') }}</a-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StoragePoolSelectView from '@/components/view/StoragePoolSelectView'
|
||||
|
||||
export default {
|
||||
name: 'VolumeStoragePoolSelectionForm',
|
||||
components: {
|
||||
StoragePoolSelectView
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
clusterId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
suitabilityEnabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
autoAssignAllowed: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
selectedStoragePool: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isOpen (newValue) {
|
||||
if (newValue) {
|
||||
setTimeout(() => {
|
||||
this.$refs.selectionView.reset()
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSelect (storagePool) {
|
||||
this.selectedStoragePool = storagePool
|
||||
},
|
||||
closeModal () {
|
||||
this.$emit('close-action')
|
||||
},
|
||||
handleKeyboardSubmit () {
|
||||
if (this.selectedStoragePool != null) {
|
||||
this.submitForm()
|
||||
}
|
||||
},
|
||||
submitForm () {
|
||||
this.$emit('select', this.resource.id, this.selectedStoragePool)
|
||||
this.closeModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form {
|
||||
width: 80vw;
|
||||
|
||||
@media (min-width: 900px) {
|
||||
width: 850px;
|
||||
}
|
||||
}
|
||||
|
||||
.top-spaced {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
|
||||
button {
|
||||
&:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -26,7 +26,7 @@ import './core/lazy_use'
|
|||
import './core/ext'
|
||||
import './permission' // permission control
|
||||
import './utils/filter' // global filter
|
||||
import { pollJobPlugin, notifierPlugin, toLocaleDatePlugin, configUtilPlugin, apiMetaUtilPlugin, showIconPlugin, resourceTypePlugin } from './utils/plugins'
|
||||
import { pollJobPlugin, notifierPlugin, toLocaleDatePlugin, configUtilPlugin, apiMetaUtilPlugin, showIconPlugin, resourceTypePlugin, fileSizeUtilPlugin } from './utils/plugins'
|
||||
import { VueAxios } from './utils/request'
|
||||
import './utils/directives'
|
||||
|
||||
|
|
@ -60,3 +60,4 @@ fetch('config.json').then(response => response.json()).then(config => {
|
|||
|
||||
Vue.use(configUtilPlugin)
|
||||
Vue.use(apiMetaUtilPlugin)
|
||||
Vue.use(fileSizeUtilPlugin)
|
||||
|
|
|
|||
|
|
@ -299,3 +299,30 @@ export const apiMetaUtilPlugin = {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const KB = 1024
|
||||
const MB = 1024 * KB
|
||||
const GB = 1024 * MB
|
||||
const TB = 1024 * GB
|
||||
|
||||
export const fileSizeUtilPlugin = {
|
||||
install (Vue) {
|
||||
Vue.prototype.$bytesToHumanReadableSize = function (bytes) {
|
||||
if (bytes == null) {
|
||||
return ''
|
||||
}
|
||||
if (bytes < KB && bytes >= 0) {
|
||||
return bytes + ' bytes'
|
||||
}
|
||||
if (bytes < MB) {
|
||||
return (bytes / KB).toFixed(2) + ' KB'
|
||||
} else if (bytes < GB) {
|
||||
return (bytes / MB).toFixed(2) + ' MB'
|
||||
} else if (bytes < TB) {
|
||||
return (bytes / GB).toFixed(2) + ' GB'
|
||||
} else {
|
||||
return (bytes / TB).toFixed(2) + ' TB'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,47 +16,53 @@
|
|||
// under the License.
|
||||
|
||||
<template>
|
||||
<div class="form-layout">
|
||||
<a-spin :spinning="loading">
|
||||
<a-form
|
||||
:form="form"
|
||||
@submit="handleSubmit"
|
||||
layout="vertical">
|
||||
<a-form-item>
|
||||
<tooltip-label slot="label" :title="$t('label.storageid')" :tooltip="apiParams.storageid ? apiParams.storageid.description : ''"/>
|
||||
<a-select
|
||||
:loading="loading"
|
||||
v-decorator="['storageid', {
|
||||
rules: [{ required: true, message: `${this.$t('message.error.required.input')}` }]
|
||||
}]"
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
:filterOption="(input, option) => {
|
||||
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}" >
|
||||
<a-select-option v-for="storagePool in storagePools" :key="storagePool.id">
|
||||
{{ storagePool.name || storagePool.id }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<div class="form-layout" v-ctrl-enter="handleKeyboardSubmit">
|
||||
<a-alert type="warning">
|
||||
<span slot="message" v-html="$t('message.migrate.instance.to.ps')" />
|
||||
</a-alert>
|
||||
<a-radio-group
|
||||
v-if="migrateVmWithVolumeAllowed"
|
||||
:defaultValue="migrateMode"
|
||||
@change="e => { handleMigrateModeChange(e.target.value) }">
|
||||
<a-radio class="radio-style" :value="1">
|
||||
{{ $t('label.migrate.instance.single.storage') }}
|
||||
</a-radio>
|
||||
<a-radio class="radio-style" :value="2">
|
||||
{{ $t('label.migrate.instance.specific.storages') }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
<div v-if="migrateMode == 1">
|
||||
<storage-pool-select-view
|
||||
ref="storagePoolSelection"
|
||||
:resource="resource"
|
||||
@select="handleStoragePoolChange" />
|
||||
</div>
|
||||
<instance-volumes-storage-pool-select-list-view
|
||||
v-else
|
||||
:resource="resource"
|
||||
@select="handleVolumeToPoolChange" />
|
||||
|
||||
<div :span="24" class="action-button">
|
||||
<a-button @click="closeAction">{{ this.$t('label.cancel') }}</a-button>
|
||||
<a-button :loading="loading" type="primary" @click="handleSubmit">{{ this.$t('label.ok') }}</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
<a-divider />
|
||||
|
||||
<div class="actions">
|
||||
<a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
|
||||
<a-button type="primary" :disabled="!formSubmitAllowed" @click="submitForm">{{ $t('label.ok') }}</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { api } from '@/api'
|
||||
import TooltipLabel from '@/components/widgets/TooltipLabel'
|
||||
import StoragePoolSelectView from '@/components/view/StoragePoolSelectView'
|
||||
import InstanceVolumesStoragePoolSelectListView from '@/components/view/InstanceVolumesStoragePoolSelectListView'
|
||||
|
||||
export default {
|
||||
name: 'MigrateVMStorage',
|
||||
components: {
|
||||
TooltipLabel
|
||||
TooltipLabel,
|
||||
StoragePoolSelectView,
|
||||
InstanceVolumesStoragePoolSelectListView
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
|
|
@ -66,50 +72,44 @@ export default {
|
|||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
storagePools: []
|
||||
migrateMode: 1,
|
||||
selectedPool: {},
|
||||
volumeToPoolSelection: []
|
||||
}
|
||||
},
|
||||
beforeCreate () {
|
||||
this.form = this.$form.createForm(this)
|
||||
this.apiParams = {}
|
||||
if (this.$route.meta.name === 'vm') {
|
||||
this.apiConfig = this.$store.getters.apis.migrateVirtualMachineWithVolume || {}
|
||||
this.apiConfig.params.forEach(param => {
|
||||
this.apiParams[param.name] = param
|
||||
})
|
||||
this.apiConfig = this.$store.getters.apis.migrateVirtualMachine || {}
|
||||
this.apiConfig.params.forEach(param => {
|
||||
if (!(param.name in this.apiParams)) {
|
||||
this.apiParams[param.name] = param
|
||||
this.migrateVmWithVolumeApiParams = this.$getApiParams('migrateVirtualMachineWithVolume')
|
||||
},
|
||||
computed: {
|
||||
migrateVmWithVolumeAllowed () {
|
||||
return this.$route.meta.name === 'vm' && this.migrateVmWithVolumeApiParams.hostid && this.migrateVmWithVolumeApiParams.hostid.required === false
|
||||
},
|
||||
formSubmitAllowed () {
|
||||
return this.migrateMode === 2 ? this.volumeToPoolSelection.length > 0 : this.selectedPool.id
|
||||
},
|
||||
isSelectedVolumeOnlyClusterStoragePoolVolume () {
|
||||
if (this.volumesWithClusterStoragePool.length !== 1) {
|
||||
return false
|
||||
}
|
||||
for (const volume of this.volumesWithClusterStoragePool) {
|
||||
if (volume.id === this.selectedVolumeForStoragePoolSelection.id) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.apiConfig = this.$store.getters.apis.migrateSystemVm || {}
|
||||
this.apiConfig.params.forEach(param => {
|
||||
if (!(param.name in this.apiParams)) {
|
||||
this.apiParams[param.name] = param
|
||||
}
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
},
|
||||
mounted () {
|
||||
this.fetchData()
|
||||
},
|
||||
methods: {
|
||||
fetchData () {
|
||||
this.loading = true
|
||||
api('listStoragePools', {
|
||||
zoneid: this.resource.zoneid
|
||||
}).then(response => {
|
||||
if (this.arrayHasItems(response.liststoragepoolsresponse.storagepool)) {
|
||||
this.storagePools = response.liststoragepoolsresponse.storagepool
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
if (this.migrateMode === 2) {
|
||||
this.fetchVolumes()
|
||||
}
|
||||
},
|
||||
handleMigrateModeChange (value) {
|
||||
this.migrateMode = value
|
||||
this.selectedPool = {}
|
||||
this.volumeToPoolSelection = []
|
||||
},
|
||||
isValidValueForKey (obj, key) {
|
||||
return key in obj && obj[key] != null
|
||||
|
|
@ -120,89 +120,70 @@ export default {
|
|||
isObjectEmpty (obj) {
|
||||
return !(obj !== null && obj !== undefined && Object.keys(obj).length > 0 && obj.constructor === Object)
|
||||
},
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
this.form.validateFieldsAndScroll((err, values) => {
|
||||
if (err) {
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
var isUserVm = true
|
||||
if (this.$route.meta.name !== 'vm') {
|
||||
isUserVm = false
|
||||
}
|
||||
var migrateApi = isUserVm ? 'migrateVirtualMachine' : 'migrateSystemVm'
|
||||
if (isUserVm && this.apiParams.hostid && this.apiParams.hostid.required === false) {
|
||||
migrateApi = 'migrateVirtualMachineWithVolume'
|
||||
var rootVolume = null
|
||||
api('listVolumes', {
|
||||
listAll: true,
|
||||
virtualmachineid: this.resource.id
|
||||
}).then(response => {
|
||||
var volumes = response.listvolumesresponse.volume
|
||||
if (volumes && volumes.length > 0) {
|
||||
volumes = volumes.filter(item => item.type === 'ROOT')
|
||||
if (volumes && volumes.length > 0) {
|
||||
rootVolume = volumes[0]
|
||||
}
|
||||
if (rootVolume == null) {
|
||||
this.$message.error('Failed to find ROOT volume for the VM ' + this.resource.id)
|
||||
this.closeAction()
|
||||
}
|
||||
this.migrateVm(migrateApi, values.storageid, rootVolume.id)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
this.migrateVm(migrateApi, values.storageid, null)
|
||||
})
|
||||
handleStoragePoolChange (storagePool) {
|
||||
this.selectedPool = storagePool
|
||||
},
|
||||
migrateVm (migrateApi, storageId, rootVolumeId) {
|
||||
var params = {
|
||||
virtualmachineid: this.resource.id,
|
||||
storageid: storageId
|
||||
handleVolumeToPoolChange (volumeToPool) {
|
||||
this.volumeToPoolSelection = volumeToPool
|
||||
},
|
||||
handleKeyboardSubmit () {
|
||||
if (this.formSubmitAllowed) {
|
||||
this.submitForm()
|
||||
}
|
||||
if (rootVolumeId !== null) {
|
||||
params = {
|
||||
virtualmachineid: this.resource.id,
|
||||
'migrateto[0].volume': rootVolumeId,
|
||||
'migrateto[0].pool': storageId
|
||||
},
|
||||
submitForm () {
|
||||
var isUserVm = true
|
||||
if (this.$route.meta.name !== 'vm') {
|
||||
isUserVm = false
|
||||
}
|
||||
var migrateApi = isUserVm ? 'migrateVirtualMachine' : 'migrateSystemVm'
|
||||
if (isUserVm && this.migrateMode === 2) {
|
||||
migrateApi = 'migrateVirtualMachineWithVolume'
|
||||
this.migrateVm(migrateApi, null, this.volumeToPoolSelection)
|
||||
return
|
||||
}
|
||||
this.migrateVm(migrateApi, this.selectedPool.id, null)
|
||||
},
|
||||
migrateVm (migrateApi, storageId, volumeToPool) {
|
||||
var params = {
|
||||
virtualmachineid: this.resource.id
|
||||
}
|
||||
if (this.migrateMode === 2) {
|
||||
for (var i = 0; i < volumeToPool.length; i++) {
|
||||
const mapping = volumeToPool[i]
|
||||
params['migrateto[' + i + '].volume'] = mapping.volume
|
||||
params['migrateto[' + i + '].pool'] = mapping.pool
|
||||
}
|
||||
} else {
|
||||
params.storageid = storageId
|
||||
}
|
||||
api(migrateApi, params).then(response => {
|
||||
var jobId = ''
|
||||
if (migrateApi === 'migrateVirtualMachineWithVolume') {
|
||||
jobId = response.migratevirtualmachinewithvolumeresponse.jobid
|
||||
} else if (migrateApi === 'migrateSystemVm') {
|
||||
jobId = response.migratesystemvmresponse.jobid
|
||||
} else {
|
||||
jobId = response.migratevirtualmachineresponse.jobid
|
||||
}
|
||||
const jobId = response[migrateApi.toLowerCase() + 'response'].jobid
|
||||
this.$pollJob({
|
||||
title: `${this.$t('label.migrating')} ${this.resource.name}`,
|
||||
description: this.resource.name,
|
||||
jobId: jobId,
|
||||
successMessage: `${this.$t('message.success.migrating')} ${this.resource.name}`,
|
||||
successMethod: () => {
|
||||
this.$parent.$parent.close()
|
||||
this.closeModal()
|
||||
},
|
||||
errorMessage: this.$t('message.migrating.failed'),
|
||||
errorMethod: () => {
|
||||
this.$parent.$parent.close()
|
||||
this.closeModal()
|
||||
},
|
||||
loadingMessage: `${this.$t('message.migrating.processing')} ${this.resource.name}`,
|
||||
catchMessage: this.$t('error.fetching.async.job.result'),
|
||||
catchMethod: () => {
|
||||
this.$parent.$parent.close()
|
||||
this.closeModal()
|
||||
}
|
||||
})
|
||||
this.$parent.$parent.close()
|
||||
this.closeModal()
|
||||
}).catch(error => {
|
||||
console.error(error)
|
||||
this.$message.error(`${this.$t('message.migrating.vm.to.storage.failed')} ${storageId}`)
|
||||
})
|
||||
},
|
||||
closeAction () {
|
||||
closeModal () {
|
||||
this.$emit('close-action')
|
||||
}
|
||||
}
|
||||
|
|
@ -211,18 +192,33 @@ export default {
|
|||
|
||||
<style scoped lang="less">
|
||||
.form-layout {
|
||||
width: 60vw;
|
||||
width: 80vw;
|
||||
|
||||
@media (min-width: 500px) {
|
||||
width: 450px;
|
||||
@media (min-width: 900px) {
|
||||
width: 850px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-button {
|
||||
text-align: right;
|
||||
.top-spaced {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.radio-style {
|
||||
display: block;
|
||||
margin-left: 10px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
|
||||
button {
|
||||
margin-right: 5px;
|
||||
&:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -16,14 +16,18 @@
|
|||
// under the License.
|
||||
|
||||
<template>
|
||||
<div class="form" v-ctrl-enter="submitForm">
|
||||
<div class="form" v-ctrl-enter="handleKeyboardSubmit">
|
||||
<a-alert type="warning">
|
||||
<span slot="message" v-html="$t('message.migrate.instance.to.host')" />
|
||||
</a-alert>
|
||||
<a-input-search
|
||||
class="top-spaced"
|
||||
:placeholder="$t('label.search')"
|
||||
v-model="searchQuery"
|
||||
style="margin-bottom: 10px;"
|
||||
@search="fetchData"
|
||||
autoFocus />
|
||||
<a-table
|
||||
class="top-spaced"
|
||||
size="small"
|
||||
style="overflow-y: auto"
|
||||
:loading="loading"
|
||||
|
|
@ -31,6 +35,12 @@
|
|||
:dataSource="hosts"
|
||||
:pagination="false"
|
||||
:rowKey="record => record.id">
|
||||
<div slot="name" slot-scope="record">
|
||||
{{ record.name }}
|
||||
<a-tooltip v-if="record.name === $t('label.auto.assign')" :title="$t('message.migrate.instance.host.auto.assign')" placement="top">
|
||||
<a-icon type="info-circle" class="table-tooltip-icon" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div slot="suitability" slot-scope="record">
|
||||
<a-icon
|
||||
class="host-item__suitability-icon"
|
||||
|
|
@ -47,7 +57,7 @@
|
|||
</div>
|
||||
<div slot="memused" slot-scope="record">
|
||||
<span v-if="record.memoryused">
|
||||
{{ record.memoryused | byteToGigabyte }} GB
|
||||
{{ $bytesToHumanReadableSize(record.memoryused) }}
|
||||
</span>
|
||||
</div>
|
||||
<div slot="memoryallocatedpercentage" slot-scope="record">
|
||||
|
|
@ -65,7 +75,7 @@
|
|||
<template slot="select" slot-scope="record">
|
||||
<a-radio
|
||||
class="host-item__radio"
|
||||
@click="selectedHost = record"
|
||||
@click="handleSelectedHostChange(record)"
|
||||
:checked="record.id === selectedHost.id"
|
||||
:disabled="!record.suitableformigration"></a-radio>
|
||||
</template>
|
||||
|
|
@ -86,10 +96,27 @@
|
|||
</template>
|
||||
</a-pagination>
|
||||
|
||||
<div style="margin-top: 20px; display: flex; justify-content:flex-end;">
|
||||
<a-button type="primary" ref="submit" :disabled="!selectedHost.id" @click="submitForm">
|
||||
{{ $t('label.ok') }}
|
||||
</a-button>
|
||||
<a-form-item
|
||||
v-if="isUserVm"
|
||||
class="top-spaced">
|
||||
<tooltip-label slot="label" :title="$t('label.migrate.with.storage')" :tooltip="$t('message.migrate.with.storage')"/>
|
||||
<a-switch
|
||||
v-model="migrateWithStorage"
|
||||
:disabled="!selectedHost || !selectedHost.id || selectedHost.id === -1" />
|
||||
</a-form-item>
|
||||
<instance-volumes-storage-pool-select-list-view
|
||||
ref="volumeToPoolSelect"
|
||||
v-if="migrateWithStorage"
|
||||
class="top-spaced"
|
||||
:resource="resource"
|
||||
:clusterId="selectedHost.id ? selectedHost.clusterid : null"
|
||||
@select="handleVolumeToPoolChange" />
|
||||
|
||||
<a-divider />
|
||||
|
||||
<div class="actions">
|
||||
<a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
|
||||
<a-button type="primary" ref="submit" :disabled="!selectedHost.id" @click="submitForm">{{ $t('label.ok') }}</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -97,9 +124,15 @@
|
|||
|
||||
<script>
|
||||
import { api } from '@/api'
|
||||
import TooltipLabel from '@/components/widgets/TooltipLabel'
|
||||
import InstanceVolumesStoragePoolSelectListView from '@/components/view/InstanceVolumesStoragePoolSelectListView'
|
||||
|
||||
export default {
|
||||
name: 'VMMigrateWizard',
|
||||
components: {
|
||||
TooltipLabel,
|
||||
InstanceVolumesStoragePoolSelectListView
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
|
|
@ -117,8 +150,8 @@ export default {
|
|||
pageSize: 10,
|
||||
columns: [
|
||||
{
|
||||
title: this.$t('label.name'),
|
||||
dataIndex: 'name'
|
||||
title: this.$t('label.hostid'),
|
||||
scopedSlots: { customRender: 'name' }
|
||||
},
|
||||
{
|
||||
title: this.$t('label.suitability'),
|
||||
|
|
@ -152,13 +185,30 @@ export default {
|
|||
title: this.$t('label.select'),
|
||||
scopedSlots: { customRender: 'select' }
|
||||
}
|
||||
]
|
||||
],
|
||||
migrateWithStorage: false,
|
||||
volumeToPoolSelection: []
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchData()
|
||||
},
|
||||
computed: {
|
||||
isUserVm () {
|
||||
return this.$route.meta.name === 'vm'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
searchQuery (newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
this.page = 1
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
arrayHasItems (array) {
|
||||
return array !== null && array !== undefined && Array.isArray(array) && array.length > 0
|
||||
},
|
||||
fetchData () {
|
||||
this.loading = true
|
||||
api('findHostsForMigration', {
|
||||
|
|
@ -173,7 +223,7 @@ export default {
|
|||
})
|
||||
for (const key in this.hosts) {
|
||||
if (this.hosts[key].suitableformigration && !this.hosts[key].requiresstoragemigration) {
|
||||
this.hosts.unshift({ id: -1, name: this.$t('label.migrate.auto.select'), suitableformigration: true, requiresstoragemigration: false })
|
||||
this.hosts.unshift({ id: -1, name: this.$t('label.auto.assign'), suitableformigration: true, requiresstoragemigration: false })
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -184,51 +234,6 @@ export default {
|
|||
this.loading = false
|
||||
})
|
||||
},
|
||||
submitForm () {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
var isUserVm = true
|
||||
if (this.$route.meta.name !== 'vm') {
|
||||
isUserVm = false
|
||||
}
|
||||
var migrateApi = isUserVm
|
||||
? this.selectedHost.requiresStorageMotion ? 'migrateVirtualMachineWithVolume' : 'migrateVirtualMachine'
|
||||
: 'migrateSystemVm'
|
||||
var migrateParams = this.selectedHost.id === -1 ? { autoselect: true, virtualmachineid: this.resource.id }
|
||||
: { hostid: this.selectedHost.id, virtualmachineid: this.resource.id }
|
||||
api(migrateApi, migrateParams).then(response => {
|
||||
const jobid = isUserVm
|
||||
? this.selectedHost.requiresStorageMotion ? response.migratevirtualmachinewithvolumeresponse.jobid : response.migratevirtualmachineresponse.jobid
|
||||
: response.migratesystemvmresponse.jobid
|
||||
this.$pollJob({
|
||||
jobId: jobid,
|
||||
title: `${this.$t('label.migrating')} ${this.resource.name}`,
|
||||
description: this.resource.name,
|
||||
successMessage: `${this.$t('message.success.migrating')} ${this.resource.name}`,
|
||||
successMethod: () => {
|
||||
this.$emit('close-action')
|
||||
},
|
||||
errorMessage: this.$t('message.migrating.failed'),
|
||||
errorMethod: () => {
|
||||
this.$emit('close-action')
|
||||
},
|
||||
loadingMessage: `${this.$t('message.migrating.processing')} ${this.resource.name}`,
|
||||
catchMessage: this.$t('error.fetching.async.job.result'),
|
||||
catchMethod: () => {
|
||||
this.$emit('close-action')
|
||||
}
|
||||
})
|
||||
this.$emit('close-action')
|
||||
}).catch(error => {
|
||||
this.$notification.error({
|
||||
message: this.$t('message.request.failed'),
|
||||
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message,
|
||||
duration: 0
|
||||
})
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleChangePage (page, pageSize) {
|
||||
this.page = page
|
||||
this.pageSize = pageSize
|
||||
|
|
@ -238,11 +243,77 @@ export default {
|
|||
this.page = currentPage
|
||||
this.pageSize = pageSize
|
||||
this.fetchData()
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
byteToGigabyte: value => {
|
||||
return (value / Math.pow(10, 9)).toFixed(2)
|
||||
},
|
||||
handleSelectedHostChange (host) {
|
||||
if (host.id === -1) {
|
||||
this.migrateWithStorage = false
|
||||
}
|
||||
this.selectedHost = host
|
||||
this.selectedVolumeForStoragePoolSelection = {}
|
||||
this.volumeToPoolSelection = []
|
||||
if (this.migrateWithStorage) {
|
||||
this.$refs.volumeToPoolSelect.resetSelection()
|
||||
}
|
||||
},
|
||||
handleVolumeToPoolChange (volumeToPool) {
|
||||
this.volumeToPoolSelection = volumeToPool
|
||||
},
|
||||
handleKeyboardSubmit () {
|
||||
if (this.selectedHost.id) {
|
||||
this.submitForm()
|
||||
}
|
||||
},
|
||||
closeModal () {
|
||||
this.$emit('close-action')
|
||||
},
|
||||
submitForm () {
|
||||
if (this.loading) return
|
||||
this.loading = true
|
||||
const migrateApi = this.isUserVm
|
||||
? (this.selectedHost.requiresStorageMotion || this.volumeToPoolSelection.length > 0)
|
||||
? 'migrateVirtualMachineWithVolume'
|
||||
: 'migrateVirtualMachine'
|
||||
: 'migrateSystemVm'
|
||||
var params = this.selectedHost.id === -1
|
||||
? { autoselect: true, virtualmachineid: this.resource.id }
|
||||
: { hostid: this.selectedHost.id, virtualmachineid: this.resource.id }
|
||||
if (this.migrateWithStorage) {
|
||||
for (var i = 0; i < this.volumeToPoolSelection.length; i++) {
|
||||
const mapping = this.volumeToPoolSelection[i]
|
||||
params['migrateto[' + i + '].volume'] = mapping.volume
|
||||
params['migrateto[' + i + '].pool'] = mapping.pool
|
||||
}
|
||||
}
|
||||
api(migrateApi, params).then(response => {
|
||||
const jobId = response[migrateApi.toLowerCase() + 'response'].jobid
|
||||
this.$pollJob({
|
||||
jobId: jobId,
|
||||
title: `${this.$t('label.migrating')} ${this.resource.name}`,
|
||||
description: this.resource.name,
|
||||
successMessage: `${this.$t('message.success.migrating')} ${this.resource.name}`,
|
||||
successMethod: () => {
|
||||
this.closeModal()
|
||||
},
|
||||
errorMessage: this.$t('message.migrating.failed'),
|
||||
errorMethod: () => {
|
||||
this.closeModal()
|
||||
},
|
||||
loadingMessage: `${this.$t('message.migrating.processing')} ${this.resource.name}`,
|
||||
catchMessage: this.$t('error.fetching.async.job.result'),
|
||||
catchMethod: () => {
|
||||
this.closeModal()
|
||||
}
|
||||
})
|
||||
this.closeModal()
|
||||
}).catch(error => {
|
||||
this.$notification.error({
|
||||
message: this.$t('message.request.failed'),
|
||||
description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message,
|
||||
duration: 0
|
||||
})
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -308,7 +379,26 @@ export default {
|
|||
|
||||
}
|
||||
|
||||
.top-spaced {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
|
||||
button {
|
||||
&:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.table-tooltip-icon {
|
||||
color: rgba(0,0,0,.45);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -16,63 +16,47 @@
|
|||
// under the License.
|
||||
|
||||
<template>
|
||||
<div class="migrate-volume-container" v-ctrl-enter="submitMigrateVolume">
|
||||
<div class="modal-form">
|
||||
<div v-if="storagePools.length > 0">
|
||||
<a-alert type="warning">
|
||||
<span slot="message" v-html="$t('message.migrate.volume')" />
|
||||
</a-alert>
|
||||
<p class="modal-form__label">{{ $t('label.storagepool') }}</p>
|
||||
<a-select
|
||||
v-model="selectedStoragePool"
|
||||
style="width: 100%;"
|
||||
:autoFocus="storagePools.length > 0"
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
:filterOption="(input, option) => {
|
||||
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}" >
|
||||
<a-select-option v-for="(storagePool, index) in storagePools" :value="storagePool.id" :key="index">
|
||||
{{ storagePool.name }} <span v-if="resource.virtualmachineid">{{ storagePool.suitableformigration ? `(${$t('label.suitable')})` : `(${$t('label.not.suitable')})` }}</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<template v-if="this.resource.virtualmachineid">
|
||||
<p class="modal-form__label" @click="replaceDiskOffering = !replaceDiskOffering" style="cursor:pointer;">
|
||||
{{ $t('label.usenewdiskoffering') }}
|
||||
</p>
|
||||
<a-checkbox v-model="replaceDiskOffering" />
|
||||
<div class="form" v-ctrl-enter="handleKeyboardSubmit">
|
||||
<a-alert class="top-spaced" type="warning">
|
||||
<span slot="message" v-html="$t('message.migrate.volume')" />
|
||||
</a-alert>
|
||||
<storage-pool-select-view
|
||||
ref="storagePoolSelection"
|
||||
:resource="resource"
|
||||
:suitabilityEnabled="true"
|
||||
@storagePoolsUpdated="handleStoragePoolsChange"
|
||||
@select="handleStoragePoolSelect" />
|
||||
<div class="top-spaced" v-if="storagePools.length > 0">
|
||||
<template v-if="this.resource.virtualmachineid">
|
||||
<p class="modal-form__label" @click="replaceDiskOffering = !replaceDiskOffering" style="cursor:pointer;">
|
||||
{{ $t('label.usenewdiskoffering') }}
|
||||
</p>
|
||||
<a-checkbox v-model="replaceDiskOffering" />
|
||||
|
||||
<template v-if="replaceDiskOffering">
|
||||
<p class="modal-form__label">{{ $t('label.newdiskoffering') }}</p>
|
||||
<a-select
|
||||
v-model="selectedDiskOffering"
|
||||
style="width: 100%;"
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
:filterOption="(input, option) => {
|
||||
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}" >
|
||||
<a-select-option v-for="(diskOffering, index) in diskOfferings" :value="diskOffering.id" :key="index">
|
||||
{{ diskOffering.displaytext }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
<template v-if="replaceDiskOffering">
|
||||
<p class="modal-form__label">{{ $t('label.newdiskoffering') }}</p>
|
||||
<a-select
|
||||
:loading="diskOfferingLoading"
|
||||
v-model="selectedDiskOffering"
|
||||
style="width: 100%;"
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
:filterOption="(input, option) => {
|
||||
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}" >
|
||||
<a-select-option v-for="(diskOffering, index) in diskOfferings" :value="diskOffering.id" :key="index">
|
||||
{{ diskOffering.displaytext }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</div>
|
||||
<a-alert style="margin-top: 15px" type="warning" v-else>
|
||||
<span slot="message" v-html="$t('message.no.primary.stores')" />
|
||||
</a-alert>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<div class="actions">
|
||||
<a-button @click="closeModal">
|
||||
{{ $t('label.cancel') }}
|
||||
</a-button>
|
||||
<a-button type="primary" ref="submit" @click="submitMigrateVolume">
|
||||
{{ $t('label.ok') }}
|
||||
</a-button>
|
||||
<a-button @click="closeModal">{{ $t('label.cancel') }}</a-button>
|
||||
<a-button type="primary" ref="submit" :disabled="!selectedStoragePool" @click="submitForm">{{ $t('label.ok') }}</a-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -80,9 +64,13 @@
|
|||
|
||||
<script>
|
||||
import { api } from '@/api'
|
||||
import StoragePoolSelectView from '@/components/view/StoragePoolSelectView'
|
||||
|
||||
export default {
|
||||
name: 'MigrateVolume',
|
||||
components: {
|
||||
StoragePoolSelectView
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
type: Object,
|
||||
|
|
@ -95,71 +83,67 @@ export default {
|
|||
storagePools: [],
|
||||
selectedStoragePool: null,
|
||||
diskOfferings: [],
|
||||
diskOfferingLoading: false,
|
||||
replaceDiskOffering: false,
|
||||
selectedDiskOffering: null,
|
||||
isSubmitted: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.fetchStoragePools()
|
||||
this.resource.virtualmachineid && this.fetchDiskOfferings()
|
||||
watch: {
|
||||
replaceDiskOffering (newValue) {
|
||||
if (newValue) {
|
||||
this.fetchDiskOfferings()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchStoragePools () {
|
||||
if (this.resource.virtualmachineid) {
|
||||
api('findStoragePoolsForMigration', {
|
||||
id: this.resource.id
|
||||
}).then(response => {
|
||||
this.storagePools = response.findstoragepoolsformigrationresponse.storagepool || []
|
||||
if (Array.isArray(this.storagePools) && this.storagePools.length) {
|
||||
this.selectedStoragePool = this.storagePools[0].id || ''
|
||||
}
|
||||
}).catch(error => {
|
||||
this.$notifyError(error)
|
||||
this.closeModal()
|
||||
})
|
||||
} else {
|
||||
api('listStoragePools', {
|
||||
zoneid: this.resource.zoneid
|
||||
}).then(response => {
|
||||
this.storagePools = response.liststoragepoolsresponse.storagepool || []
|
||||
this.storagePools = this.storagePools.filter(pool => { return pool.id !== this.resource.storageid })
|
||||
if (Array.isArray(this.storagePools) && this.storagePools.length) {
|
||||
this.selectedStoragePool = this.storagePools[0].id || ''
|
||||
}
|
||||
}).catch(error => {
|
||||
this.$notifyError(error)
|
||||
this.closeModal()
|
||||
})
|
||||
}
|
||||
},
|
||||
fetchDiskOfferings () {
|
||||
this.diskOfferingLoading = true
|
||||
api('listDiskOfferings', {
|
||||
listall: true
|
||||
}).then(response => {
|
||||
this.diskOfferings = response.listdiskofferingsresponse.diskoffering
|
||||
this.selectedDiskOffering = this.diskOfferings[0].id
|
||||
}).catch(error => {
|
||||
this.$notifyError(error)
|
||||
this.closeModal()
|
||||
}).finally(() => {
|
||||
this.diskOfferingLoading = false
|
||||
if (this.diskOfferings.length > 0) {
|
||||
this.selectedDiskOffering = this.diskOfferings[0].id
|
||||
}
|
||||
})
|
||||
},
|
||||
closeModal () {
|
||||
this.$parent.$parent.close()
|
||||
handleStoragePoolsChange (storagePools) {
|
||||
this.storagePools = storagePools
|
||||
},
|
||||
submitMigrateVolume () {
|
||||
handleStoragePoolSelect (storagePool) {
|
||||
this.selectedStoragePool = storagePool
|
||||
},
|
||||
handleKeyboardSubmit () {
|
||||
if (!this.selectedStoragePool) {
|
||||
return
|
||||
}
|
||||
this.submitForm()
|
||||
},
|
||||
closeModal () {
|
||||
this.$emit('close-action')
|
||||
},
|
||||
submitForm () {
|
||||
if (this.isSubmitted) return
|
||||
if (this.storagePools.length === 0) {
|
||||
this.closeModal()
|
||||
return
|
||||
}
|
||||
this.isSubmitted = true
|
||||
api('migrateVolume', {
|
||||
var params = {
|
||||
livemigrate: this.resource.vmstate === 'Running',
|
||||
storageid: this.selectedStoragePool,
|
||||
volumeid: this.resource.id,
|
||||
newdiskofferingid: this.replaceDiskOffering ? this.selectedDiskOffering : null
|
||||
}).then(response => {
|
||||
storageid: this.selectedStoragePool.id,
|
||||
volumeid: this.resource.id
|
||||
}
|
||||
if (this.replaceDiskOffering) {
|
||||
params.newdiskofferingid = this.selectedDiskOffering
|
||||
}
|
||||
api('migrateVolume', params).then(response => {
|
||||
this.$pollJob({
|
||||
jobId: response.migratevolumeresponse.jobid,
|
||||
successMessage: this.$t('message.success.migrate.volume'),
|
||||
|
|
@ -185,14 +169,18 @@ export default {
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.migrate-volume-container {
|
||||
width: 85vw;
|
||||
.form {
|
||||
width: 80vw;
|
||||
|
||||
@media (min-width: 760px) {
|
||||
width: 500px;
|
||||
@media (min-width: 900px) {
|
||||
width: 850px;
|
||||
}
|
||||
}
|
||||
|
||||
.top-spaced {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
|
@ -204,14 +192,4 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-form {
|
||||
margin-top: -20px;
|
||||
|
||||
&__label {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import mockRouter from '../mock/mockRouter'
|
|||
|
||||
import localVue from '../setup'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { pollJobPlugin, notifierPlugin, configUtilPlugin, apiMetaUtilPlugin, toLocaleDatePlugin, showIconPlugin, resourceTypePlugin } from '@/utils/plugins'
|
||||
import { pollJobPlugin, notifierPlugin, configUtilPlugin, apiMetaUtilPlugin, toLocaleDatePlugin, showIconPlugin, resourceTypePlugin, fileSizeUtilPlugin } from '@/utils/plugins'
|
||||
|
||||
localVue.use(pollJobPlugin)
|
||||
localVue.use(notifierPlugin)
|
||||
|
|
@ -30,6 +30,7 @@ localVue.use(apiMetaUtilPlugin)
|
|||
localVue.use(toLocaleDatePlugin)
|
||||
localVue.use(showIconPlugin)
|
||||
localVue.use(resourceTypePlugin)
|
||||
localVue.use(fileSizeUtilPlugin)
|
||||
|
||||
function createMockRouter (newRoutes = []) {
|
||||
let routes = []
|
||||
|
|
|
|||
Loading…
Reference in New Issue