From acaee4eb936075b3304dd38374e67f030a7c3e13 Mon Sep 17 00:00:00 2001 From: Beth Elwell Date: Tue, 25 Jul 2017 17:43:27 +0100 Subject: [PATCH] Add delete action to network_qos panel Added batch and item delete actions to network_qos panel. To test the network_qos panel, either take the current patch or the latest change in the series and then build devstack with neutron qos support. Eg: local.conf: enable_plugin neutron git://git.openstack.org/openstack/neutron enable_service q-qos You will also need to amend the horizon enabled file to enable the plugin as it is currently disabled by default: _1510_project_network_qos_panel.py: Remove or comment line 13 'DISABLED = True The panel should then appear under Project > Network > Network QoS Implements: blueprint network-bandwidth-limiting-qos Change-Id: I97289d4f666fd66c71fda713ad29634bb270e2fc --- openstack_dashboard/api/rest/neutron.py | 6 +- .../network_qos/actions/actions.module.js | 66 +++++ .../actions/actions.module.spec.js | 46 ++++ .../actions/delete.action.service.js | 117 +++++++++ .../actions/delete.action.service.spec.js | 233 ++++++++++++++++++ .../static/app/core/network_qos/qos.module.js | 9 +- .../app/core/network_qos/qos.module.spec.js | 25 ++ 7 files changed, 499 insertions(+), 3 deletions(-) create mode 100644 openstack_dashboard/static/app/core/network_qos/actions/actions.module.js create mode 100644 openstack_dashboard/static/app/core/network_qos/actions/actions.module.spec.js create mode 100644 openstack_dashboard/static/app/core/network_qos/actions/delete.action.service.js create mode 100644 openstack_dashboard/static/app/core/network_qos/actions/delete.action.service.spec.js diff --git a/openstack_dashboard/api/rest/neutron.py b/openstack_dashboard/api/rest/neutron.py index 31945c2efc..12cc8ffdac 100644 --- a/openstack_dashboard/api/rest/neutron.py +++ b/openstack_dashboard/api/rest/neutron.py @@ -303,7 +303,11 @@ class QoSPolicies(generic.View): @urls.register class QoSPolicy(generic.View): """API for a single QoS Policy.""" - url_regex = r'neutron/qos_policy/(?P[^/]+)/$' + url_regex = r'neutron/qos_policies/(?P[^/]+)/$' + + @rest_utils.ajax() + def delete(self, request, policy_id): + api.neutron.policy_delete(request, policy_id) @rest_utils.ajax() def get(self, request, policy_id): diff --git a/openstack_dashboard/static/app/core/network_qos/actions/actions.module.js b/openstack_dashboard/static/app/core/network_qos/actions/actions.module.js new file mode 100644 index 0000000000..f9126c0b8a --- /dev/null +++ b/openstack_dashboard/static/app/core/network_qos/actions/actions.module.js @@ -0,0 +1,66 @@ +/** + * Licensed 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. + */ + +(function() { + 'use strict'; + + /** + * @ngdoc overview + * @ngname horizon.app.core.network_qos.actions + * + * @description + * Provides all of the actions for network_qos. + */ + + angular.module('horizon.app.core.network_qos.actions', [ + 'horizon.framework.conf', + 'horizon.app.core.network_qos' + ]) + .run(registerQosActions); + + registerQosActions.$inject = [ + 'horizon.framework.conf.resource-type-registry.service', + 'horizon.app.core.network_qos.actions.delete.service', + 'horizon.app.core.network_qos.resourceType' + ]; + + function registerQosActions( + registry, + deleteService, + qosResourceTypeCode + ) { + var qosResourceType = registry.getResourceType(qosResourceTypeCode); + + qosResourceType.itemActions + .append({ + id: 'deletePolicyAction', + service: deleteService, + template: { + text: gettext('Delete Policy'), + type: 'delete' + } + }); + + qosResourceType.batchActions + .append({ + id: 'batchDeletePolicyAction', + service: deleteService, + template: { + text: gettext('Delete Policies'), + type: 'delete-selected' + } + }); + } + +})(); diff --git a/openstack_dashboard/static/app/core/network_qos/actions/actions.module.spec.js b/openstack_dashboard/static/app/core/network_qos/actions/actions.module.spec.js new file mode 100644 index 0000000000..59ebd78f16 --- /dev/null +++ b/openstack_dashboard/static/app/core/network_qos/actions/actions.module.spec.js @@ -0,0 +1,46 @@ +/* + * Licensed 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. + */ + +(function() { + 'use strict'; + + describe('network_qos actions module', function() { + var registry; + beforeEach(module('horizon.app.core.network_qos.actions')); + + beforeEach(inject(function($injector) { + registry = $injector.get('horizon.framework.conf.resource-type-registry.service'); + })); + + it('registers Delete Policy as an item action', function() { + var actions = registry.getResourceType('OS::Neutron::QoSPolicy').itemActions; + expect(actionHasId(actions, 'deletePolicyAction')).toBe(true); + }); + + it('registers Delete Policies as a batch action', function() { + var actions = registry.getResourceType('OS::Neutron::QoSPolicy').batchActions; + expect(actionHasId(actions, 'batchDeletePolicyAction')).toBe(true); + }); + + function actionHasId(list, value) { + return list.filter(matchesId).length === 1; + + function matchesId(action) { + return action.id === value; + } + } + + }); + +})(); diff --git a/openstack_dashboard/static/app/core/network_qos/actions/delete.action.service.js b/openstack_dashboard/static/app/core/network_qos/actions/delete.action.service.js new file mode 100644 index 0000000000..e7f3d83398 --- /dev/null +++ b/openstack_dashboard/static/app/core/network_qos/actions/delete.action.service.js @@ -0,0 +1,117 @@ +/* + * Licensed 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. + */ + +(function() { + 'use strict'; + + angular + .module('horizon.app.core.network_qos') + .factory('horizon.app.core.network_qos.actions.delete.service', deleteService); + + deleteService.$inject = [ + '$q', + 'horizon.app.core.openstack-service-api.neutron', + 'horizon.app.core.openstack-service-api.policy', + 'horizon.app.core.network_qos.resourceType', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.widgets.modal.deleteModalService' + ]; + + function deleteService( + $q, + neutron, + policy, + resourceType, + actionResultService, + deleteModal + ) { + var service = { + allowed: allowed, + perform: perform + }; + + return service; + + //////////// + + function allowed() { + return policy.ifAllowed( + {rules: [ + ['network', 'delete_qos_policy'] + ]} + ); + } + + function perform(items, scope) { + var policies = angular.isArray(items) ? items : [items]; + + return openModal().then(onComplete); + + function openModal() { + return deleteModal.open( + scope, + policies, + { + labels: labelize(policies.length), + deleteEntity: neutron.deletePolicy + } + ); + } + + function onComplete(result) { + var actionResult = actionResultService.getActionResult(); + + result.pass.forEach(function markDeleted(item) { + actionResult.deleted(resourceType, item.context.id); + }); + result.fail.forEach(function markFailed(item) { + actionResult.failed(resourceType, item.context.id); + }); + + return actionResult.result; + } + } + + function labelize(count) { + return { + title: ngettext( + 'Confirm Delete Policy', + 'Confirm Delete Policies', + count), + + message: ngettext( + 'You have selected "%s". Deleted policy is not recoverable.', + 'You have selected "%s". Deleted policies are not recoverable.', + count), + + submit: ngettext( + 'Delete Policy', + 'Delete Policies', + count), + + success: ngettext( + 'Deleted policy: %s.', + 'Deleted policies: %s.', + count), + + error: ngettext( + 'Unable to delete policy: %s.', + 'Unable to delete policies: %s.', + count) + }; + } + + } + +})(); diff --git a/openstack_dashboard/static/app/core/network_qos/actions/delete.action.service.spec.js b/openstack_dashboard/static/app/core/network_qos/actions/delete.action.service.spec.js new file mode 100644 index 0000000000..45398b5fd9 --- /dev/null +++ b/openstack_dashboard/static/app/core/network_qos/actions/delete.action.service.spec.js @@ -0,0 +1,233 @@ +/* + * Licensed 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. + */ + +(function() { + 'use strict'; + + describe('horizon.app.core.network_qos.actions.delete.service', function() { + + var deleteModalService = { + open: function () { + deferredModal.resolve({ + pass: [{context: {id: 'a'}}], + fail: [{context: {id: 'b'}}] + }); + return deferredModal.promise; + } + }; + + var neutronAPI = { + deletePolicy: function() { + return; + } + }; + + var policyAPI = { + ifAllowed: function() { + return { + success: function(callback) { + callback({allowed: true}); + }, + failure: function(callback) { + callback({allowed: false}); + } + }; + } + }; + + var deferred, service, $scope, deferredModal; + + //////////// + + beforeEach(module('horizon.app.core')); + beforeEach(module('horizon.app.core.network_qos')); + beforeEach(module('horizon.framework')); + + beforeEach(module('horizon.framework.widgets.modal', function($provide) { + $provide.value('horizon.framework.widgets.modal.deleteModalService', deleteModalService); + })); + + beforeEach(module('horizon.app.core.openstack-service-api', function($provide) { + $provide.value('horizon.app.core.openstack-service-api.neutron', neutronAPI); + $provide.value('horizon.app.core.openstack-service-api.policy', policyAPI); + spyOn(policyAPI, 'ifAllowed').and.callThrough(); + })); + + beforeEach(inject(function($injector, _$rootScope_, $q) { + $scope = _$rootScope_.$new(); + service = $injector.get('horizon.app.core.network_qos.actions.delete.service'); + deferred = $q.defer(); + deferredModal = $q.defer(); + })); + + function generatePolicy(policyCount) { + + var policies = []; + var data = { + owner: 'project', + name: '', + id: '' + }; + + for (var index = 0; index < policyCount; index++) { + var policy = angular.copy(data); + policy.id = String(index); + policy.name = 'policy' + index; + policies.push(policy); + } + return policies; + } + + describe('perform method', function() { + + beforeEach(function() { + spyOn(deleteModalService, 'open').and.callThrough(); + }); + + //////////// + + it('should open the delete modal and show correct labels, one object', + function testSingleObject() { + var policies = generatePolicy(1); + service.perform(policies[0]); + $scope.$apply(); + + var labels = deleteModalService.open.calls.argsFor(0)[2].labels; + expect(deleteModalService.open).toHaveBeenCalled(); + angular.forEach(labels, function eachLabel(label) { + expect(label.toLowerCase()).toContain('policy'); + }); + } + ); + + it('should open the delete modal and show correct singular labels', + function testSingleLabels() { + var policies = generatePolicy(1); + service.perform(policies); + $scope.$apply(); + + var labels = deleteModalService.open.calls.argsFor(0)[2].labels; + expect(deleteModalService.open).toHaveBeenCalled(); + angular.forEach(labels, function eachLabel(label) { + expect(label.toLowerCase()).toContain('policy'); + }); + } + ); + + it('should open the delete modal and show correct plural labels', + function testpluralLabels() { + var policies = generatePolicy(2); + service.perform(policies); + $scope.$apply(); + + var labels = deleteModalService.open.calls.argsFor(0)[2].labels; + expect(deleteModalService.open).toHaveBeenCalled(); + angular.forEach(labels, function eachLabel(label) { + expect(label.toLowerCase()).toContain('policies'); + }); + } + ); + + it('should open the delete modal with correct entities', + function testEntities() { + var count = 3; + var policies = generatePolicy(count); + service.perform(policies); + $scope.$apply(); + + var entities = deleteModalService.open.calls.argsFor(0)[1]; + expect(deleteModalService.open).toHaveBeenCalled(); + expect(entities.length).toEqual(count); + } + ); + + it('should only delete policies that are valid', + function testValids() { + var count = 2; + var policies = generatePolicy(count); + service.perform(policies); + $scope.$apply(); + + var entities = deleteModalService.open.calls.argsFor(0)[1]; + expect(deleteModalService.open).toHaveBeenCalled(); + expect(entities.length).toBe(count); + expect(entities[0].name).toEqual('policy0'); + expect(entities[1].name).toEqual('policy1'); + } + ); + + it('should pass in a function that deletes a policy', + function testNeutron() { + spyOn(neutronAPI, 'deletePolicy'); + var count = 1; + var policies = generatePolicy(count); + var policy = policies[0]; + service.perform(policies); + $scope.$apply(); + + var contextArg = deleteModalService.open.calls.argsFor(0)[2]; + var deleteFunction = contextArg.deleteEntity; + deleteFunction(policy.id); + expect(neutronAPI.deletePolicy).toHaveBeenCalledWith(policy.id); + } + ); + + }); // end of delete modal + + describe('allow method', function() { + + var resolver = { + success: function() {}, + error: function() {} + }; + + beforeEach(function() { + spyOn(resolver, 'success'); + spyOn(resolver, 'error'); + }); + + //////////// + + it('should use default policy if batch action', + function testBatch() { + service.allowed(); + $scope.$apply(); + expect(policyAPI.ifAllowed).toHaveBeenCalled(); + expect(resolver.success).not.toHaveBeenCalled(); + expect(resolver.error).not.toHaveBeenCalled(); + } + ); + + it('allows delete if policy can be deleted', + function testValid() { + service.allowed().success(resolver.success); + $scope.$apply(); + expect(resolver.success).toHaveBeenCalled(); + } + ); + + it('disallows delete if policy is not owned by user', + function testOwner() { + deferred.reject(); + service.allowed().failure(resolver.error); + $scope.$apply(); + expect(resolver.error).toHaveBeenCalled(); + } + ); + + }); // end of allow method + + }); // end of delete.service + +})(); diff --git a/openstack_dashboard/static/app/core/network_qos/qos.module.js b/openstack_dashboard/static/app/core/network_qos/qos.module.js index f2f9898c24..a66699f0fd 100644 --- a/openstack_dashboard/static/app/core/network_qos/qos.module.js +++ b/openstack_dashboard/static/app/core/network_qos/qos.module.js @@ -26,7 +26,10 @@ angular .module('horizon.app.core.network_qos', [ 'ngRoute', - 'horizon.app.core.network_qos.details' + 'horizon.framework.conf', + 'horizon.app.core.network_qos.actions', + 'horizon.app.core.network_qos.details', + 'horizon.app.core' ]) .constant('horizon.app.core.network_qos.resourceType', 'OS::Neutron::QoSPolicy') .run(run) @@ -34,12 +37,14 @@ run.$inject = [ 'horizon.framework.conf.resource-type-registry.service', + 'horizon.framework.util.i18n.gettext', 'horizon.app.core.network_qos.basePath', 'horizon.app.core.network_qos.service', 'horizon.app.core.network_qos.resourceType' ]; function run(registry, + gettext, basePath, qosService, qosResourceType) { @@ -130,7 +135,7 @@ .when('/project/network_qos', { templateUrl: path + 'panel.html' }) - .when('/project/network_qos/:policy_id', { + .when('/project/network_qos/:id', { redirectTo: goToAngularDetails }); diff --git a/openstack_dashboard/static/app/core/network_qos/qos.module.spec.js b/openstack_dashboard/static/app/core/network_qos/qos.module.spec.js index 51be982fd7..a2e12ce1f7 100644 --- a/openstack_dashboard/static/app/core/network_qos/qos.module.spec.js +++ b/openstack_dashboard/static/app/core/network_qos/qos.module.spec.js @@ -21,4 +21,29 @@ }); }); + describe('loading the qos module', function () { + var registry; + + beforeEach(module('horizon.app.core.network_qos')); + beforeEach(inject(function($injector) { + registry = $injector.get('horizon.framework.conf.resource-type-registry.service'); + })); + + it('registers names', function() { + expect(registry.getResourceType('OS::Neutron::QoSPolicy').getName()).toBe("QoS Policies"); + }); + + it('should set facets for search', function () { + var names = registry.getResourceType('OS::Neutron::QoSPolicy').filterFacets + .map(getName); + expect(names).toContain('name'); + expect(names).toContain('description'); + expect(names).toContain('shared'); + + function getName(x) { + return x.name; + } + }); + }); + })();