diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 3ff927b578ab..8c5e8ee7ca77 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -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", diff --git a/ui/src/components/header/HeaderNotice.vue b/ui/src/components/header/HeaderNotice.vue index 03a5acd2017a..07d9936a55d3 100644 --- a/ui/src/components/header/HeaderNotice.vue +++ b/ui/src/components/header/HeaderNotice.vue @@ -33,8 +33,11 @@ - - + +
+ {{ getResourceName(job.description, "name") + ' - ' }} + {{ getResourceName(job.description, "msg") }} + {{ job.description }}
@@ -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) diff --git a/ui/src/components/view/BulkActionProgress.vue b/ui/src/components/view/BulkActionProgress.vue new file mode 100644 index 000000000000..73a6a3419e9c --- /dev/null +++ b/ui/src/components/view/BulkActionProgress.vue @@ -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. + + + diff --git a/ui/src/components/view/BulkActionView.vue b/ui/src/components/view/BulkActionView.vue new file mode 100644 index 000000000000..acdc79961515 --- /dev/null +++ b/ui/src/components/view/BulkActionView.vue @@ -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. + + + + diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index b17a1333ce0a..b940da5978e8 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -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 diff --git a/ui/src/components/widgets/Status.vue b/ui/src/components/widgets/Status.vue index d380271aba05..cd68103c1936 100644 --- a/ui/src/components/widgets/Status.vue +++ b/ui/src/components/widgets/Status.vue @@ -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': diff --git a/ui/src/config/section/account.js b/ui/src/config/section/account.js index 86c88fcf0108..1b919035e0f4 100644 --- a/ui/src/config/section/account.js +++ b/ui/src/config/section/account.js @@ -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 } }) } } ] } diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index d6ecbf53b96a..ade79d80f73d 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -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 } }) } } ] } diff --git a/ui/src/config/section/infra/ilbvms.js b/ui/src/config/section/infra/ilbvms.js index 393a769604eb..2a22922bebd0 100644 --- a/ui/src/config/section/infra/ilbvms.js +++ b/ui/src/config/section/infra/ilbvms.js @@ -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', diff --git a/ui/src/config/section/infra/routers.js b/ui/src/config/section/infra/routers.js index fd10acc3353e..b34389d1dc2e 100644 --- a/ui/src/config/section/infra/routers.js +++ b/ui/src/config/section/infra/routers.js @@ -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 } }) } } ] } diff --git a/ui/src/config/section/infra/systemVms.js b/ui/src/config/section/infra/systemVms.js index cdde06805562..27a2e47d8d7f 100644 --- a/ui/src/config/section/infra/systemVms.js +++ b/ui/src/config/section/infra/systemVms.js @@ -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 } }) } } ] } diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js index 4076509ad211..937d58a3167c 100644 --- a/ui/src/config/section/network.js +++ b/ui/src/config/section/network.js @@ -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 } }) } } ] } diff --git a/ui/src/config/section/offering.js b/ui/src/config/section/offering.js index 13be7c4ee07e..3bd1ab98eb4d 100644 --- a/ui/src/config/section/offering.js +++ b/ui/src/config/section/offering.js @@ -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 } }) } }] } ] diff --git a/ui/src/config/section/project.js b/ui/src/config/section/project.js index 72cdd713ad2c..531901c50b68 100644 --- a/ui/src/config/section/project.js +++ b/ui/src/config/section/project.js @@ -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 } }) } } ] } diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js index 976cab9acde9..f22d702a4662 100644 --- a/ui/src/config/section/storage.js +++ b/ui/src/config/section/storage.js @@ -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 } }) } } ] }, diff --git a/ui/src/utils/plugins.js b/ui/src/utils/plugins.js index 87e4e74ff389..d452b0d2dd8f 100644 --- a/ui/src/utils/plugins.js +++ b/ui/src/utils/plugins.js @@ -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() + // } }) } } diff --git a/ui/src/views/AutogenView.vue b/ui/src/views/AutogenView.vue index 697194102567..f88f7fd1c772 100644 --- a/ui/src/views/AutogenView.vue +++ b/ui/src/views/AutogenView.vue @@ -68,7 +68,7 @@ @@ -144,9 +147,37 @@ - - - +
+ + + + + + + + + +
+
+ + + +
+
+ + + +

+
@@ -330,6 +362,12 @@ + @@ -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 diff --git a/ui/src/views/compute/StartVirtualMachine.vue b/ui/src/views/compute/StartVirtualMachine.vue index deac449dd223..2a534fa4f90e 100644 --- a/ui/src/views/compute/StartVirtualMachine.vue +++ b/ui/src/views/compute/StartVirtualMachine.vue @@ -100,6 +100,7 @@