UI - Added option to allow users to select volumes when doing destroy the list of VMs (#5893)

* added option to allow users to select volumes when doing destroy list of VMs

* fixes
This commit is contained in:
Hoang Nguyen 2022-01-27 12:39:48 +07:00 committed by GitHub
parent 065847e6af
commit ee9c05b5fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 318 additions and 76 deletions

View File

@ -103,7 +103,7 @@
</a-affix>
<div v-show="showAction">
<keep-alive v-if="currentAction.component && (!currentAction.groupAction || this.selectedRowKeys.length === 0)">
<keep-alive v-if="currentAction.component && (!currentAction.groupAction || this.selectedRowKeys.length === 0 || (this.selectedRowKeys.length > 0 && currentAction.api === 'destroyVirtualMachine'))">
<a-modal
:visible="showAction"
:closable="true"
@ -131,10 +131,14 @@
:resource="resource"
:loading="loading"
:action="{currentAction}"
:selectedRowKeys="selectedRowKeys"
:selectedItems="selectedItems"
:chosenColumns="chosenColumns"
v-bind="{currentAction}"
@refresh-data="fetchData"
@poll-action="pollActionCompletion"
@close-action="closeAction"/>
@close-action="closeAction"
@cancel-bulk-action="handleCancel"/>
</a-modal>
</keep-alive>
<a-modal
@ -1596,4 +1600,9 @@ export default {
.ant-breadcrumb {
vertical-align: text-bottom;
}
/deep/.ant-alert-message {
display: flex;
align-items: center;
}
</style>

View File

@ -16,67 +16,155 @@
// under the License.
<template>
<div class="form-layout" v-ctrl-enter="handleSubmit">
<a-alert type="warning" v-html="resource.backupofferingid ? $t('message.action.destroy.instance.with.backups') : $t('message.action.destroy.instance')" /><br/>
<a-spin :spinning="loading">
<a-form
:form="form"
@submit="handleSubmit"
layout="vertical">
<a-form-item v-if="$store.getters.userInfo.roletype === 'Admin' || $store.getters.features.allowuserexpungerecovervm">
<tooltip-label slot="label" :title="$t('label.expunge')" :tooltip="apiParams.expunge.description"/>
<a-switch v-decorator="['expunge']" :auto-focus="true" />
</a-form-item>
<div :class="['form-layout', { 'form-list': selectedRowKeys.length > 0 }]" v-ctrl-enter="handleSubmit">
<div v-if="selectedRowKeys.length === 0">
<a-alert type="warning" v-html="resource.backupofferingid ? $t('message.action.destroy.instance.with.backups') : $t('message.action.destroy.instance')" /><br/>
<a-spin :spinning="loading">
<a-form
:form="form"
@submit="handleSubmit"
layout="vertical">
<a-form-item v-if="$store.getters.userInfo.roletype === 'Admin' || $store.getters.features.allowuserexpungerecovervm">
<tooltip-label slot="label" :title="$t('label.expunge')" :tooltip="apiParams.expunge.description"/>
<a-switch v-decorator="['expunge']" :auto-focus="true" />
</a-form-item>
<a-form-item v-if="volumes.length > 0">
<tooltip-label slot="label" :title="$t('label.delete.volumes')" :tooltip="apiParams.volumeids.description"/>
<a-select
v-decorator="['volumeids']"
:placeholder="$t('label.delete.volumes')"
mode="multiple"
:loading="loading"
:autoFocus="$store.getters.userInfo.roletype !== 'Admin' && !$store.getters.features.allowuserexpungerecovervm"
showSearch
optionFilterProp="children"
:filterOption="(input, option) => {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="volume in volumes" :key="volume.id">
{{ volume.name }}
</a-select-option>
</a-select>
</a-form-item>
<p v-else v-html="$t('label.volume.empty')" />
<a-form-item v-if="volumes.length > 0">
<tooltip-label slot="label" :title="$t('label.delete.volumes')" :tooltip="apiParams.volumeids.description"/>
<a-select
v-decorator="['volumeids']"
:placeholder="$t('label.delete.volumes')"
mode="multiple"
:loading="loading"
:autoFocus="$store.getters.userInfo.roletype !== 'Admin' && !$store.getters.features.allowuserexpungerecovervm"
showSearch
optionFilterProp="children"
:filterOption="(input, option) => {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}" >
<a-select-option v-for="volume in volumes" :key="volume.id">
{{ volume.name }}
</a-select-option>
</a-select>
</a-form-item>
<p v-else v-html="$t('label.volume.empty')" />
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ this.$t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ this.$t('label.ok') }}</a-button>
</div>
</a-form>
</a-spin>
</div>
<div v-else>
<div v-if="!showGroupActionModal">
<div>
<a-alert type="error">
<a-icon slot="message" type="exclamation-circle" style="color: red; fontSize: 30px; display: inline-flex" />
<span style="padding-left: 5px" slot="message" v-html="`<b>${selectedRowKeys.length} ` + $t('label.items.selected') + `. </b>`" />
<span slot="message" v-html="$t(action.currentAction.message)" />
</a-alert>
</div>
<div v-if="selectedRowKeys.length > 0" class="row-keys">
<a-divider />
<a-table
v-if="selectedRowKeys.length > 0"
size="middle"
:columns="chosenColumns"
:dataSource="selectedItems"
:rowKey="(record, idx) => record.id || record.name || record.usageType || idx + '-' + Math.random()"
:pagination="true"
style="overflow-y: auto"
>
<p
slot="expandedRowRender"
slot-scope="record"
style="margin: 0">
<a-form-item :label="$t('label.delete.volumes')" v-if="listVolumes[record.id].opts.length > 0">
<a-select
mode="multiple"
showSearch
optionFilterProp="children"
:filterOption="(input, option) => {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:loading="listVolumes[record.id].loading"
:placeholder="$t('label.delete.volumes')"
@change="(value) => onChangeVolume(record.id, value)">
<a-select-option v-for="item in listVolumes[record.id].opts" :key="item.id">
{{ item.name || item.description }}
</a-select-option>
</a-select>
</a-form-item>
<span v-else v-html="$t('label.volume.empty')" />
</p>
</a-table>
<a-form-item v-if="$store.getters.userInfo.roletype === 'Admin' || $store.getters.features.allowuserexpungerecovervm">
<tooltip-label slot="label" :title="$t('label.expunge')" :tooltip="apiParams.expunge.description"/>
<a-switch v-model="expunge" :auto-focus="true" />
</a-form-item>
</div>
<div :span="24" class="action-button">
<a-button @click="closeAction">{{ this.$t('label.cancel') }}</a-button>
<a-button :loading="loading" ref="submit" type="primary" @click="handleSubmit">{{ this.$t('label.ok') }}</a-button>
</div>
</a-form>
</a-spin>
</div>
</div>
<bulk-action-progress
:showGroupActionModal="showGroupActionModal"
:selectedItems="selectedItemsProgress"
:selectedColumns="selectedColumns"
:message="modalInfo"
@handle-cancel="handleCancel" />
</div>
</template>
<script>
import { api } from '@/api'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import BulkActionProgress from '@/components/view/BulkActionProgress'
export default {
name: 'DestroyVM',
components: {
TooltipLabel
TooltipLabel,
BulkActionProgress
},
props: {
resource: {
type: Object,
required: true
},
action: {
type: Object,
default: () => {}
},
selectedRowKeys: {
type: Array,
default: () => []
},
selectedItems: {
type: Array,
default: () => []
},
chosenColumns: {
type: Array,
default: () => []
}
},
inject: ['parentFetchData'],
data () {
return {
volumes: [],
loading: false
loading: false,
volumeIds: {},
listVolumes: {},
selectedColumns: [],
selectedItemsProgress: [],
showGroupActionModal: false,
modalInfo: {},
expunge: false
}
},
beforeCreate () {
@ -88,66 +176,198 @@ export default {
},
methods: {
fetchData () {
this.volumes = []
if (this.selectedRowKeys.length === 0) {
this.fetchVolumes()
} else {
const promises = []
this.selectedRowKeys.forEach(vmId => {
this.listVolumes[vmId] = {
loading: true,
opts: []
}
promises.push(this.callListVolume(vmId))
})
Promise.all(promises).then((data) => {
data.forEach(item => {
this.listVolumes[item.id].loading = false
this.listVolumes[item.id].opts = item.volumes || []
})
this.$forceUpdate()
})
}
},
async fetchVolumes () {
this.loading = true
api('listVolumes', {
virtualMachineId: this.resource.id,
type: 'DATADISK',
details: 'min',
listall: 'true'
}).then(json => {
this.volumes = json.listvolumesresponse.volume || []
}).finally(() => {
this.loading = false
const data = await this.callListVolume(this.resource.id)
this.volumes = data.volumes || []
this.loading = false
},
callListVolume (vmId) {
return new Promise((resolve) => {
this.volumes = []
api('listVolumes', {
virtualMachineId: vmId,
type: 'DATADISK',
details: 'min',
listall: 'true'
}).then(json => {
const volumes = json.listvolumesresponse.volume || []
resolve({
id: vmId,
volumes
})
})
})
},
onChangeVolume (vmId, volumes) {
this.volumeIds[vmId] = volumes
},
handleCancel () {
this.$emit('cancel-bulk-action')
this.showGroupActionModal = false
this.selectedItemsProgress = []
this.selectedColumns = []
this.closeAction()
},
handleSubmit (e) {
e.preventDefault()
if (this.loading) return
this.form.validateFieldsAndScroll((err, values) => {
if (err) {
return
}
this.loading = true
const params = {
id: this.resource.id
}
if (values.volumeids) {
params.volumeids = values.volumeids.join(',')
}
if (values.expunge) {
params.expunge = values.expunge
}
if (this.selectedRowKeys.length > 0) {
this.destroyGroupVMs()
} else {
this.form.validateFieldsAndScroll(async (err, values) => {
if (err) {
return
}
const params = {
id: this.resource.id
}
if (values.volumeids) {
params.volumeids = values.volumeids.join(',')
}
if (values.expunge) {
params.expunge = values.expunge
}
this.loading = true
try {
const jobId = await this.destroyVM(params)
await this.$pollJob({
jobId,
title: this.$t('label.action.destroy.instance'),
description: this.resource.name,
loadingMessage: `${this.$t('message.deleting.vm')} ${this.resource.name}`,
catchMessage: this.$t('error.fetching.async.job.result'),
successMessage: `${this.$t('message.success.delete.vm')} ${this.resource.name}`,
successMethod: () => {
if (this.$route.path.includes('/vm/') && values.expunge) {
this.$router.go(-1)
} else {
this.parentFetchData()
}
},
action: {
isFetchData: false
}
})
await this.closeAction()
this.loading = false
} catch (error) {
await this.$notifyError(error)
await this.closeAction()
this.loading = false
}
})
}
},
destroyVM (params) {
return new Promise((resolve, reject) => {
api('destroyVirtualMachine', params).then(json => {
const jobId = json.destroyvirtualmachineresponse.jobid
return resolve(jobId)
}).catch(error => {
return reject(error)
})
})
},
destroyGroupVMs () {
this.selectedColumns = Array.from(this.chosenColumns)
this.selectedItemsProgress = Array.from(this.selectedItems)
this.selectedItemsProgress = this.selectedItemsProgress.map(v => ({ ...v, status: 'InProgress' }))
this.selectedColumns.splice(0, 0, {
dataIndex: 'status',
title: this.$t('label.operation.status'),
scopedSlots: { customRender: 'status' },
filters: [
{ text: 'In Progress', value: 'InProgress' },
{ text: 'Success', value: 'success' },
{ text: 'Failed', value: 'failed' }
]
})
this.showGroupActionModal = true
this.modalInfo.title = this.action.currentAction.label
this.modalInfo.docHelp = this.action.currentAction.docHelp
const promises = []
this.selectedRowKeys.forEach(vmId => {
const params = {}
params.id = vmId
if (this.volumeIds[vmId] && this.volumeIds[vmId].length > 0) {
params.volumeids = this.volumeIds[vmId].join(',')
}
if (this.expunge) {
params.expunge = this.expunge
}
promises.push(this.callGroupApi(params))
})
this.$message.info({
content: this.$t(this.action.currentAction.label),
key: this.action.currentAction.label,
duration: 3
})
this.loading = true
Promise.all(promises).finally(() => {
this.loading = false
this.parentFetchData()
})
},
callGroupApi (params) {
return new Promise((resolve, reject) => {
const resource = this.selectedItems.filter(item => item.id === params.id)[0] || {}
this.destroyVM(params).then(jobId => {
this.updateResourceState(resource.id, 'InProgress', jobId)
this.$pollJob({
jobId,
showLoading: false,
bulkAction: false,
title: this.$t('label.action.destroy.instance'),
description: this.resource.name,
loadingMessage: `${this.$t('message.deleting.vm')} ${this.resource.name}`,
catchMessage: this.$t('error.fetching.async.job.result'),
successMessage: `${this.$t('message.success.delete.vm')} ${this.resource.name}`,
successMessage: `${this.$t('message.success.delete.vm')} ${resource.name}`,
successMethod: () => {
if (this.$route.path.includes('/vm/') && values.expunge) {
this.$router.go(-1)
} else {
this.parentFetchData()
}
this.updateResourceState(resource.id, 'success')
return resolve()
},
errorMethod: () => {
this.updateResourceState(resource.id, 'failed')
},
action: {
isFetchData: false
}
})
this.closeAction()
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading = false
this.updateResourceState(resource.id, 'failed')
return reject(error)
})
})
},
updateResourceState (resource, state, jobId) {
const objIndex = this.selectedItemsProgress.findIndex(item => item.id === resource)
if (state && objIndex !== -1) {
this.selectedItemsProgress[objIndex].status = state
}
if (jobId && objIndex !== -1) {
this.selectedItemsProgress[objIndex].jobid = jobId
}
},
closeAction () {
this.$emit('close-action')
}
@ -157,10 +377,23 @@ export default {
<style scoped lang="less">
.form-layout {
width: 60vw;
&.form-list {
max-width: 60vw;
}
@media (min-width: 500px) {
width: 450px;
&:not(.form-list) {
width: 60vw;
@media (min-width: 500px) {
width: 450px;
}
}
.row-keys {
.ant-select {
display: block;
width: 90%;
}
}
}
</style>