mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
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:
parent
065847e6af
commit
ee9c05b5fa
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user