From e7f22178b28840adf30769bd73b06030b4bdee29 Mon Sep 17 00:00:00 2001 From: Shu Muto Date: Mon, 10 Jul 2017 16:59:01 +0900 Subject: [PATCH] Add delete action for key pair This patch adds delete actions for angularized key pair panel. Change-Id: Iccb5014add0e19d6154bd6261d97a83b2ecdf32f Partial-Implements: blueprint ng-keypairs --- openstack_dashboard/api/nova.py | 8 +- openstack_dashboard/api/rest/nova.py | 4 + .../core/keypairs/actions/actions.module.js | 62 ++++++++ .../core/keypairs/actions/delete.service.js | 121 ++++++++++++++++ .../keypairs/actions/delete.service.spec.js | 135 ++++++++++++++++++ .../keypairs/details/details.controller.js | 2 + .../app/core/keypairs/details/details.html | 2 +- .../app/core/keypairs/keypairs.module.js | 4 +- .../app/core/keypairs/keypairs.service.js | 10 +- .../core/keypairs/keypairs.service.spec.js | 4 +- .../openstack-service-api/nova.service.js | 23 +++ .../nova.service.spec.js | 13 ++ .../test/api_tests/nova_rest_tests.py | 6 + 13 files changed, 381 insertions(+), 13 deletions(-) create mode 100644 openstack_dashboard/static/app/core/keypairs/actions/actions.module.js create mode 100644 openstack_dashboard/static/app/core/keypairs/actions/delete.service.js create mode 100644 openstack_dashboard/static/app/core/keypairs/actions/delete.service.spec.js diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 16c40445e9..3009cfd232 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -459,8 +459,8 @@ def keypair_import(request, name, public_key): @profiler.trace -def keypair_delete(request, keypair_id): - novaclient(request).keypairs.delete(keypair_id) +def keypair_delete(request, name): + novaclient(request).keypairs.delete(name) @profiler.trace @@ -469,8 +469,8 @@ def keypair_list(request): @profiler.trace -def keypair_get(request, keypair_id): - return novaclient(request).keypairs.get(keypair_id) +def keypair_get(request, name): + return novaclient(request).keypairs.get(name) @profiler.trace diff --git a/openstack_dashboard/api/rest/nova.py b/openstack_dashboard/api/rest/nova.py index 1fb111a4b8..36e7579113 100644 --- a/openstack_dashboard/api/rest/nova.py +++ b/openstack_dashboard/api/rest/nova.py @@ -101,6 +101,10 @@ class Keypair(generic.View): """Get a specific keypair.""" return api.nova.keypair_get(request, name).to_dict() + @rest_utils.ajax() + def delete(self, request, name): + api.nova.keypair_delete(request, name) + @urls.register class Services(generic.View): diff --git a/openstack_dashboard/static/app/core/keypairs/actions/actions.module.js b/openstack_dashboard/static/app/core/keypairs/actions/actions.module.js new file mode 100644 index 0000000000..e37262932c --- /dev/null +++ b/openstack_dashboard/static/app/core/keypairs/actions/actions.module.js @@ -0,0 +1,62 @@ +/** + * 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.keypairs.actions + * + * @description + * Provides all of the actions for keypairs. + */ + angular.module('horizon.app.core.keypairs.actions', [ + 'horizon.framework.conf', + 'horizon.app.core.keypairs' + ]) + .run(registerKeypairActions); + + registerKeypairActions.$inject = [ + 'horizon.framework.conf.resource-type-registry.service', + 'horizon.app.core.keypairs.actions.delete.service', + 'horizon.app.core.keypairs.resourceType' + ]; + + function registerKeypairActions( + registry, + deleteKeypairService, + resourceType + ) { + var keypairResourceType = registry.getResourceType(resourceType); + keypairResourceType.batchActions + .append({ + id: 'batchDeleteKeypairAction', + service: deleteKeypairService, + template: { + type: 'delete-selected', + text: gettext('Delete Key Pairs') + } + }); + keypairResourceType.itemActions + .append({ + id: 'deleteKeypairAction', + service: deleteKeypairService, + template: { + type: 'delete', + text: gettext('Delete Key Pair') + } + }); + } +})(); diff --git a/openstack_dashboard/static/app/core/keypairs/actions/delete.service.js b/openstack_dashboard/static/app/core/keypairs/actions/delete.service.js new file mode 100644 index 0000000000..ecb36991ea --- /dev/null +++ b/openstack_dashboard/static/app/core/keypairs/actions/delete.service.js @@ -0,0 +1,121 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use self 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.keypairs') + .factory('horizon.app.core.keypairs.actions.delete.service', deleteService); + + deleteService.$inject = [ + '$location', + 'horizon.app.core.keypairs.resourceType', + 'horizon.app.core.openstack-service-api.nova', + 'horizon.app.core.openstack-service-api.policy', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.util.i18n.gettext', + 'horizon.framework.widgets.modal.deleteModalService' + ]; + + /* + * @ngdoc factory + * @name horizon.app.core.keypairs.actions.delete.service + * + * @Description + * Brings up the delete keypairs confirmation modal dialog. + + * On submit, delete given keypairs. + * On cancel, do nothing. + */ + function deleteService( + $location, + resourceType, + nova, + policy, + actionResultService, + gettext, + deleteModal + ) { + + var service = { + allowed: allowed, + perform: perform + }; + + return service; + + ////////////// + + function allowed() { + return policy.ifAllowed({ rules: [['compute', 'os_compute_api:os-keypairs:delete']] }); + } + + function perform(items, scope) { + var keypairs = angular.isArray(items) ? items : [items]; + var context = { + labels: labelize(keypairs.length), + deleteEntity: deleteKeypair + }; + return deleteModal.open(scope, keypairs, context).then(deleteResult); + } + + function deleteResult(deleteModalResult) { + // To make the result of this action generically useful, reformat the return + // from the deleteModal into a standard form + var actionResult = actionResultService.getActionResult(); + deleteModalResult.pass.forEach(function markDeleted(item) { + actionResult.deleted(resourceType, item.context.id); + }); + deleteModalResult.fail.forEach(function markFailed(item) { + actionResult.failed(resourceType, item.context.id); + }); + + if (actionResult.result.failed.length === 0 && actionResult.result.deleted.length > 0) { + $location.path("/project/key_pairs"); + } else { + return actionResult.result; + } + } + + function labelize(count) { + return { + + title: ngettext( + 'Confirm Delete Key Pair', + 'Confirm Delete Key Pairs', count), + + message: ngettext( + 'You have selected "%s". Deleted key pair is not recoverable.', + 'You have selected "%s". Deleted key pairs are not recoverable.', count), + + submit: ngettext( + 'Delete Key Pair', + 'Delete Key Pairs', count), + + success: ngettext( + 'Deleted Key Pair: %s.', + 'Deleted Key Pairs: %s.', count), + + error: ngettext( + 'Unable to delete Key Pair: %s.', + 'Unable to delete Key Pairs: %s.', count) + }; + } + + function deleteKeypair(keypair) { + return nova.deleteKeypair(keypair, true); + } + } +})(); diff --git a/openstack_dashboard/static/app/core/keypairs/actions/delete.service.spec.js b/openstack_dashboard/static/app/core/keypairs/actions/delete.service.spec.js new file mode 100644 index 0000000000..53577e3cd7 --- /dev/null +++ b/openstack_dashboard/static/app/core/keypairs/actions/delete.service.spec.js @@ -0,0 +1,135 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use self 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.keypairs.actions.delete.service', function() { + + var service, novaAPI, $scope, deferredModal; + var deleteModalService = { + open: function () { + deferredModal.resolve({ + pass: [{context: {id: 'a'}}], + fail: [{context: {id: 'b'}}] + }); + return deferredModal.promise; + } + }; + + /////////////////////// + + beforeEach(module('horizon.app.core')); + beforeEach(module('horizon.app.core.keypairs')); + beforeEach(module('horizon.framework')); + beforeEach(module('horizon.framework.widgets.modal', function($provide) { + $provide.value('horizon.framework.widgets.modal.deleteModalService', deleteModalService); + })); + beforeEach(inject(function($injector, _$rootScope_, $q) { + $scope = _$rootScope_.$new(); + deferredModal = $q.defer(); + service = $injector.get('horizon.app.core.keypairs.actions.delete.service'); + novaAPI = $injector.get('horizon.app.core.openstack-service-api.nova'); + })); + + describe('perform method', function() { + + beforeEach(function() { + spyOn(deleteModalService, 'open').and.callThrough(); + }); + + //////////// + + it('should open the delete modal and show correct labels, single object', testSingleLabels); + it('should open the delete modal and show correct labels, plural objects', testPluralLabels); + it('should open the delete modal with correct entities', testEntities); + it('should only delete keypairs that are valid', testValids); + it('should pass in a function that deletes an keypair', testNova); + it('should check the policy if the user is allowed to delete key pair', testAllowed); + + //////////// + + function testSingleLabels() { + var keypairs = {name: 'Hokusai'}; + service.perform(keypairs); + + $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('key pair'); + }); + } + + function testPluralLabels() { + var keypairs = [{name: 'Hokusai'}, {name: 'Utamaro'}]; + service.perform(keypairs); + + $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('key pairs'); + }); + } + + function testEntities() { + var keypairs = [{name: 'Hokusai'}, {name: 'Utamaro'}, {name: 'Hiroshige'}]; + service.perform(keypairs); + + $scope.$apply(); + + var entities = deleteModalService.open.calls.argsFor(0)[1]; + expect(deleteModalService.open).toHaveBeenCalled(); + expect(entities.length).toEqual(keypairs.length); + } + + function testValids() { + var keypairs = [{name: 'Hokusai'}, {name: 'Utamaro'}, {name: 'Hiroshige'}]; + service.perform(keypairs); + + $scope.$apply(); + + var entities = deleteModalService.open.calls.argsFor(0)[1]; + expect(deleteModalService.open).toHaveBeenCalled(); + expect(entities.length).toBe(keypairs.length); + expect(entities[0].name).toEqual('Hokusai'); + expect(entities[1].name).toEqual('Utamaro'); + expect(entities[2].name).toEqual('Hiroshige'); + } + + function testNova() { + spyOn(novaAPI, 'deleteKeypair').and.callFake(angular.noop); + var keypairs = [{id: 1760, name: 'Hokusai'}, {id: 1753, name: 'Utamaro'}]; + service.perform(keypairs); + + $scope.$apply(); + + var contextArg = deleteModalService.open.calls.argsFor(0)[2]; + var deleteFunction = contextArg.deleteEntity; + deleteFunction(keypairs[0].id); + expect(novaAPI.deleteKeypair).toHaveBeenCalledWith(keypairs[0].id, true); + } + + function testAllowed() { + var allowed = service.allowed(); + expect(allowed).toBeTruthy(); + } + }); // end of delete modal + + }); // end of delete + +})(); diff --git a/openstack_dashboard/static/app/core/keypairs/details/details.controller.js b/openstack_dashboard/static/app/core/keypairs/details/details.controller.js index d3d72223a4..42aa06a1b5 100644 --- a/openstack_dashboard/static/app/core/keypairs/details/details.controller.js +++ b/openstack_dashboard/static/app/core/keypairs/details/details.controller.js @@ -28,6 +28,8 @@ function onGetKeypair(response) { ctrl.keypair = response.data; + ctrl.keypair.keypair_id = ctrl.keypair.id; + ctrl.keypair.id = ctrl.keypair.name; } } })(); diff --git a/openstack_dashboard/static/app/core/keypairs/details/details.html b/openstack_dashboard/static/app/core/keypairs/details/details.html index 340e04f585..c72da13f2d 100644 --- a/openstack_dashboard/static/app/core/keypairs/details/details.html +++ b/openstack_dashboard/static/app/core/keypairs/details/details.html @@ -5,7 +5,7 @@ resource-type-name="OS::Nova::Keypair" cls="dl-horizontal" item="ctrl.keypair" - property-groups="[['id', 'name', 'fingerprint', 'created_at', 'user_id', 'public_key']]"> + property-groups="[['keypair_id', 'name', 'fingerprint', 'created_at', 'user_id', 'public_key']]"> diff --git a/openstack_dashboard/static/app/core/keypairs/keypairs.module.js b/openstack_dashboard/static/app/core/keypairs/keypairs.module.js index 433e13db71..f75c783087 100644 --- a/openstack_dashboard/static/app/core/keypairs/keypairs.module.js +++ b/openstack_dashboard/static/app/core/keypairs/keypairs.module.js @@ -28,6 +28,7 @@ angular .module('horizon.app.core.keypairs', [ 'ngRoute', + 'horizon.app.core.keypairs.actions', 'horizon.app.core.keypairs.details' ]) .constant('horizon.app.core.keypairs.resourceType', 'OS::Nova::Keypair') @@ -72,7 +73,8 @@ function keypairProperties() { return { - 'id': {label: gettext('ID'), filters: ['noValue'] }, + 'id': {}, + 'keypair_id': {label: gettext('ID'), filters: ['noValue'] }, 'name': {label: gettext('Name'), filters: ['noName'] }, 'fingerprint': {label: gettext('Fingerprint'), filters: ['noValue'] }, 'created_at': {label: gettext('Created'), filters: ['mediumDate'] }, diff --git a/openstack_dashboard/static/app/core/keypairs/keypairs.service.js b/openstack_dashboard/static/app/core/keypairs/keypairs.service.js index 891c599a3c..111329e0dc 100644 --- a/openstack_dashboard/static/app/core/keypairs/keypairs.service.js +++ b/openstack_dashboard/static/app/core/keypairs/keypairs.service.js @@ -19,7 +19,6 @@ .factory('horizon.app.core.keypairs.service', keypairsService); keypairsService.$inject = [ - '$filter', 'horizon.app.core.detailRoute', 'horizon.app.core.openstack-service-api.nova' ]; @@ -34,7 +33,7 @@ * but do not need to be restricted to such use. Each exposed function * is documented below. */ - function keypairsService($filter, detailRoute, nova) { + function keypairsService(detailRoute, nova) { return { getKeypairsPromise: getKeypairsPromise, getKeypairPromise: getKeypairPromise, @@ -54,11 +53,12 @@ return nova.getKeypairs(params).then(modifyResponse); function modifyResponse(response) { - return {data: {items: response.data.items.map(modifyItems)}}; + return {data: {items: response.data.items.map(modifyItem)}}; - function modifyItems(item) { + function modifyItem(item) { item = item.keypair; - item.trackBy = item.name; + item.id = item.name; + item.trackBy = item.name + item.fingerprint; return item; } } diff --git a/openstack_dashboard/static/app/core/keypairs/keypairs.service.spec.js b/openstack_dashboard/static/app/core/keypairs/keypairs.service.spec.js index d64f60b603..48169bb9cc 100644 --- a/openstack_dashboard/static/app/core/keypairs/keypairs.service.spec.js +++ b/openstack_dashboard/static/app/core/keypairs/keypairs.service.spec.js @@ -40,13 +40,13 @@ deferredSession.resolve({}); deferred.resolve({ data: { - items: [{keypair: {name: 'keypair1'}}] + items: [{keypair: {name: 'keypair1', fingerprint: 'fp'}}] } }); $timeout.flush(); expect(nova.getKeypairs).toHaveBeenCalled(); expect(result.$$state.value.data.items[0].name).toBe('keypair1'); - expect(result.$$state.value.data.items[0].trackBy).toBe('keypair1'); + expect(result.$$state.value.data.items[0].trackBy).toBe('keypair1fp'); })); }); diff --git a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js index 84137d5f7c..0317997da1 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.js @@ -46,6 +46,7 @@ getKeypairs: getKeypairs, createKeypair: createKeypair, getKeypair: getKeypair, + deleteKeypair: deleteKeypair, getAvailabilityZones: getAvailabilityZones, getLimits: getLimits, createServer: createServer, @@ -175,6 +176,28 @@ }); } + /** + * @name deleteKeypair + * @description + * Delete a single keypair by name. + * + * @param {String} name + * Keypair to delete + * + * @param {boolean} suppressError + * If passed in, this will not show the default error handling + * (horizon alert). + * + * @returns {Object} The result of the API call + */ + function deleteKeypair(name, suppressError) { + var promise = apiService.delete('/api/nova/keypairs/' + name); + return suppressError ? promise : promise.error(function() { + var msg = gettext('Unable to delete the keypair with name: %(name)s'); + toastService.add('error', interpolate(msg, { name: name }, true)); + }); + } + // Availability Zones /** diff --git a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js index 20da7fdef0..909a9c78ab 100644 --- a/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js +++ b/openstack_dashboard/static/app/core/openstack-service-api/nova.service.spec.js @@ -142,6 +142,19 @@ {} ] }, + { + "func": "deleteKeypair", + "method": "delete", + "path": "/api/nova/keypairs/19", + "error": "Unable to delete the keypair with name: 19", + "testInput": [19] + }, + { + "func": "deleteKeypair", + "method": "delete", + "path": "/api/nova/keypairs/19", + "testInput": [19, true] + }, { "func": "getKeypair", "method": "get", diff --git a/openstack_dashboard/test/api_tests/nova_rest_tests.py b/openstack_dashboard/test/api_tests/nova_rest_tests.py index e166e07d2d..3b6fc66bdc 100644 --- a/openstack_dashboard/test/api_tests/nova_rest_tests.py +++ b/openstack_dashboard/test/api_tests/nova_rest_tests.py @@ -224,6 +224,12 @@ class NovaRestTestCase(test.TestCase): response.json) nc.keypair_get.assert_called_once_with(request, "1") + @mock.patch.object(nova.api, 'nova') + def test_keypair_delete(self, nc): + request = self.mock_rest_request() + nova.Keypair().delete(request, "1") + nc.keypair_delete.assert_called_once_with(request, "1") + # # Availability Zones #