UI: bulk action support for various resources (#5034)

* ui: support bulk action for various resources

* ui: support bulk action for various resources

* Bulk actions support - progress review

* Extract common code + suppress error notification with bulk actions

* cleanup + suppress notification

* add progress view

* Add routes to notification + add async jobs + refactor progress view

* minor tweaks

* fix group action for vpn users

* Refactor code

* Unique row key

* remove redundant cols

* address comments

* Added the following:
1. Make Cancel as default button for bulk actions
2. Add Filter Filter on the Operation status Column - Progress View
3. For Stop and delete bulk operations - add An alert message(in Red) to inform users that it is a destructive operation

* Add dynamism to column filtering
This commit is contained in:
Pearl Dsilva 2021-07-16 14:03:04 +05:30 committed by GitHub
parent ff07fee286
commit 15d3d39cb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1750 additions and 139 deletions

View File

@ -71,6 +71,13 @@
"label.action.attach.disk.processing": "Attaching Disk....",
"label.action.attach.iso": "Attach ISO",
"label.action.attach.iso.processing": "Attaching ISO....",
"label.action.bulk.delete.egress.firewall.rules": "Bulk delete egress firewall rules",
"label.action.bulk.delete.firewall.rules": "Bulk delete firewall rules",
"label.action.bulk.delete.load.balancer.rules": "Bulk delete load balancer rules",
"label.action.bulk.delete.templates": "Bulk delete templates",
"label.action.bulk.delete.isos": "Bulk delete ISOs",
"label.action.bulk.delete.portforward.rules": "Bulk delete Port Forward rules",
"label.action.bulk.release.public.ip.address": "Bulk release Public IP Addresses",
"label.action.cancel.maintenance.mode": "Cancel Maintenance Mode",
"label.action.cancel.maintenance.mode.processing": "Cancelling Maintenance Mode....",
"label.action.change.password": "Change Password",
@ -99,6 +106,7 @@
"label.action.delete.disk.offering.processing": "Deleting Disk Offering....",
"label.action.delete.domain": "Delete Domain",
"label.action.delete.domain.processing": "Deleting Domain....",
"label.action.delete.egress.firewall": "Delete egress firewall rule",
"label.action.delete.firewall": "Delete firewall rule",
"label.action.delete.firewall.processing": "Deleting Firewall....",
"label.action.delete.ingress.rule": "Delete Ingress Rule",
@ -586,6 +594,13 @@
"label.confirmdeclineinvitation": "Are you sure you want to decline this project invitation?",
"label.confirmpassword": "Confirm Password",
"label.confirmpassword.description": "Please type the same password again",
"label.confirm.delete.egress.firewall.rules": "Please confirm you wish to delete the selected egress firewall rules",
"label.confirm.delete.firewall.rules": "Please confirm you wish to delete the selected firewall rules",
"label.confirm.delete.loadbalancer.rules": "Please confirm you wish to delete the selected load balancing rules",
"label.confirm.delete.portforward.rules": "Please confirm you wish to delete the selected port-forward rules",
"label.confirm.delete.templates": "Please confirm you wish to delete the selected templates",
"label.confirm.delete.isos": "Please confirm you wish to delete the selected isos",
"label.confirm.release.public.ip.addresses": "Please confirm you wish to release the selected public IP addresses",
"label.congratulations": "Congratulations!",
"label.connectiontimeout": "Connection Timeout",
"label.conservemode": "Conserve mode",
@ -693,6 +708,7 @@
"label.delete.opendaylight.device": "Delete OpenDaylight Controller",
"label.delete.pa": "Delete Palo Alto",
"label.delete.portable.ip.range": "Delete Portable IP Range",
"label.delete.portforward.rules": "Delete Port Forward Rules",
"label.delete.project": "Delete project",
"label.delete.project.role": "Delete Project Role",
"label.delete.role": "Delete Role",
@ -902,6 +918,7 @@
"label.filterby": "Filter by",
"label.fingerprint": "FingerPrint",
"label.firewall": "Firewall",
"label.firewallrule": "Firewall Rule",
"label.firstname": "First Name",
"label.firstname.lower": "firstname",
"label.fix.errors": "Fix errors",
@ -1169,6 +1186,7 @@
"label.isvolatile": "Volatile",
"label.item.listing": "Item listing",
"label.items": "items",
"label.items.selected": "item(s) selected",
"label.japanese.keyboard": "Japanese keyboard",
"label.keep": "Keep",
"label.keep.colon": "Keep:",
@ -1520,6 +1538,7 @@
"label.opendaylight.controllerdetail": "OpenDaylight Controller Details",
"label.opendaylight.controllers": "OpenDaylight Controllers",
"label.operation": "Operation",
"label.operation.status": "Operation Status",
"label.optional": "Optional",
"label.order": "Order",
"label.oscategoryid": "OS Preference",
@ -1605,6 +1624,7 @@
"label.portable.ip.ranges": "Portable IP Ranges",
"label.portableipaddress": "Portable IPs",
"label.portforwarding": "Port Forwarding",
"label.portforwarding.rule": "Port Forwarding Rule",
"label.powerflex.gateway": "Gateway",
"label.powerflex.gateway.username": "Gateway Username",
"label.powerflex.gateway.password": "Gateway Password",
@ -2399,6 +2419,7 @@
"message.action.delete.external.firewall": "Please confirm that you would like to remove this external firewall. Warning: If you are planning to add back the same external firewall, you must reset usage data on the device.",
"message.action.delete.external.load.balancer": "Please confirm that you would like to remove this external load balancer. Warning: If you are planning to add back the same external load balancer, you must reset usage data on the device.",
"message.action.delete.ingress.rule": "Please confirm that you want to delete this ingress rule.",
"message.action.delete.instance.group": "Please confirm that you want to delete the instance group",
"message.action.delete.iso": "Please confirm that you want to delete this ISO.",
"message.action.delete.iso.for.all.zones": "The ISO is used by all zones. Please confirm that you want to delete it from all zones.",
"message.action.delete.network": "Please confirm that you want to delete this network.",
@ -3323,6 +3344,8 @@
"state.error": "Error",
"state.expired": "Expired",
"state.expunging": "Expunging",
"state.failed": "Failed",
"state.inprogress": "In Progress",
"state.migrating": "Migrating",
"state.pending": "Pending",
"state.readonly": "Read-Only",

View File

@ -33,8 +33,11 @@
</a-list-item-meta>
</a-list-item>
<a-list-item v-for="(job, index) in jobs" :key="index">
<a-list-item-meta :title="job.title" :description="job.description">
<a-avatar :style="notificationAvatar[job.status].style" :icon="notificationAvatar[job.status].icon" slot="avatar"/>
<a-list-item-meta :title="job.title">
<a-avatar :style="notificationAvatar[job.status].style" :icon="notificationAvatar[job.status].icon" slot="avatar"/><br/>
<span v-if="getResourceName(job.description, 'name') && job.path" slot="description"><router-link :to="{ path: job.path}"> {{ getResourceName(job.description, "name") + ' - ' }}</router-link></span>
<span v-if="getResourceName(job.description, 'name') && job.path" slot="description"> {{ getResourceName(job.description, "msg") }}</span>
<span v-else slot="description"> {{ job.description }} </span>
</a-list-item-meta>
</a-list-item>
</a-list>
@ -80,6 +83,16 @@ export default {
this.pollJobs()
}, 4000)
},
getResourceName (description, data) {
if (description) {
if (data === 'name') {
const name = description.match(/\(([^)]+)\)/)
return name ? name[1] : null
}
const msg = description.substring(description.indexOf(')') + 1)
return msg
}
},
async pollJobs () {
var hasUpdated = false
for (var i in this.jobs) {
@ -102,12 +115,14 @@ export default {
if (result.jobresult.errortext !== null) {
this.jobs[i].description = '(' + this.jobs[i].description + ') ' + result.jobresult.errortext
}
this.$notification.error({
message: this.jobs[i].title,
description: this.jobs[i].description,
key: this.jobs[i].jobid,
duration: 0
})
if (!this.jobs[i].bulkAction) {
this.$notification.error({
message: this.jobs[i].title,
description: this.jobs[i].description,
key: this.jobs[i].jobid,
duration: 0
})
}
}
}).catch(function (e) {
console.log(this.$t('error.fetching.async.job.result') + e)

View File

@ -0,0 +1,191 @@
// 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>
<a-modal
:visible="showGroupActionModal"
:closable="true"
:maskClosable="false"
:cancelText="$t('label.cancel')"
@cancel="handleCancel"
width="50vw"
style="top: 20px;overflow-y: auto"
centered
>
<span slot="title"> {{ $t(message.title) }}
<a
v-if="message.docHelp || $route.meta.docHelp"
style="margin-left: 5px"
:href="$config.docBase + '/' + (message.docHelp || $route.meta.docHelp)"
target="_blank">
<a-icon type="question-circle-o"></a-icon>
</a>
</span>
<template slot="footer">
<a-button key="back" @click="handleCancel"> {{ $t('label.close') }} </a-button>
</template>
<a-card :bordered="false" style="background:#f1f1f1">
<div><a-icon type="check-circle-o" style="color: #52c41a; margin-right: 8px"/> {{ $t('label.success') + ': ' + succeededCount }}</div>
<div><a-icon type="close-circle-o" style="color: #f5222d; margin-right: 8px"/> {{ $t('state.failed') + ': ' + failedCount }}</div>
<div><a-icon type="sync-o" style="color: #1890ff; margin-right: 8px"/> {{ $t('state.inprogress') + ': ' + selectedItems.filter(item => item.status === 'InProgress').length || 0 }}</div>
</a-card>
<a-divider />
<div v-if="showGroupActionModal">
<a-table
v-if="selectedItems.length > 0"
size="middle"
:columns="selectedColumns"
:dataSource="tableChanged ? filteredItems : selectedItems"
:rowKey="(record, idx) => (this.$route.path.includes('/template') || this.$route.path.includes('/iso')) ? record.zoneid: record.id"
:pagination="true"
@change="handleTableChange"
style="overflow-y: auto">
<div slot="status" slot-scope="text">
<status :text=" text ? text : $t('state.inprogress') " displayText></status>
</div>
<template slot="algorithm" slot-scope="record">
{{ returnAlgorithmName(record.algorithm) }}
</template>
<template slot="privateport" slot-scope="record">
{{ record.privateport }} - {{ record.privateendport }}
</template>
<template slot="publicport" slot-scope="record">
{{ record.publicport }} - {{ record.publicendport }}
</template>
<template slot="protocol" slot-scope="record">
{{ record.protocol | capitalise }}
</template>
<template slot="startport" slot-scope="record">
{{ record.icmptype || record.startport >= 0 ? record.icmptype || record.startport : $t('label.all') }}
</template>
<template slot="endport" slot-scope="record">
{{ record.icmpcode || record.endport >= 0 ? record.icmpcode || record.endport : $t('label.all') }}
</template>
<template slot="vm" slot-scope="record">
<div><a-icon type="desktop"/> {{ record.virtualmachinename }} ({{ record.vmguestip }})</div>
</template>
</a-table>
<br/>
</div>
</a-modal>
</template>
<script>
import Status from '@/components/widgets/Status'
export default {
name: 'BulkActionProgress',
components: {
Status
},
filters: {
capitalise: val => {
if (val === 'all') return 'All'
return val.toUpperCase()
}
},
props: {
showGroupActionModal: {
type: Boolean,
default: false
},
selectedItems: {
type: Array,
default: () => []
},
selectedColumns: {
type: Array,
default: () => []
},
message: {
type: Object,
default: () => {}
}
},
created () {
this.filteredItems = this.selectedItems
},
data () {
return {
appliedFilterStatus: {},
filteredItems: [],
filterItemsTimer: null,
tableChanged: false
}
},
inject: ['parentFetchData'],
watch: {
succeededCount (count) {
if (count > 0) {
this.filterItemsDelayed()
}
},
failedCount (count) {
if (count > 0) {
this.filterItemsDelayed()
}
}
},
computed: {
succeededCount () {
return this.selectedItems.filter(item => item.status === 'success').length || 0
},
failedCount () {
return this.selectedItems.filter(item => item.status === 'failed').length || 0
}
},
methods: {
handleTableChange (pagination, filters, sorter) {
this.filteredItems = this.selectedItems
this.appliedFilterStatus = filters.status
this.filterItems()
this.tableChanged = true
},
filterItems () {
if (this.appliedFilterStatus && this.appliedFilterStatus.length > 0) {
this.filteredItems = this.selectedItems.filter(item => {
if (this.appliedFilterStatus.includes(item.status)) {
return item
}
})
}
},
filterItemsDelayed () {
clearTimeout(this.filterItemsTimer)
this.filterItemsTimer = setTimeout(() => {
this.filterItems()
}, 50)
},
handleCancel () {
this.filteredItems = []
this.tableChanged = false
this.$emit('handle-cancel')
},
returnAlgorithmName (name) {
switch (name) {
case 'leastconn':
return 'Least connections'
case 'roundrobin' :
return 'Round-robin'
case 'source':
return 'Source'
default :
return ''
}
}
}
}
</script>

View File

@ -0,0 +1,192 @@
// 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-modal
v-if="showConfirmationAction"
:visible="showConfirmationAction"
:closable="true"
:maskClosable="false"
:okText="$t('label.ok')"
:cancelText="$t('label.cancel')"
style="top: 20px;"
width="50vw"
@ok="groupAction"
@cancel="closeModal"
:ok-button-props="{props: { type: 'default' } }"
:cancel-button-props="{props: { type: 'primary' } }"
centered>
<span slot="title">
{{ $t(message.title) }}
</span>
<span>
<a-alert
v-if="isDestructiveAction()"
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(message.confirmMessage)" />
</a-alert>
<a-alert v-else type="warning">
<span v-if="selectedRowKeys.length > 0" slot="message" v-html="`<b>${selectedRowKeys.length} ` + $t('label.items.selected') + `. </b>`" />
<span slot="message" v-html="$t(message.confirmMessage)" />
</a-alert>
<a-divider />
<a-table
size="middle"
:columns="selectedColumns"
:dataSource="selectedItems"
:rowKey="(record, idx) => this.$route.path.includes('/iso/') ? record.zoneid : record.id"
:pagination="true"
style="overflow-y: auto">
<template slot="algorithm" slot-scope="record">
{{ returnAlgorithmName(record.algorithm) }}
</template>
<template v-for="(column, index) in selectedColumns" :slot="column" slot-scope="text" >
<span :key="index"> {{ text }} ==== {{ column }} </span>
</template>
<template slot="privateport" slot-scope="record">
{{ record.privateport }} - {{ record.privateendport }}
</template>
<template slot="publicport" slot-scope="record">
{{ record.publicport }} - {{ record.publicendport }}
</template>
<template slot="protocol" slot-scope="record">
{{ record.protocol | capitalise }}
</template>
<template slot="vm" slot-scope="record">
<div><a-icon type="desktop"/> {{ record.virtualmachinename }} ({{ record.vmguestip }})</div>
</template>
<template slot="startport" slot-scope="record">
{{ record.icmptype || record.startport >= 0 ? record.icmptype || record.startport : $t('label.all') }}
</template>
<template slot="endport" slot-scope="record">
{{ record.icmpcode || record.endport >= 0 ? record.icmpcode || record.endport : $t('label.all') }}
</template>
</a-table>
<a-divider />
<br/>
</span>
</a-modal>
<bulk-action-progress
:showGroupActionModal="showGroupActionModal"
:selectedItems="selectedItems"
:selectedColumns="selectedColumns"
:message="message"
@handle-cancel="handleCancel" />
</div>
</template>
<script>
import Status from '@/components/widgets/Status'
import BulkActionProgress from '@/components/view/BulkActionProgress'
export default {
name: 'BulkActionView',
components: {
Status,
BulkActionProgress
},
props: {
showConfirmationAction: {
type: Boolean,
default: false
},
showGroupActionModal: {
type: Boolean,
default: false
},
items: {
type: Array,
default: () => []
},
selectedRowKeys: {
type: Array,
default: () => []
},
selectedItems: {
type: Array,
default: () => []
},
columns: {
type: Array,
default: () => []
},
selectedColumns: {
type: Array,
default: () => []
},
action: {
type: String,
required: true
},
loading: {
type: Boolean,
default: false
},
message: {
type: Object,
default: () => {}
}
},
filters: {
capitalise: val => {
if (val === 'all') return 'All'
return val.toUpperCase()
}
},
inject: ['parentFetchData'],
data () {
return {
totalCount: 0,
page: 1,
pageSize: 10
}
},
methods: {
handleCancel () {
this.$emit('handle-cancel')
},
groupAction () {
this.$emit('group-action')
},
isDestructiveAction () {
if (new RegExp(['remove', 'delete', 'destroy', 'stop', 'release', 'disassociate'].join('|')).test(this.action)) {
return true
}
return false
},
returnAlgorithmName (name) {
switch (name) {
case 'leastconn':
return 'Least connections'
case 'roundrobin' :
return 'Round-robin'
case 'source':
return 'Source'
default :
return ''
}
},
closeModal () {
this.$emit('close-modal')
}
}
}
</script>

View File

@ -23,8 +23,7 @@
:dataSource="items"
:rowKey="(record, idx) => record.id || record.name || record.usageType || idx + '-' + Math.random()"
:pagination="false"
:rowSelection="['vm', 'alert'].includes($route.name) || $route.name === 'event' && $store.getters.userInfo.roletype === 'Admin'
? {selectedRowKeys: selectedRowKeys, onChange: onSelectChange} : null"
:rowSelection=" enableGroupAction() || $route.name === 'event' ? {selectedRowKeys: selectedRowKeys, onChange: onSelectChange} : null"
:rowClassName="getRowClassName"
style="overflow-y: auto"
>
@ -422,6 +421,13 @@ export default {
'/computeoffering', '/systemoffering', '/diskoffering', '/backupoffering', '/networkoffering', '/vpcoffering'].join('|'))
.test(this.$route.path)
},
enableGroupAction () {
return ['vm', 'alert', 'vmgroup', 'ssh', 'affinitygroup', 'volume', 'snapshot',
'vmsnapshot', 'guestnetwork', 'vpc', 'publicip', 'vpnuser', 'vpncustomergateway',
'project', 'account', 'systemvm', 'router', 'computeoffering', 'systemoffering',
'diskoffering', 'backupoffering', 'networkoffering', 'vpcoffering', 'ilbvm', 'kubernetes'
].includes(this.$route.name)
},
fetchColumns () {
if (this.isOrderUpdatable()) {
return this.columns

View File

@ -78,6 +78,9 @@ export default {
case 'ReadWrite':
state = this.$t('state.readwrite')
break
case 'InProgress':
state = this.$t('state.inprogress')
break
}
return state.charAt(0).toUpperCase() + state.slice(1)
}
@ -102,6 +105,7 @@ export default {
case 'True':
case 'Up':
case 'enabled':
case 'success':
status = 'success'
break
case 'Alert':
@ -112,6 +116,7 @@ export default {
case 'Error':
case 'False':
case 'Stopped':
case 'failed':
status = 'error'
break
case 'Migrating':
@ -119,6 +124,7 @@ export default {
case 'Starting':
case 'Stopping':
case 'Upgrading':
case 'InProgress':
status = 'processing'
break
case 'Allocated':

View File

@ -116,7 +116,10 @@ export default {
!(record.domain === 'ROOT' && record.name === 'admin' && record.accounttype === 1) &&
(record.state === 'disabled' || record.state === 'locked')
},
params: { lock: 'false' }
params: { lock: 'false' },
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
},
{
api: 'disableAccount',
@ -134,7 +137,10 @@ export default {
lock: {
value: (record) => { return false }
}
}
},
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x, lock: false } }) }
},
{
api: 'disableAccount',
@ -152,7 +158,10 @@ export default {
lock: {
value: (record) => { return true }
}
}
},
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x, lock: true } }) }
},
{
api: 'uploadSslCert',
@ -180,7 +189,10 @@ export default {
show: (record, store) => {
return ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) && !record.isdefault &&
!(record.domain === 'ROOT' && record.name === 'admin' && record.accounttype === 1)
}
},
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
}

View File

@ -131,7 +131,10 @@ export default {
}
}
return fields
}
},
groupAction: true,
popup: true,
groupMap: (selection, values) => { return selection.map(x => { return { id: x, forced: values.forced } }) }
},
{
api: 'restoreVirtualMachine',
@ -452,7 +455,10 @@ export default {
message: 'message.kubernetes.cluster.start',
docHelp: 'plugins/cloudstack-kubernetes-service.html#starting-a-stopped-kubernetes-cluster',
dataView: true,
show: (record) => { return ['Stopped'].includes(record.state) }
show: (record) => { return ['Stopped'].includes(record.state) },
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
},
{
api: 'stopKubernetesCluster',
@ -461,7 +467,10 @@ export default {
message: 'message.kubernetes.cluster.stop',
docHelp: 'plugins/cloudstack-kubernetes-service.html#stopping-kubernetes-cluster',
dataView: true,
show: (record) => { return !['Stopped', 'Destroyed', 'Destroying'].includes(record.state) }
show: (record) => { return !['Stopped', 'Destroyed', 'Destroying'].includes(record.state) },
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
},
{
api: 'scaleKubernetesCluster',
@ -492,7 +501,10 @@ export default {
message: 'message.kubernetes.cluster.delete',
docHelp: 'plugins/cloudstack-kubernetes-service.html#deleting-kubernetes-cluster',
dataView: true,
show: (record) => { return !['Destroyed', 'Destroying'].includes(record.state) }
show: (record) => { return !['Destroyed', 'Destroying'].includes(record.state) },
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
},
@ -528,7 +540,11 @@ export default {
api: 'deleteInstanceGroup',
icon: 'delete',
label: 'label.delete.instance.group',
dataView: true
message: 'message.action.delete.instance.group',
dataView: true,
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
},
@ -578,6 +594,16 @@ export default {
domainid: {
value: (record, params) => { return record.domainid }
}
},
groupAction: true,
popup: true,
groupMap: (selection, values, record) => {
return selection.map(x => {
const data = record.filter(y => { return y.name === x })
return {
name: x, account: data[0].account, domainid: data[0].domainid
}
})
}
}
]
@ -621,7 +647,10 @@ export default {
label: 'label.delete.affinity.group',
docHelp: 'adminguide/virtual_machines.html#delete-an-affinity-group',
message: 'message.delete.affinity.group',
dataView: true
dataView: true,
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
}

View File

@ -30,7 +30,10 @@ export default {
label: 'label.action.start.router',
message: 'message.confirm.start.lb.vm',
dataView: true,
show: (record) => { return record.state === 'Stopped' }
show: (record) => { return record.state === 'Stopped' },
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
},
{
api: 'stopInternalLoadBalancerVM',
@ -38,7 +41,10 @@ export default {
label: 'label.action.stop.router',
dataView: true,
args: ['forced'],
show: (record) => { return record.state === 'Running' }
show: (record) => { return record.state === 'Running' },
groupAction: true,
popup: true,
groupMap: (selection, values) => { return selection.map(x => { return { id: x, forced: values.forced } }) }
},
{
api: 'migrateSystemVm',

View File

@ -49,7 +49,10 @@ export default {
label: 'label.action.start.router',
message: 'message.action.start.router',
dataView: true,
show: (record) => { return record.state === 'Stopped' }
show: (record) => { return record.state === 'Stopped' },
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
},
{
api: 'stopRouter',
@ -58,7 +61,10 @@ export default {
message: 'message.action.stop.router',
dataView: true,
args: ['forced'],
show: (record) => { return record.state === 'Running' }
show: (record) => { return record.state === 'Running' },
groupAction: true,
popup: true,
groupMap: (selection, values) => { return selection.map(x => { return { id: x, forced: values.forced } }) }
},
{
api: 'rebootRouter',
@ -67,7 +73,10 @@ export default {
message: 'message.action.reboot.router',
dataView: true,
args: ['forced'],
hidden: (record) => { return record.state === 'Running' }
hidden: (record) => { return record.state === 'Running' },
groupAction: true,
popup: true,
groupMap: (selection, values) => { return selection.map(x => { return { id: x, forced: values.forced } }) }
},
{
api: 'scaleSystemVm',
@ -156,7 +165,10 @@ export default {
label: 'label.destroy.router',
message: 'message.confirm.destroy.router',
dataView: true,
show: (record) => { return ['Running', 'Error', 'Stopped'].includes(record.state) }
show: (record) => { return ['Running', 'Error', 'Stopped'].includes(record.state) },
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
}

View File

@ -30,7 +30,10 @@ export default {
label: 'label.action.start.systemvm',
message: 'message.action.start.systemvm',
dataView: true,
show: (record) => { return record.state === 'Stopped' }
show: (record) => { return record.state === 'Stopped' },
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
},
{
api: 'stopSystemVm',
@ -39,7 +42,10 @@ export default {
message: 'message.action.stop.systemvm',
dataView: true,
show: (record) => { return record.state === 'Running' },
args: ['forced']
args: ['forced'],
groupAction: true,
popup: true,
groupMap: (selection, values) => { return selection.map(x => { return { id: x, forced: values.forced } }) }
},
{
api: 'rebootSystemVm',
@ -48,7 +54,10 @@ export default {
message: 'message.action.reboot.systemvm',
dataView: true,
show: (record) => { return record.state === 'Running' },
args: ['forced']
args: ['forced'],
groupAction: true,
popup: true,
groupMap: (selection, values) => { return selection.map(x => { return { id: x, forced: values.forced } }) }
},
{
api: 'scaleSystemVm',
@ -121,7 +130,10 @@ export default {
label: 'label.action.destroy.systemvm',
message: 'message.action.destroy.systemvm',
dataView: true,
show: (record) => { return ['Running', 'Error', 'Stopped'].includes(record.state) }
show: (record) => { return ['Running', 'Error', 'Stopped'].includes(record.state) },
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
}

View File

@ -88,7 +88,10 @@ export default {
message: 'message.restart.network',
dataView: true,
args: ['cleanup'],
show: (record) => record.type !== 'L2'
show: (record) => record.type !== 'L2',
groupAction: true,
popup: true,
groupMap: (selection, values) => { return selection.map(x => { return { id: x, cleanup: values.cleanup } }) }
},
{
api: 'replaceNetworkACLList',
@ -114,7 +117,10 @@ export default {
icon: 'delete',
label: 'label.action.delete.network',
message: 'message.action.delete.network',
dataView: true
dataView: true,
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
},
@ -174,14 +180,20 @@ export default {
fields.push('makeredundant')
}
return fields
}
},
groupAction: true,
popup: true,
groupMap: (selection, values) => { return selection.map(x => { return { id: x, cleanup: values.cleanup, makeredundant: values.makeredundant } }) }
},
{
api: 'deleteVPC',
icon: 'delete',
label: 'label.remove.vpc',
message: 'message.remove.vpc',
dataView: true
dataView: true,
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
},
@ -257,7 +269,10 @@ export default {
}, {
name: 'firewall',
component: () => import('@/views/network/FirewallRules.vue'),
networkServiceFilter: networkService => networkService.filter(x => x.name === 'Firewall').length > 0
networkServiceFilter: networkService => networkService.filter(x => x.name === 'Firewall').length > 0,
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
},
{
name: 'portforwarding',
@ -305,7 +320,10 @@ export default {
message: 'message.action.release.ip',
docHelp: 'adminguide/networking_and_traffic.html#releasing-an-ip-address-alloted-to-a-vpc',
dataView: true,
show: (record) => { return !record.issourcenat }
show: (record) => { return !record.issourcenat },
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
},
@ -583,6 +601,16 @@ export default {
account: {
value: (record) => { return record.account }
}
},
groupAction: true,
popup: true,
groupMap: (selection, values, record) => {
return selection.map(x => {
const data = record.filter(y => { return y.id === x })
return {
username: data[0].username, account: data[0].account, domainid: data[0].domainid
}
})
}
}
]
@ -624,7 +652,10 @@ export default {
label: 'label.delete.vpn.customer.gateway',
message: 'message.delete.vpn.customer.gateway',
docHelp: 'adminguide/networking_and_traffic.html#updating-and-removing-a-vpn-customer-gateway',
dataView: true
dataView: true,
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
}

View File

@ -76,7 +76,10 @@ export default {
label: 'label.action.delete.service.offering',
message: 'message.action.delete.service.offering',
docHelp: 'adminguide/service_offerings.html#modifying-or-deleting-a-service-offering',
dataView: true
dataView: true,
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}]
},
{
@ -112,7 +115,10 @@ export default {
message: 'message.action.delete.system.service.offering',
docHelp: 'adminguide/service_offerings.html#modifying-or-deleting-a-service-offering',
dataView: true,
params: { issystem: 'true' }
params: { issystem: 'true' },
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}]
},
{
@ -165,7 +171,10 @@ export default {
label: 'label.action.delete.disk.offering',
message: 'message.action.delete.disk.offering',
docHelp: 'adminguide/service_offerings.html#modifying-or-deleting-a-service-offering',
dataView: true
dataView: true,
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}]
},
{
@ -190,7 +199,10 @@ export default {
label: 'label.action.delete.backup.offering',
message: 'message.action.delete.backup.offering',
docHelp: 'adminguide/service_offerings.html#modifying-or-deleting-a-service-offering',
dataView: true
dataView: true,
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}]
},
{
@ -234,7 +246,10 @@ export default {
state: {
value: (record) => { return 'Enabled' }
}
}
},
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x, state: 'Enabled' } }) }
}, {
api: 'updateNetworkOffering',
icon: 'pause-circle',
@ -247,7 +262,10 @@ export default {
state: {
value: (record) => { return 'Disabled' }
}
}
},
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x, state: 'Disabled' } }) }
}, {
api: 'updateNetworkOffering',
icon: 'lock',
@ -262,7 +280,10 @@ export default {
label: 'label.remove.network.offering',
message: 'message.confirm.remove.network.offering',
docHelp: 'adminguide/service_offerings.html#modifying-or-deleting-a-service-offering',
dataView: true
dataView: true,
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}]
},
{
@ -306,7 +327,10 @@ export default {
state: {
value: (record) => { return 'Enabled' }
}
}
},
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x, state: 'Enabled' } }) }
}, {
api: 'updateVPCOffering',
icon: 'pause-circle',
@ -319,7 +343,10 @@ export default {
state: {
value: (record) => { return 'Disabled' }
}
}
},
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x, state: 'Disabled' } }) }
}, {
api: 'updateVPCOffering',
icon: 'lock',
@ -332,7 +359,10 @@ export default {
icon: 'delete',
label: 'label.remove.vpc.offering',
message: 'message.confirm.remove.vpc.offering',
dataView: true
dataView: true,
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}]
}
]

View File

@ -104,7 +104,10 @@ export default {
dataView: true,
show: (record, store) => {
return ((['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)) || record.isCurrentUserProjectAdmin) && record.state === 'Suspended'
}
},
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
},
{
api: 'suspendProject',
@ -116,7 +119,10 @@ export default {
show: (record, store) => {
return ((['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)) ||
record.isCurrentUserProjectAdmin) && record.state !== 'Suspended'
}
},
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
},
{
api: 'addAccountToProject',
@ -139,7 +145,10 @@ export default {
dataView: true,
show: (record, store) => {
return (['Admin', 'DomainAdmin'].includes(store.userInfo.roletype)) || record.isCurrentUserProjectAdmin
}
},
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
}

View File

@ -223,12 +223,14 @@ export default {
label: 'label.action.delete.volume',
message: 'message.action.delete.volume',
dataView: true,
groupAction: true,
show: (record, store) => {
return ['Expunging', 'Expunged', 'UploadError'].includes(record.state) ||
['Allocated', 'Uploaded'].includes(record.state) && record.type !== 'ROOT' && !record.virtualmachineid ||
((['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) || store.features.allowuserexpungerecovervolume) && record.state === 'Destroy')
}
},
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
},
{
api: 'destroyVolume',
@ -311,7 +313,10 @@ export default {
label: 'label.action.delete.snapshot',
message: 'message.action.delete.snapshot',
dataView: true,
show: (record) => { return record.state !== 'Destroyed' }
show: (record) => { return record.state !== 'Destroyed' },
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { id: x } }) }
}
]
},
@ -369,7 +374,10 @@ export default {
vmsnapshotid: {
value: (record) => { return record.id }
}
}
},
groupAction: true,
popup: true,
groupMap: (selection) => { return selection.map(x => { return { vmsnapshotid: x } }) }
}
]
},

View File

@ -36,6 +36,7 @@ export const pollJobPlugin = {
* @param {String} [catchMessage=Error caught]
* @param {Function} [catchMethod=() => {}]
* @param {Object} [action=null]
* @param {Object} [bulkAction=false]
*/
const {
jobId,
@ -48,7 +49,8 @@ export const pollJobPlugin = {
showLoading = true,
catchMessage = i18n.t('label.error.caught'),
catchMethod = () => {},
action = null
action = null,
bulkAction = false
} = options
api('queryAsyncJobResult', { jobId }).then(json => {
@ -69,11 +71,13 @@ export const pollJobPlugin = {
eventBus.$emit('async-job-complete', action)
successMethod(result)
} else if (result.jobstatus === 2) {
message.error({
content: errorMessage,
key: jobId,
duration: 1
})
if (!bulkAction) {
message.error({
content: errorMessage,
key: jobId,
duration: 1
})
}
var title = errorMessage
if (action && action.label) {
title = i18n.t(action.label)
@ -82,12 +86,14 @@ export const pollJobPlugin = {
if (name) {
desc = `(${name}) ${desc}`
}
notification.error({
message: title,
description: desc,
key: jobId,
duration: 0
})
if (!bulkAction) {
notification.error({
message: title,
description: desc,
key: jobId,
duration: 0
})
}
eventBus.$emit('async-job-complete', action)
errorMethod(result)
} else if (result.jobstatus === 0) {
@ -110,6 +116,7 @@ export const pollJobPlugin = {
duration: 0
})
catchMethod && catchMethod()
// }
})
}
}

View File

@ -68,7 +68,7 @@
<slot name="action" v-if="dataView && $route.path.startsWith('/publicip')"></slot>
<action-button
v-else
:style="dataView ? { float: device === 'mobile' ? 'left' : 'right' } : { 'margin-right': '10px', display: 'inline-flex' }"
:style="dataView ? { float: device === 'mobile' ? 'left' : 'right' } : { 'margin-right': '10px', display: getStyle(), padding: '5px' }"
:loading="loading"
:actions="actions"
:selectedRowKeys="selectedRowKeys"
@ -127,8 +127,11 @@
:okText="$t('label.ok')"
:cancelText="$t('label.cancel')"
style="top: 20px;"
:width="modalWidth"
@ok="handleSubmit"
@cancel="closeAction"
:ok-button-props="getOkProps()"
:cancel-button-props="getCancelProps()"
:confirmLoading="actionLoading"
centered
>
@ -144,9 +147,37 @@
</span>
<a-spin :spinning="actionLoading">
<span v-if="currentAction.message">
<a-alert type="warning">
<span slot="message" v-html="$t(currentAction.message)" />
</a-alert>
<div v-if="selectedRowKeys.length > 0">
<a-alert
v-if="['delete', 'poweroff'].includes(currentAction.icon)"
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(currentAction.message)" />
</a-alert>
<a-alert v-else type="warning">
<span v-if="selectedRowKeys.length > 0" slot="message" v-html="`<b>${selectedRowKeys.length} ` + $t('label.items.selected') + `. </b>`" />
<span slot="message" v-html="$t(currentAction.message)" />
</a-alert>
</div>
<div v-else>
<a-alert type="warning">
<span slot="message" v-html="$t(currentAction.message)" />
</a-alert>
</div>
<div v-if="selectedRowKeys.length > 0">
<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"
>
</a-table>
</div>
<br v-if="currentAction.paramFields.length > 0"/>
</span>
<a-form
@ -293,6 +324,7 @@
</a-form-item>
</a-form>
</a-spin>
<br />
</a-modal>
</div>
@ -330,6 +362,12 @@
</template>
</a-pagination>
</div>
<bulk-action-progress
:showGroupActionModal="showGroupActionModal"
:selectedItems="selectedItems"
:selectedColumns="selectedColumns"
:message="modalInfo"
@handle-cancel="handleCancel" />
</div>
</template>
@ -347,6 +385,7 @@ import ListView from '@/components/view/ListView'
import ResourceView from '@/components/view/ResourceView'
import ActionButton from '@/components/view/ActionButton'
import SearchView from '@/components/view/SearchView'
import BulkActionProgress from '@/components/view/BulkActionProgress'
import TooltipLabel from '@/components/widgets/TooltipLabel'
export default {
@ -359,6 +398,7 @@ export default {
Status,
ActionButton,
SearchView,
BulkActionProgress,
TooltipLabel
},
mixins: [mixinDevice],
@ -381,7 +421,12 @@ export default {
loading: false,
actionLoading: false,
columns: [],
selectedColumns: [],
chosenColumns: [],
showGroupActionModal: false,
selectedItems: [],
items: [],
modalInfo: {},
itemCount: 0,
page: 1,
pageSize: 10,
@ -398,7 +443,8 @@ export default {
actions: [],
formModel: {},
confirmDirty: false,
firstIndex: 0
firstIndex: 0,
modalWidth: '30vw'
}
},
beforeCreate () {
@ -416,11 +462,74 @@ export default {
return
}
}
if ((this.$route.path.includes('/publicip/') && ['firewall', 'portforwarding', 'loadbalancing'].includes(this.$route.query.tab)) ||
(this.$route.path.includes('/guestnetwork/') && (this.$route.query.tab === 'egress.rules' || this.$route.query.tab === 'public.ip.addresses'))) {
return
}
if (this.$route.path.includes('/template/') || this.$route.path.includes('/iso/')) {
return
}
this.fetchData()
})
eventBus.$on('exec-action', (action, isGroupAction) => {
this.execAction(action, isGroupAction)
})
eventBus.$on('update-bulk-job-status', (items, action) => {
for (const item of items) {
this.$store.getters.asyncJobIds.map(function (j) {
if (j.jobid === item.jobid) {
j.bulkAction = action
}
})
}
})
eventBus.$on('update-job-details', (jobId, resourceId) => {
const fullPath = this.$route.fullPath
const path = this.$route.path
var jobs = this.$store.getters.asyncJobIds.map(job => {
if (job.jobid === jobId) {
if (resourceId && !path.includes(resourceId)) {
job.path = path + '/' + resourceId
} else {
job.path = fullPath
}
}
return job
})
this.$store.commit('SET_ASYNC_JOB_IDS', jobs)
})
eventBus.$on('update-resource-state', (selectedItems, resource, state, jobid) => {
if (selectedItems.length === 0) {
return
}
var tempResource = []
if (selectedItems && resource) {
if (resource.includes(',')) {
resource = resource.split(',')
tempResource = resource
} else {
tempResource.push(resource)
}
for (var r = 0; r < tempResource.length; r++) {
var objIndex = 0
if (this.$route.path.includes('/template') || this.$route.path.includes('/iso')) {
objIndex = selectedItems.findIndex(obj => (obj.zoneid === tempResource[r]))
} else {
objIndex = selectedItems.findIndex(obj => (obj.id === tempResource[r] || obj.username === tempResource[r]))
}
if (state && objIndex !== -1) {
selectedItems[objIndex].status = state
}
if (jobid && objIndex !== -1) {
selectedItems[objIndex].jobid = jobid
}
}
}
})
if (this.device === 'desktop') {
this.pageSize = 20
@ -465,7 +574,32 @@ export default {
this.fetchData()
}
},
computed: {
hasSelected () {
return this.selectedRowKeys.length > 0
}
},
methods: {
getStyle () {
if (['snapshot', 'vmsnapshot', 'publicip'].includes(this.$route.name)) {
return 'table-cell'
}
return 'inline-flex'
},
getOkProps () {
if (this.selectedRowKeys.length > 0 && this.currentAction?.groupAction) {
return { props: { type: 'default' } }
} else {
return { props: { type: 'primary' } }
}
},
getCancelProps () {
if (this.selectedRowKeys.length > 0 && this.currentAction?.groupAction) {
return { props: { type: 'primary' } }
} else {
return { props: { type: 'default' } }
}
},
switchProject (projectId) {
if (!projectId || !projectId.length || projectId.length !== 36) {
return
@ -600,6 +734,12 @@ export default {
sorter: function (a, b) { return genericCompare(a[this.dataIndex] || '', b[this.dataIndex] || '') }
})
}
this.chosenColumns = this.columns.filter(column => {
return ![this.$t('label.state'), this.$t('label.hostname'), this.$t('label.hostid'), this.$t('label.zonename'),
this.$t('label.zone'), this.$t('label.zoneid'), this.$t('label.ip'), this.$t('label.ipaddress'), this.$t('label.privateip'),
this.$t('label.linklocalip'), this.$t('label.size'), this.$t('label.sizegb'), this.$t('label.current'),
this.$t('label.created'), this.$t('label.order')].includes(column.title)
})
if (['listTemplates', 'listIsos'].includes(this.apiName) && this.dataView) {
delete params.showunique
@ -716,6 +856,14 @@ export default {
},
onRowSelectionChange (selection) {
this.selectedRowKeys = selection
if (selection?.length > 0) {
this.modalWidth = '50vw'
this.selectedItems = (this.items.filter(function (item) {
return selection.indexOf(item.id) !== -1
}))
} else {
this.modalWidth = '30vw'
}
},
execAction (action, isGroupAction) {
const self = this
@ -860,12 +1008,16 @@ export default {
}).then(function () {
})
},
pollActionCompletion (jobId, action, resourceName, showLoading = true) {
pollActionCompletion (jobId, action, resourceName, resource, showLoading = true) {
eventBus.$emit('update-job-details', jobId, resource)
this.$pollJob({
jobId,
name: resourceName,
successMethod: result => {
this.fetchData()
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, resource, 'success')
}
if (action.response) {
const description = action.response(result.jobresult)
if (description) {
@ -880,11 +1032,17 @@ export default {
action.successMethod(this, result)
}
},
errorMethod: () => this.fetchData(),
errorMethod: () => {
this.fetchData()
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, resource, 'failed')
}
},
loadingMessage: `${this.$t(action.label)} - ${resourceName}`,
showLoading: showLoading,
catchMessage: this.$t('error.fetching.async.job.result'),
action
action,
bulkAction: `${this.selectedItems.length > 0}` && this.showGroupActionModal
})
},
fillEditFormFieldValues () {
@ -904,8 +1062,33 @@ export default {
}
})
},
handleCancel () {
eventBus.$emit('update-bulk-job-status', this.selectedItems, false)
this.showGroupActionModal = false
this.selectedItems = []
this.selectedColumns = []
this.selectedRowKeys = []
this.message = {}
},
handleSubmit (e) {
if (!this.dataView && this.currentAction.groupAction && this.selectedRowKeys.length > 0) {
if (this.selectedRowKeys.length > 0) {
this.selectedColumns = this.chosenColumns
this.selectedItems = this.selectedItems.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.currentAction.label
this.modalInfo.docHelp = this.currentAction.docHelp
}
this.form.validateFields((err, values) => {
if (!err) {
this.actionLoading = true
@ -913,9 +1096,9 @@ export default {
this.items.map(x => {
itemsNameMap[x.id] = x.name || x.displaytext || x.id
})
const paramsList = this.currentAction.groupMap(this.selectedRowKeys, values)
const paramsList = this.currentAction.groupMap(this.selectedRowKeys, values, this.items)
for (const params of paramsList) {
var resourceName = itemsNameMap[params.id]
var resourceName = itemsNameMap[params.id || params.vmsnapshotid || params.username || params.name]
// Using a method for this since it's an async call and don't want wrong prarms to be passed
this.callGroupApi(params, resourceName)
}
@ -938,23 +1121,44 @@ export default {
callGroupApi (params, resourceName) {
const action = this.currentAction
api(action.api, params).then(json => {
this.handleResponse(json, resourceName, action, false)
this.handleResponse(json, resourceName, this.getDataIdentifier(params), action, false)
}).catch(error => {
if ([401].includes(error.response.status)) {
return
}
this.$notifyError(error)
if (this.selectedItems.length !== 0) {
this.$notifyError(error)
eventBus.$emit('update-resource-state', this.selectedItems, this.getDataIdentifier(params), 'failed')
}
})
},
handleResponse (response, resourceName, action, showLoading = true) {
getDataIdentifier (params) {
var dataIdentifier = ''
dataIdentifier = params.id || params.username || params.name || params.vmsnapshotid || params.ids
return dataIdentifier
},
handleResponse (response, resourceName, resource, action, showLoading = true) {
for (const obj in response) {
if (obj.includes('response')) {
if (response[obj].jobid) {
const jobid = response[obj].jobid
this.$store.dispatch('AddAsyncJob', { title: this.$t(action.label), jobid: jobid, description: resourceName, status: 'progress' })
this.pollActionCompletion(jobid, action, resourceName, showLoading)
this.$store.dispatch('AddAsyncJob', {
title: this.$t(action.label),
jobid: jobid,
description: resourceName,
status: 'progress',
bulkAction: this.selectedItems.length > 0 && this.showGroupActionModal
})
eventBus.$emit('update-resource-state', this.selectedItems, resource, 'InProgress', jobid)
this.pollActionCompletion(jobid, action, resourceName, resource, showLoading)
return true
} else {
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, resource, 'success')
if (resource) {
this.selectedItems.filter(item => item === resource)
}
}
var message = action.successMessage ? this.$t(action.successMessage) : this.$t(action.label) +
(resourceName ? ' - ' + resourceName : '')
var duration = 2
@ -1049,7 +1253,7 @@ export default {
args = [action.api, params]
}
api(...args).then(json => {
hasJobId = this.handleResponse(json, resourceName, action)
hasJobId = this.handleResponse(json, resourceName, this.getDataIdentifier(params), action)
if ((action.icon === 'delete' || ['archiveEvents', 'archiveAlerts', 'unmanageVirtualMachine'].includes(action.api)) && this.dataView) {
this.$router.go(-1)
} else {
@ -1064,6 +1268,7 @@ export default {
}
console.log(error)
eventBus.$emit('update-resource-state', this.selectedItems, this.getDataIdentifier(params), 'failed')
this.$notifyError(error)
}).finally(f => {
this.actionLoading = false

View File

@ -100,6 +100,7 @@
<script>
import { api } from '@/api'
import eventBus from '@/config/eventBus'
import TooltipLabel from '@/components/widgets/TooltipLabel'
export default {
@ -238,6 +239,8 @@ export default {
},
response: (result) => { return result.virtualmachine && result.virtualmachine.password ? `The password of VM <b>${result.virtualmachine.displayname}</b> is <b>${result.virtualmachine.password}</b>` : null }
})
const resourceId = this.resource.id
eventBus.$emit('update-job-details', jobId, resourceId)
this.closeAction()
}).catch(error => {
this.$notifyError(error)

View File

@ -17,6 +17,14 @@
<template>
<div>
<a-button
v-if="(('deleteIso' in $store.getters.apis) && this.selectedItems.length > 0)"
type="danger"
icon="plus"
style="width: 100%; margin-bottom: 15px"
@click="bulkActionConfirmation()">
{{ $t(message.title) }}
</a-button>
<a-table
size="small"
style="overflow-y: auto"
@ -24,6 +32,7 @@
:columns="columns"
:dataSource="dataSource"
:pagination="false"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
:rowKey="record => record.zoneid">
<div slot="isready" slot-scope="text, record">
<span v-if="record.isready">{{ $t('label.yes') }}</span>
@ -118,17 +127,35 @@
</a-form>
</a-spin>
</a-modal>
<bulk-action-view
v-if="showConfirmationAction || showGroupActionModal"
:showConfirmationAction="showConfirmationAction"
:showGroupActionModal="showGroupActionModal"
:items="dataSource"
:selectedRowKeys="selectedRowKeys"
:selectedItems="selectedItems"
:columns="columns"
:selectedColumns="selectedColumns"
action="deleteIso"
:loading="loading"
:message="message"
@group-action="deleteIsos"
@handle-cancel="handleCancel"
@close-modal="closeModal" />
</div>
</template>
<script>
import { api } from '@/api'
import TooltipButton from '@/components/widgets/TooltipButton'
import BulkActionView from '@/components/view/BulkActionView'
import eventBus from '@/config/eventBus'
export default {
name: 'IsoZones',
components: {
TooltipButton
TooltipButton,
BulkActionView
},
props: {
resource: {
@ -153,7 +180,18 @@ export default {
zones: [],
zoneLoading: false,
copyLoading: false,
deleteLoading: false
deleteLoading: false,
selectedRowKeys: [],
showGroupActionModal: false,
selectedItems: [],
selectedColumns: [],
filterColumns: ['Status', 'Ready'],
showConfirmationAction: false,
message: {
title: this.$t('label.action.bulk.delete.isos'),
confirmMessage: this.$t('label.confirm.delete.isos')
},
modalWidth: '30vw'
}
},
beforeCreate () {
@ -238,6 +276,56 @@ export default {
(this.resource.isready || !this.resource.status || this.resource.status.indexOf('Downloaded') === -1) && // Iso is ready or downloaded
this.resource.account !== 'system'
},
setSelection (selection) {
this.selectedRowKeys = selection
this.$emit('selection-change', this.selectedRowKeys)
this.selectedItems = (this.dataSource.filter(function (item) {
return selection.indexOf(item.zoneid) !== -1
}))
},
resetSelection () {
this.setSelection([])
},
onSelectChange (selectedRowKeys, selectedRows) {
this.setSelection(selectedRowKeys)
},
bulkActionConfirmation () {
this.showConfirmationAction = true
this.selectedColumns = this.columns.filter(column => {
return !this.filterColumns.includes(column.title)
})
this.selectedItems = this.selectedItems.map(v => ({ ...v, status: 'InProgress' }))
},
handleCancel () {
eventBus.$emit('update-bulk-job-status', this.selectedItems, false)
this.showGroupActionModal = false
this.selectedItems = []
this.selectedColumns = []
this.selectedRowKeys = []
this.fetchData()
if (this.dataSource.length === 0) {
this.$router.go(-1)
}
},
deleteIsos (e) {
this.showConfirmationAction = false
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' }
]
})
if (this.selectedRowKeys.length > 0) {
this.showGroupActionModal = true
}
for (const iso of this.selectedItems) {
this.deleteIso(iso)
}
},
deleteIso (record) {
const params = {
id: record.id,
@ -250,21 +338,39 @@ export default {
title: this.$t('label.action.delete.iso'),
jobid: jobId,
description: this.resource.name,
status: 'progress'
status: 'progress',
bulkAction: this.selectedItems.length > 0 && this.showGroupActionModal
})
eventBus.$emit('update-job-details', jobId, null)
const singleZone = (this.dataSource.length === 1)
this.$pollJob({
jobId,
successMethod: result => {
if (singleZone) {
this.$router.go(-1)
if (this.selectedItems.length === 0) {
this.$router.go(-1)
}
} else {
this.fetchData()
if (this.selectedItems.length === 0) {
this.fetchData()
}
}
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, record.zoneid, 'success')
}
},
errorMethod: () => this.fetchData(),
errorMethod: () => {
if (this.selectedItems.length === 0) {
this.fetchData()
}
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, record.zoneid, 'failed')
}
},
showLoading: !(this.selectedItems.length > 0 && this.showGroupActionModal),
loadingMessage: `${this.$t('label.deleting.iso')} ${this.resource.name} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result')
catchMessage: this.$t('error.fetching.async.job.result'),
bulkAction: this.selectedItems.length > 0 && this.showGroupActionModal
})
}).catch(error => {
this.$notifyError(error)
@ -315,6 +421,7 @@ export default {
description: this.resource.name,
status: 'progress'
})
eventBus.$emit('update-job-details', jobId, null)
this.$pollJob({
jobId,
successMethod: result => {
@ -336,6 +443,9 @@ export default {
this.fetchData()
})
})
},
closeModal () {
this.showConfirmationAction = false
}
}
}

View File

@ -17,6 +17,14 @@
<template>
<div>
<a-button
v-if="(('deleteTemplate' in $store.getters.apis) && this.selectedRowKeys.length > 0)"
type="danger"
icon="plus"
style="width: 100%; margin-bottom: 15px"
@click="bulkActionConfirmation()">
{{ $t('label.action.bulk.delete.templates') }}
</a-button>
<a-table
size="small"
style="overflow-y: auto"
@ -24,6 +32,7 @@
:columns="columns"
:dataSource="dataSource"
:pagination="false"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
:rowKey="record => record.zoneid">
<div slot="isready" slot-scope="text, record">
<span v-if="record.isready">{{ $t('label.yes') }}</span>
@ -110,34 +119,65 @@
</a-modal>
<a-modal
:title="$t('label.action.delete.template')"
:title="selectedItems.length > 0 && showTable ? $t(message.title) : $t('label.action.delete.template')"
:visible="showDeleteTemplate"
:closable="true"
:maskClosable="false"
:okText="$t('label.ok')"
:cancelText="$t('label.cancel')"
@ok="deleteTemplate"
:width="showTable ? modalWidth : '30vw'"
@ok="selectedItems.length > 0 ? deleteTemplates() : deleteTemplate(currentRecord)"
@cancel="onCloseModal"
:ok-button-props="getOkProps()"
:cancel-button-props="getCancelProps()"
:confirmLoading="deleteLoading"
centered>
<div v-if="selectedRowKeys.length > 0">
<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(message.confirmMessage)" />
</a-alert>
</div>
<a-alert v-else :message="$t('message.action.delete.template')" type="warning" />
<br />
<a-table
v-if="selectedRowKeys.length > 0 && showTable"
size="middle"
:columns="selectedColumns"
:dataSource="selectedItems"
:rowKey="(record, idx) => record.zoneid || record.name"
:pagination="true"
style="overflow-y: auto">
</a-table>
<a-spin :spinning="deleteLoading">
<a-alert :message="$t('message.action.delete.template')" type="warning" />
<a-form-item :label="$t('label.isforced')" style="margin-bottom: 0;">
<a-switch v-model="forcedDelete"></a-switch>
</a-form-item>
</a-spin>
</a-modal>
<bulk-action-progress
:showGroupActionModal="showGroupActionModal"
:selectedItems="selectedItems"
:selectedColumns="selectedColumns"
:message="message"
@handle-cancel="handleCancel" />
</div>
</template>
<script>
import { api } from '@/api'
import TooltipButton from '@/components/widgets/TooltipButton'
import BulkActionProgress from '@/components/view/BulkActionProgress'
import Status from '@/components/widgets/Status'
import eventBus from '@/config/eventBus'
export default {
name: 'TemplateZones',
components: {
TooltipButton
TooltipButton,
BulkActionProgress,
Status
},
props: {
resource: {
@ -164,7 +204,19 @@ export default {
copyLoading: false,
deleteLoading: false,
showDeleteTemplate: false,
forcedDelete: false
forcedDelete: false,
selectedRowKeys: [],
showGroupActionModal: false,
selectedItems: [],
selectedColumns: [],
filterColumns: ['Status', 'Ready'],
showConfirmationAction: false,
message: {
title: this.$t('label.action.bulk.delete.templates'),
confirmMessage: this.$t('label.confirm.delete.templates')
},
modalWidth: '30vw',
showTable: false
}
},
beforeCreate () {
@ -206,7 +258,7 @@ export default {
},
watch: {
loading (newData, oldData) {
if (!newData) {
if (!newData && !this.showGroupActionModal) {
this.fetchData()
}
}
@ -249,11 +301,83 @@ export default {
(this.resource.isready || !this.resource.status || this.resource.status.indexOf('Downloaded') === -1) && // Template is ready or downloaded
this.resource.templatetype !== 'SYSTEM'
},
deleteTemplate () {
setSelection (selection) {
this.selectedRowKeys = selection
if (selection?.length > 0) {
this.modalWidth = '50vw'
this.$emit('selection-change', this.selectedRowKeys)
this.selectedItems = (this.dataSource.filter(function (item) {
return selection.indexOf(item.zoneid) !== -1
}))
} else {
this.modalWidth = '30vw'
this.selectedItems = []
}
},
resetSelection () {
this.setSelection([])
},
onSelectChange (selectedRowKeys, selectedRows) {
this.setSelection(selectedRowKeys)
},
bulkActionConfirmation () {
this.showConfirmationAction = true
this.selectedColumns = this.columns.filter(column => {
return !this.filterColumns.includes(column.title)
})
this.selectedItems = this.selectedItems.map(v => ({ ...v, status: 'InProgress' }))
this.onShowDeleteModal(this.selectedItems[0])
},
handleCancel () {
eventBus.$emit('update-bulk-job-status', this.selectedItems, false)
this.showGroupActionModal = false
this.selectedItems = []
this.selectedColumns = []
this.selectedRowKeys = []
this.showTable = false
this.fetchData()
if (this.dataSource.length === 0) {
this.$router.go(-1)
}
},
getOkProps () {
if (this.selectedRowKeys.length > 0) {
return { props: { type: 'default' } }
} else {
return { props: { type: 'primary' } }
}
},
getCancelProps () {
if (this.selectedRowKeys.length > 0) {
return { props: { type: 'primary' } }
} else {
return { props: { type: 'default' } }
}
},
deleteTemplates (e) {
this.showConfirmationAction = false
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' }
]
})
if (this.selectedRowKeys.length > 0 && this.showTable) {
this.showGroupActionModal = true
}
for (const template of this.selectedItems) {
this.deleteTemplate(template)
}
},
deleteTemplate (template) {
const params = {
id: this.currentRecord.id,
id: template.id,
forced: this.forcedDelete,
zoneid: this.currentRecord.zoneid
zoneid: template.zoneid
}
this.deleteLoading = true
api('deleteTemplate', params).then(json => {
@ -262,8 +386,10 @@ export default {
title: this.$t('label.action.delete.template'),
jobid: jobId,
description: this.resource.name,
status: 'progress'
status: 'progress',
bulkAction: this.selectedItems.length > 0 && this.showGroupActionModal
})
eventBus.$emit('update-job-details', jobId, null)
const singleZone = (this.dataSource.length === 1)
this.$pollJob({
jobId,
@ -271,18 +397,36 @@ export default {
if (singleZone) {
const isResourcePage = (this.$route.params && this.$route.params.id)
if (isResourcePage) {
this.$router.go(-1)
if (this.selectedItems.length === 0 && !this.showGroupActionModal) {
this.$router.push({ path: '/template' })
}
}
} else {
this.fetchData()
if (this.selectedItems.length === 0) {
this.fetchData()
}
}
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, template.zoneid, 'success')
}
},
errorMethod: () => this.fetchData(),
errorMethod: () => {
if (this.selectedItems.length === 0) {
this.fetchData()
}
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, template.zoneid, 'failed')
}
},
showLoading: !(this.selectedItems.length > 0 && this.showGroupActionModal),
loadingMessage: `${this.$t('label.deleting.template')} ${this.resource.name} ${this.$t('label.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result')
catchMessage: this.$t('error.fetching.async.job.result'),
bulkAction: this.selectedItems.length > 0 && this.showGroupActionModal
})
this.onCloseModal()
this.fetchData()
if (this.selectedItems.length === 0) {
this.fetchData()
}
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
@ -311,11 +455,19 @@ export default {
this.forcedDelete = false
this.currentRecord = record
this.showDeleteTemplate = true
if (this.showConfirmationAction) {
this.showTable = true
} else {
this.selectedItems = []
}
},
onCloseModal () {
this.currentRecord = {}
this.showCopyActionForm = false
this.showDeleteTemplate = false
this.showConfirmationAction = false
this.showTable = false
this.selectedRowKeys = []
},
handleCopyTemplateSubmit (e) {
e.preventDefault()
@ -337,6 +489,7 @@ export default {
description: this.resource.name,
status: 'progress'
})
eventBus.$emit('update-job-details', jobId, null)
this.$pollJob({
jobId,
successMethod: result => {

View File

@ -59,7 +59,14 @@
</div>
<a-divider/>
<a-button
v-if="(('deleteEgressFirewallRule' in $store.getters.apis) && this.selectedRowKeys.length > 0)"
type="danger"
icon="plus"
style="width: 100%; margin-bottom: 15px"
@click="bulkActionConfirmation()">
{{ $t('label.action.bulk.delete.egress.firewall.rules') }}
</a-button>
<a-table
size="small"
style="overflow-y: auto"
@ -67,6 +74,7 @@
:columns="columns"
:dataSource="egressRules"
:pagination="false"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
:rowKey="record => record.id">
<template slot="protocol" slot-scope="record">
{{ record.protocol | capitalise }}
@ -97,17 +105,37 @@
</template>
</a-pagination>
<bulk-action-view
v-if="showConfirmationAction || showGroupActionModal"
:showConfirmationAction="showConfirmationAction"
:showGroupActionModal="showGroupActionModal"
:items="egressRules"
:selectedRowKeys="selectedRowKeys"
:selectedItems="selectedItems"
:columns="columns"
:selectedColumns="selectedColumns"
action="deleteEgressFirewallRule"
:loading="loading"
:message="message"
@group-action="deleteRules"
@handle-cancel="handleCancel"
@close-modal="closeModal" />
</div>
</template>
<script>
import { api } from '@/api'
import Status from '@/components/widgets/Status'
import TooltipButton from '@/components/widgets/TooltipButton'
import BulkActionView from '@/components/view/BulkActionView'
import eventBus from '@/config/eventBus'
export default {
name: 'EgressRulesTab',
components: {
TooltipButton
Status,
TooltipButton,
BulkActionView
},
props: {
resource: {
@ -117,6 +145,16 @@ export default {
},
data () {
return {
selectedRowKeys: [],
showGroupActionModal: false,
selectedItems: [],
selectedColumns: [],
filterColumns: ['Action'],
showConfirmationAction: false,
message: {
title: this.$t('label.action.bulk.delete.egress.firewall.rules'),
confirmMessage: this.$t('label.confirm.delete.egress.firewall.rules')
},
loading: true,
egressRules: [],
newRule: {
@ -160,6 +198,11 @@ export default {
]
}
},
computed: {
hasSelected () {
return this.selectedRowKeys.length > 0
}
},
created () {
this.fetchData()
},
@ -178,6 +221,7 @@ export default {
this.fetchData()
}
},
inject: ['parentFetchData'],
methods: {
fetchData () {
this.loading = true
@ -193,22 +237,91 @@ export default {
this.loading = false
})
},
setSelection (selection) {
this.selectedRowKeys = selection
this.$emit('selection-change', this.selectedRowKeys)
this.selectedItems = (this.egressRules.filter(function (item) {
return selection.indexOf(item.id) !== -1
}))
},
resetSelection () {
this.setSelection([])
},
onSelectChange (selectedRowKeys, selectedRows) {
this.setSelection(selectedRowKeys)
},
bulkActionConfirmation () {
this.showConfirmationAction = true
this.selectedColumns = this.columns.filter(column => {
return !this.filterColumns.includes(column.title)
})
this.selectedItems = this.selectedItems.map(v => ({ ...v, status: 'InProgress' }))
},
handleCancel () {
eventBus.$emit('update-bulk-job-status', this.selectedItems, false)
this.showGroupActionModal = false
this.selectedItems = []
this.selectedColumns = []
this.selectedRowKeys = []
this.parentFetchData()
},
deleteRules (e) {
this.showConfirmationAction = false
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' }
]
})
if (this.selectedRowKeys.length > 0) {
this.showGroupActionModal = true
}
for (const rule of this.selectedItems) {
this.deleteRule(rule)
}
},
deleteRule (rule) {
this.loading = true
api('deleteEgressFirewallRule', { id: rule.id }).then(response => {
const jobId = response.deleteegressfirewallruleresponse.jobid
this.$store.dispatch('AddAsyncJob', {
title: this.$t('label.action.delete.egress.firewall'),
jobid: jobId,
description: rule.id,
status: 'progress',
bulkAction: this.selectedItems.length > 0 && this.showGroupActionModal
})
eventBus.$emit('update-job-details', jobId, null)
this.$pollJob({
jobId: response.deleteegressfirewallruleresponse.jobid,
jobId: jobId,
successMessage: this.$t('message.success.remove.egress.rule'),
successMethod: () => this.fetchData(),
successMethod: () => {
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, rule.id, 'success')
}
this.fetchData()
},
errorMessage: this.$t('message.remove.egress.rule.failed'),
errorMethod: () => this.fetchData(),
errorMethod: () => {
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, rule.id, 'failed')
}
this.fetchData()
},
loadingMessage: this.$t('message.remove.egress.rule.processing'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => this.fetchData()
catchMethod: () => this.fetchData(),
bulkAction: `${this.selectedItems.length > 0}` && this.showGroupActionModal
})
}).catch(error => {
this.$notifyError(error)
this.fetchData()
}).finally(() => {
this.loading = false
})
},
addRule () {
@ -252,6 +365,9 @@ export default {
this.newRule.startport = null
this.newRule.endport = null
},
closeModal () {
this.showConfirmationAction = false
},
handleChangePage (page, pageSize) {
this.page = page
this.pageSize = pageSize

View File

@ -54,7 +54,14 @@
</div>
<a-divider/>
<a-button
v-if="(('deleteFirewallRule' in $store.getters.apis) && this.selectedItems.length > 0)"
type="danger"
icon="plus"
style="width: 100%; margin-bottom: 15px"
@click="bulkActionConfirmation()">
{{ $t('label.action.bulk.delete.firewall.rules') }}
</a-button>
<a-table
size="small"
style="overflow-y: auto"
@ -62,6 +69,7 @@
:columns="columns"
:dataSource="firewallRules"
:pagination="false"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
:rowKey="record => record.id">
<template slot="protocol" slot-scope="record">
{{ record.protocol | capitalise }}
@ -132,16 +140,36 @@
<a-button class="add-tags-done" @click="tagsModalVisible = false" type="primary">{{ $t('label.done') }}</a-button>
</a-modal>
<bulk-action-view
v-if="showConfirmationAction || showGroupActionModal"
:showConfirmationAction="showConfirmationAction"
:showGroupActionModal="showGroupActionModal"
:items="firewallRules"
:selectedRowKeys="selectedRowKeys"
:selectedItems="selectedItems"
:columns="columns"
:selectedColumns="selectedColumns"
action="deleteFirewallRule"
:loading="loading"
:message="message"
@group-action="deleteRules"
@handle-cancel="handleCancel"
@close-modal="closeModal" />
</div>
</template>
<script>
import { api } from '@/api'
import Status from '@/components/widgets/Status'
import TooltipButton from '@/components/widgets/TooltipButton'
import BulkActionView from '@/components/view/BulkActionView'
import eventBus from '@/config/eventBus'
export default {
components: {
TooltipButton
Status,
TooltipButton,
BulkActionView
},
props: {
resource: {
@ -152,6 +180,16 @@ export default {
inject: ['parentFetchData', 'parentToggleLoading'],
data () {
return {
selectedRowKeys: [],
showGroupActionModal: false,
selectedItems: [],
selectedColumns: [],
filterColumns: ['State', 'Action'],
showConfirmationAction: false,
message: {
title: this.$t('label.action.bulk.delete.firewall.rules'),
confirmMessage: this.$t('label.confirm.delete.firewall.rules')
},
loading: true,
addTagLoading: false,
firewallRules: [],
@ -202,6 +240,11 @@ export default {
]
}
},
computed: {
hasSelected () {
return this.selectedRowKeys.length > 0
}
},
created () {
this.fetchData()
},
@ -237,18 +280,85 @@ export default {
this.loading = false
})
},
setSelection (selection) {
this.selectedRowKeys = selection
this.$emit('selection-change', this.selectedRowKeys)
this.selectedItems = (this.firewallRules.filter(function (item) {
return selection.indexOf(item.id) !== -1
}))
},
resetSelection () {
this.setSelection([])
},
onSelectChange (selectedRowKeys, selectedRows) {
this.setSelection(selectedRowKeys)
},
bulkActionConfirmation () {
this.showConfirmationAction = true
this.selectedColumns = this.columns.filter(column => {
return !this.filterColumns.includes(column.title)
})
this.selectedItems = this.selectedItems.map(v => ({ ...v, status: 'InProgress' }))
},
handleCancel () {
eventBus.$emit('update-bulk-job-status', this.selectedItems, false)
this.showGroupActionModal = false
this.selectedItems = []
this.selectedColumns = []
this.selectedRowKeys = []
this.parentFetchData()
},
deleteRules (e) {
this.showConfirmationAction = false
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' }
]
})
if (this.selectedRowKeys.length > 0) {
this.showGroupActionModal = true
}
for (const rule of this.selectedItems) {
this.deleteRule(rule)
}
},
deleteRule (rule) {
this.loading = true
api('deleteFirewallRule', { id: rule.id }).then(response => {
const jobId = response.deletefirewallruleresponse.jobid
this.$store.dispatch('AddAsyncJob', {
title: this.$t('label.action.delete.firewall'),
jobid: jobId,
description: rule.id,
status: 'progress',
bulkAction: this.selectedItems.length > 0 && this.showGroupActionModal
})
eventBus.$emit('update-job-details', jobId, null)
this.$pollJob({
jobId: response.deletefirewallruleresponse.jobid,
jobId: jobId,
successMessage: this.$t('message.success.remove.firewall.rule'),
successMethod: () => this.fetchData(),
successMethod: () => {
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, rule.id, 'success')
}
this.fetchData()
},
errorMessage: this.$t('message.remove.firewall.rule.failed'),
errorMethod: () => this.fetchData(),
errorMethod: () => {
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, rule.id, 'failed')
}
this.fetchData()
},
loadingMessage: this.$t('message.remove.firewall.rule.processing'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => this.fetchData()
catchMethod: () => this.fetchData(),
bulkAction: `${this.selectedItems.length > 0}` && this.showGroupActionModal
})
}).catch(error => {
this.$notifyError(error)
@ -300,6 +410,7 @@ export default {
this.tagsModalVisible = false
this.newTag.key = null
this.newTag.value = null
this.showConfirmationAction = false
},
openTagsModal (id) {
this.selectedRule = id

View File

@ -26,6 +26,14 @@
@click="onShowAcquireIp">
{{ $t('label.acquire.new.ip') }}
</a-button>
<a-button
v-if="(('disassociateIpAddress' in $store.getters.apis) && this.selectedRowKeys.length > 0)"
type="danger"
icon="plus"
style="width: 100%; margin-bottom: 15px"
@click="bulkActionConfirmation()">
{{ $t('label.action.bulk.release.public.ip.address') }}
</a-button>
<div v-if="$route.path.startsWith('/vpc')">
Select Tier:
<a-select
@ -50,6 +58,7 @@
:columns="columns"
:dataSource="ips"
:rowKey="item => item.id"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
:pagination="false" >
<template slot="ipaddress" slot-scope="text, record">
<router-link v-if="record.forvirtualnetwork === true" :to="{ path: '/publicip/' + record.id }" >{{ text }} </router-link>
@ -124,18 +133,36 @@
</a-form-item>
</a-spin>
</a-modal>
<bulk-action-view
v-if="showConfirmationAction || showGroupActionModal"
:showConfirmationAction="showConfirmationAction"
:showGroupActionModal="showGroupActionModal"
:items="ips"
:selectedRowKeys="selectedRowKeys"
:selectedItems="selectedItems"
:columns="columns"
:selectedColumns="selectedColumns"
action="disassociateIpAddress"
:loading="loading"
:message="message"
@group-action="releaseIpAddresses"
@handle-cancel="handleCancel"
@close-modal="closeModal" />
</div>
</template>
<script>
import { api } from '@/api'
import Status from '@/components/widgets/Status'
import TooltipButton from '@/components/widgets/TooltipButton'
import BulkActionView from '@/components/view/BulkActionView'
import eventBus from '@/config/eventBus'
export default {
name: 'IpAddressesTab',
components: {
Status,
TooltipButton
TooltipButton,
BulkActionView
},
props: {
resource: {
@ -159,6 +186,16 @@ export default {
pageSize: 10,
totalIps: 0,
tiersSelect: false,
selectedRowKeys: [],
showGroupActionModal: false,
selectedItems: [],
selectedColumns: [],
filterColumns: ['Action'],
showConfirmationAction: false,
message: {
title: this.$t('label.action.bulk.release.public.ip.address'),
confirmMessage: this.$t('label.confirm.release.public.ip.addresses')
},
columns: [
{
title: this.$t('label.ipaddress'),
@ -202,6 +239,7 @@ export default {
this.fetchData()
}
},
inject: ['parentFetchData'],
methods: {
fetchData () {
const params = {
@ -250,6 +288,19 @@ export default {
this.vpcTier = tier
this.fetchData()
},
setSelection (selection) {
this.selectedRowKeys = selection
this.$emit('selection-change', this.selectedRowKeys)
this.selectedItems = (this.ips.filter(function (item) {
return selection.indexOf(item.id) !== -1
}))
},
resetSelection () {
this.setSelection([])
},
onSelectChange (selectedRowKeys, selectedRows) {
this.setSelection(selectedRowKeys)
},
changePage (page, pageSize) {
this.page = page
this.pageSize = pageSize
@ -260,6 +311,13 @@ export default {
this.pageSize = pageSize
this.fetchData()
},
bulkActionConfirmation () {
this.showConfirmationAction = true
this.selectedColumns = this.columns.filter(column => {
return !this.filterColumns.includes(column.title)
})
this.selectedItems = this.selectedItems.map(v => ({ ...v, status: 'InProgress' }))
},
acquireIpAddress () {
const params = {}
if (this.$route.path.startsWith('/vpc')) {
@ -298,23 +356,66 @@ export default {
this.acquireLoading = false
})
},
handleCancel () {
eventBus.$emit('update-bulk-job-status', this.selectedItems, false)
this.showGroupActionModal = false
this.selectedItems = []
this.selectedColumns = []
this.selectedRowKeys = []
this.parentFetchData()
},
releaseIpAddresses (e) {
this.showConfirmationAction = false
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' }
]
})
if (this.selectedRowKeys.length > 0) {
this.showGroupActionModal = true
}
for (const ip of this.selectedItems) {
this.releaseIpAddress(ip)
}
},
releaseIpAddress (ip) {
this.fetchLoading = true
api('disassociateIpAddress', {
id: ip.id
}).then(response => {
const jobId = response.disassociateipaddressresponse.jobid
this.$store.dispatch('AddAsyncJob', {
title: this.$t('label.action.release.ip'),
jobid: jobId,
description: ip.id,
status: 'progress',
bulkAction: this.selectedItems.length > 0 && this.showGroupActionModal
})
eventBus.$emit('update-job-details', jobId, null)
this.$pollJob({
jobId: response.disassociateipaddressresponse.jobid,
jobId: jobId,
successMessage: this.$t('message.success.release.ip'),
successMethod: () => {
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, ip.id, 'success')
}
this.fetchData()
},
errorMessage: this.$t('message.release.ip.failed'),
errorMethod: () => {
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, ip.id, 'failed')
}
this.fetchData()
},
loadingMessage: `${this.$t('label.releasing.ip')} ${this.$t('label.for')} ${this.resource.name} ${this.$t('label.is.in.progress')}`,
catchMessage: this.$t('error.fetching.async.job.result')
catchMessage: this.$t('error.fetching.async.job.result'),
bulkAction: `${this.selectedItems.length > 0}` && this.showGroupActionModal
})
}).catch(error => {
this.fetchLoading = false
@ -353,6 +454,9 @@ export default {
},
onCloseModal () {
this.showAcquireIp = false
},
closeModal () {
this.showConfirmationAction = false
}
}
}

View File

@ -62,7 +62,14 @@
</div>
<a-divider />
<a-button
v-if="(('deleteLoadBalancerRule' in $store.getters.apis) && this.selectedItems.length > 0)"
type="danger"
icon="plus"
style="width: 100%; margin-bottom: 15px"
@click="bulkActionConfirmation()">
{{ $t('label.action.bulk.delete.load.balancer.rules') }}
</a-button>
<a-table
size="small"
class="list-view"
@ -70,6 +77,7 @@
:columns="columns"
:dataSource="lbRules"
:pagination="false"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
:rowKey="record => record.id">
<template slot="algorithm" slot-scope="record">
{{ returnAlgorithmName(record.algorithm) }}
@ -375,9 +383,24 @@
</template>
</a-pagination>
</div>
</a-modal>
<bulk-action-view
v-if="showConfirmationAction || showGroupActionModal"
:showConfirmationAction="showConfirmationAction"
:showGroupActionModal="showGroupActionModal"
:items="lbRules"
:selectedRowKeys="selectedRowKeys"
:selectedItems="selectedItems"
:columns="columns"
:selectedColumns="selectedColumns"
:filterColumns="filterColumns"
action="deleteLoadBalancerRule"
:loading="loading"
:message="message"
@group-action="deleteRules"
@handle-cancel="handleCancel"
@close-modal="closeModal" />
</div>
</template>
@ -385,12 +408,15 @@
import { api } from '@/api'
import Status from '@/components/widgets/Status'
import TooltipButton from '@/components/widgets/TooltipButton'
import BulkActionView from '@/components/view/BulkActionView'
import eventBus from '@/config/eventBus'
export default {
name: 'LoadBalancing',
components: {
Status,
TooltipButton
TooltipButton,
BulkActionView
},
props: {
resource: {
@ -401,6 +427,16 @@ export default {
inject: ['parentFetchData', 'parentToggleLoading'],
data () {
return {
selectedRowKeys: [],
showGroupActionModal: false,
selectedItems: [],
selectedColumns: [],
filterColumns: ['State', 'Action', 'Add VMs', 'Stickiness'],
showConfirmationAction: false,
message: {
title: this.$t('label.action.bulk.delete.load.balancer.rules'),
confirmMessage: this.$t('label.confirm.delete.loadbalancer.rules')
},
loading: true,
lbRules: [],
newTagsForm: this.$form.createForm(this),
@ -523,6 +559,11 @@ export default {
searchQuery: null
}
},
computed: {
hasSelected () {
return this.selectedRowKeys.length > 0
}
},
created () {
this.fetchData()
},
@ -939,37 +980,104 @@ export default {
this.loading = false
})
},
setSelection (selection) {
this.selectedRowKeys = selection
this.$emit('selection-change', this.selectedRowKeys)
this.selectedItems = (this.lbRules.filter(function (item) {
return selection.indexOf(item.id) !== -1
}))
},
resetSelection () {
this.setSelection([])
},
onSelectChange (selectedRowKeys, selectedRows) {
this.setSelection(selectedRowKeys)
},
bulkActionConfirmation () {
this.showConfirmationAction = true
this.selectedColumns = this.columns.filter(column => {
return !this.filterColumns.includes(column.title)
})
this.selectedItems = this.selectedItems.map(v => ({ ...v, status: 'InProgress' }))
},
handleCancel () {
eventBus.$emit('update-bulk-job-status', this.selectedItems, false)
this.showGroupActionModal = false
this.selectedItems = []
this.selectedColumns = []
this.selectedRowKeys = []
this.parentFetchData()
},
deleteRules (e) {
this.showConfirmationAction = false
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' }
]
})
if (this.selectedRowKeys.length > 0) {
this.showGroupActionModal = true
}
for (const rule of this.selectedItems) {
this.handleDeleteRule(rule)
}
},
handleDeleteRule (rule) {
this.loading = true
api('deleteLoadBalancerRule', {
id: rule.id
}).then(response => {
const jobId = response.deleteloadbalancerruleresponse.jobid
this.$store.dispatch('AddAsyncJob', {
title: this.$t('label.action.delete.load.balancer'),
jobid: jobId,
description: rule.id,
status: 'progress',
bulkAction: this.selectedItems.length > 0 && this.showGroupActionModal
})
eventBus.$emit('update-job-details', jobId, null)
this.$pollJob({
jobId: response.deleteloadbalancerruleresponse.jobid,
jobId: jobId,
successMessage: this.$t('message.success.remove.rule'),
successMethod: () => {
this.parentFetchData()
this.parentToggleLoading()
this.fetchData()
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, rule.id, 'success')
}
if (this.selectedRowKeys.length === 0) {
this.parentFetchData()
this.parentToggleLoading()
}
this.closeModal()
},
errorMessage: this.$t('message.remove.rule.failed'),
errorMethod: () => {
this.parentFetchData()
this.parentToggleLoading()
this.fetchData()
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, rule.id, 'failed')
}
if (this.selectedRowKeys.length === 0) {
this.parentFetchData()
this.parentToggleLoading()
}
this.closeModal()
},
loadingMessage: this.$t('message.delete.rule.processing'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => {
this.parentFetchData()
this.parentToggleLoading()
this.fetchData()
if (this.selectedRowKeys.length === 0) {
this.parentFetchData()
this.parentToggleLoading()
}
this.closeModal()
}
},
bulkAction: `${this.selectedItems.length > 0}` && this.showGroupActionModal
})
}).catch(error => {
console.log(error)
this.$notifyError(error)
this.loading = false
})
@ -1149,6 +1257,7 @@ export default {
this.editRuleModalLoading = false
this.addVmModalLoading = false
this.addVmModalNicLoading = false
this.showConfirmationAction = false
this.vms = []
this.nics = []
this.addVmModalVisible = false

View File

@ -71,7 +71,14 @@
</div>
<a-divider/>
<a-button
v-if="(('deletePortForwardingRule' in $store.getters.apis) && this.selectedItems.length > 0)"
type="danger"
icon="plus"
style="width: 100%; margin-bottom: 15px"
@click="bulkActionConfirmation()">
{{ $t('label.action.bulk.delete.portforward.rules') }}
</a-button>
<a-table
size="small"
style="overflow-y: auto"
@ -79,6 +86,7 @@
:columns="columns"
:dataSource="portForwardRules"
:pagination="false"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
:rowKey="record => record.id">
<template slot="privateport" slot-scope="record">
{{ record.privateport }} - {{ record.privateendport }}
@ -246,6 +254,23 @@
</a-pagination>
</div>
</a-modal>
<bulk-action-view
v-if="showConfirmationAction || showGroupActionModal"
:showConfirmationAction="showConfirmationAction"
:showGroupActionModal="showGroupActionModal"
:items="portForwardRules"
:selectedRowKeys="selectedRowKeys"
:selectedItems="selectedItems"
:columns="columns"
:selectedColumns="selectedColumns"
:filterColumns="filterColumns"
action="deletePortForwardingRule"
:loading="loading"
:message="message"
@group-action="deleteRules"
@handle-cancel="handleCancel"
@close-modal="closeModal" />
</div>
</template>
@ -253,11 +278,14 @@
import { api } from '@/api'
import Status from '@/components/widgets/Status'
import TooltipButton from '@/components/widgets/TooltipButton'
import BulkActionView from '@/components/view/BulkActionView'
import eventBus from '@/config/eventBus'
export default {
components: {
Status,
TooltipButton
TooltipButton,
BulkActionView
},
props: {
resource: {
@ -268,6 +296,16 @@ export default {
inject: ['parentFetchData', 'parentToggleLoading'],
data () {
return {
selectedRowKeys: [],
showGroupActionModal: false,
selectedItems: [],
selectedColumns: [],
filterColumns: ['State', 'Action'],
showConfirmationAction: false,
message: {
title: this.$t('label.action.bulk.delete.portforward.rules'),
confirmMessage: this.$t('label.confirm.delete.portforward.rules')
},
loading: true,
portForwardRules: [],
newRule: {
@ -369,6 +407,11 @@ export default {
searchQuery: null
}
},
computed: {
hasSelected () {
return this.selectedRowKeys.length > 0
}
},
created () {
this.fetchData()
},
@ -429,18 +472,85 @@ export default {
this.loading = false
})
},
setSelection (selection) {
this.selectedRowKeys = selection
this.$emit('selection-change', this.selectedRowKeys)
this.selectedItems = (this.portForwardRules.filter(function (item) {
return selection.indexOf(item.id) !== -1
}))
},
resetSelection () {
this.setSelection([])
},
onSelectChange (selectedRowKeys, selectedRows) {
this.setSelection(selectedRowKeys)
},
bulkActionConfirmation () {
this.showConfirmationAction = true
this.selectedColumns = this.columns.filter(column => {
return !this.filterColumns.includes(column.title)
})
this.selectedItems = this.selectedItems.map(v => ({ ...v, status: 'InProgress' }))
},
handleCancel () {
eventBus.$emit('update-bulk-job-status', this.selectedItems, false)
this.showGroupActionModal = false
this.selectedItems = []
this.selectedColumns = []
this.selectedRowKeys = []
this.parentFetchData()
},
deleteRules (e) {
this.showConfirmationAction = false
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' }
]
})
if (this.selectedRowKeys.length > 0) {
this.showGroupActionModal = true
}
for (const rule of this.selectedItems) {
this.deleteRule(rule)
}
},
deleteRule (rule) {
this.loading = true
api('deletePortForwardingRule', { id: rule.id }).then(response => {
const jobId = response.deleteportforwardingruleresponse.jobid
this.$store.dispatch('AddAsyncJob', {
title: this.$t('label.portforwarding.rule'),
jobid: jobId,
description: rule.id,
status: 'progress',
bulkAction: this.selectedItems.length > 0 && this.showGroupActionModal
})
eventBus.$emit('update-job-details', jobId, null)
this.$pollJob({
jobId: response.deleteportforwardingruleresponse.jobid,
jobId: jobId,
successMessage: this.$t('message.success.remove.port.forward'),
successMethod: () => this.fetchData(),
successMethod: () => {
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, rule.id, 'success')
}
this.fetchData()
},
errorMessage: this.$t('message.remove.port.forward.failed'),
errorMethod: () => this.fetchData(),
errorMethod: () => {
if (this.selectedItems.length > 0) {
eventBus.$emit('update-resource-state', this.selectedItems, rule.id, 'failed')
}
this.fetchData()
},
loadingMessage: this.$t('message.delete.port.forward.processing'),
catchMessage: this.$t('error.fetching.async.job.result'),
catchMethod: () => this.fetchData()
catchMethod: () => this.fetchData(),
bulkAction: `${this.selectedItems.length > 0}` && this.showGroupActionModal
})
}).catch(error => {
this.$notifyError(error)
@ -503,6 +613,7 @@ export default {
this.newRule.virtualmachineid = null
this.addVmModalLoading = false
this.addVmModalNicLoading = false
this.showConfirmationAction = false
this.nics = []
this.resetTagInputs()
},