Add delete to member row actions

Change-Id: I6d3d71984dc98bce9d6fcda9c04ccb0999d30a51
This commit is contained in:
Jacky Hu 2017-11-20 22:35:47 +08:00
parent 536da0119a
commit dabf7dac27
12 changed files with 354 additions and 11 deletions

View File

@ -776,6 +776,14 @@ class Member(generic.View):
member = conn.load_balancer.find_member(member_id, pool_id)
return _get_sdk_object_dict(member)
@rest_utils.ajax()
def delete(self, request, member_id, pool_id):
"""Delete a specific member belonging to a specific pool.
"""
conn = _get_sdk_connection(request)
conn.load_balancer.delete_member(member_id, pool_id)
@rest_utils.ajax()
def put(self, request, member_id, pool_id):
"""Edit a pool member.

View File

@ -312,6 +312,9 @@ msgstr "Confirm Delete Load Balancers (konfirmasi hapus beban penyeimbang)"
msgid "Confirm Delete Pool"
msgstr "Confirm Delete Pool (konfirmasi hapus kolam)"
msgid "Confirm Delete Member"
msgstr "Confirm Delete Member"
msgid "Confirm Disassociate Floating IP Address"
msgstr "Konfirmasi pemisahan alamat IP mengambang"
@ -360,6 +363,9 @@ msgstr "Delete Load Balancers (hapus penyeimbang beban)"
msgid "Delete Pool"
msgstr "Delete Pool (hapus kolam)"
msgid "Delete Member"
msgstr "Delete Member"
#, python-format
msgid "Deleted health monitor: %s."
msgstr "Pemantauan kesehatan yang dihapus: %s."
@ -376,6 +382,10 @@ msgstr "Penyeimbang beban yang dihapus: %s."
msgid "Deleted pool: %s."
msgstr "Kolam yang dihapus: %s."
#, python-format
msgid "Deleted member: %s."
msgstr "Deleted member: %s."
msgid "Description"
msgstr "Description (gambaran)"
@ -797,6 +807,10 @@ msgstr ""
msgid "The following pool could not be deleted: %s."
msgstr "Kolam berikut ini tidak bisa dihapus: %s."
#, python-format
msgid "The following member could not be deleted: %s."
msgstr "The following member could not be deleted: %s."
msgid "The health check interval must be greater than or equal to the timeout."
msgstr ""
"Interval pemeriksaan kesehatan harus lebih besar dari atau sama dengan batas "
@ -907,6 +921,9 @@ msgstr "Tidak dapat menghapus penyeimbang beban."
msgid "Unable to delete pool."
msgstr "Tidak dapat menghapus kolam."
msgid "Unable to delete member."
msgstr "Unable to delete member."
#, python-format
msgid "Unable to disassociate floating IP address from load balancer: %s."
msgstr ""
@ -1089,3 +1106,11 @@ msgid ""
msgstr ""
"Anda telah memilih \"%s\". Harap mengkonfirmasi pilihan Anda. Kolam yang "
"telah dihapus tidak dapat dipulihkan."
#, python-format
msgid ""
"You have selected \"%s\". Please confirm your selection. Deleted members are "
"not recoverable."
msgstr ""
"You have selected \"%s\". Please confirm your selection. Deleted members are "
"not recoverable."

View File

@ -52,6 +52,7 @@
deletePool: deletePool,
getMembers: getMembers,
getMember: getMember,
deleteMember: deleteMember,
editMember: editMember,
getHealthMonitor: getHealthMonitor,
deleteHealthMonitor: deleteHealthMonitor,
@ -346,6 +347,23 @@
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.deleteMember
* @description
* Delete a single pool Member by ID.
* @param {string} poolId
* Specifies the id of the pool the member belongs to.
* @param {string} memberId
* Specifies the id of the member to request.
*/
function deleteMember(poolId, memberId) {
return apiService.delete('/api/lbaas/pools/' + poolId + '/members/' + memberId + '/')
.error(function () {
toastService.add('error', gettext('Unable to delete member.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.editMember
* @description

View File

@ -127,6 +127,13 @@
error: 'Unable to retrieve member.',
testInput: [ '1234', '5678' ]
},
{
func: 'deleteMember',
method: 'delete',
path: '/api/lbaas/pools/1234/members/5678/',
error: 'Unable to delete member.',
testInput: [ '1234', '5678' ]
},
{
func: 'editMember',
method: 'put',

View File

@ -0,0 +1,111 @@
/*
* Copyright 2017 Walmart.
*
* 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.dashboard.project.lbaasv2.members')
.factory('horizon.dashboard.project.lbaasv2.members.actions.delete', deleteService);
deleteService.$inject = [
'$q',
'$location',
'$route',
'horizon.framework.widgets.modal.deleteModalService',
'horizon.app.core.openstack-service-api.lbaasv2',
'horizon.app.core.openstack-service-api.policy',
'horizon.framework.util.i18n.gettext'
];
/**
* @ngDoc factory
* @name horizon.dashboard.project.lbaasv2.members.actions.deleteService
* @description
* Brings up the delete member confirmation modal dialog.
* On submit, deletes selected member.
* On cancel, does nothing.
* @param $q The angular service for promises.
* @param $location The angular $location service.
* @param $route The angular $route service.
* @param deleteModal The horizon delete modal service.
* @param api The LBaaS v2 API service.
* @param policy The horizon policy service.
* @param gettext The horizon gettext function for translation.
* @returns The load balancers table delete service.
*/
function deleteService($q, $location, $route, deleteModal, api, policy, gettext) {
var loadbalancerId, listenerId, poolId, statePromise;
var context = {
labels: {
title: gettext('Confirm Delete Member'),
message: gettext('You have selected "%s". Please confirm your selection. Deleted members ' +
'are not recoverable.'),
submit: gettext('Delete Member'),
success: gettext('Deleted member: %s.'),
error: gettext('The following member could not be deleted: %s.')
},
deleteEntity: deleteItem,
successEvent: 'success',
failedEvent: 'error'
};
var service = {
perform: perform,
allowed: allowed,
init: init
};
return service;
//////////////
function init(_loadbalancerId_, _listenerId_, _poolId_, _statePromise_) {
loadbalancerId = _loadbalancerId_;
listenerId = _listenerId_;
poolId = _poolId_;
statePromise = _statePromise_;
return service;
}
function perform(item) {
deleteModal.open({ $emit: actionComplete }, [item], context);
}
function allowed(/*item*/) {
return $q.all([
statePromise,
// This rule is made up and should therefore always pass. I assume at some point there
// will be a valid rule similar to this that we will want to use.
policy.ifAllowed({ rules: [['neutron', 'pool_member_delete']] })
]);
}
function deleteItem(id) {
return api.deleteMember(poolId, id);
}
function actionComplete(eventType) {
if (eventType === context.successEvent) {
// Success, go back to pool details page
var path = 'project/load_balancer/' +
loadbalancerId + '/listeners/' + listenerId + '/pools/' + poolId;
$location.path(path);
}
$route.reload();
}
}
})();

View File

@ -0,0 +1,154 @@
/*
* Copyright 2017 Walmart.
*
* 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('LBaaS v2 Member Delete Service', function() {
var service, policy, modal, lbaasv2Api, $scope, $location, $q, toast, member;
function allowed(item) {
spyOn(policy, 'ifAllowed').and.returnValue(makePromise());
var promise = service.allowed(item);
var allowed;
promise.then(function() {
allowed = true;
}, function() {
allowed = false;
});
$scope.$apply();
expect(policy.ifAllowed).toHaveBeenCalledWith({rules: [['neutron', 'pool_member_delete']]});
return allowed;
}
function makePromise(reject) {
var def = $q.defer();
def[reject ? 'reject' : 'resolve']();
return def.promise;
}
function isActionable(id) {
if (id === 'active') {
return $q.when();
} else {
return $q.reject();
}
}
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.framework.widgets'));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(function() {
member = { id: '1', name: 'Member1' };
});
beforeEach(module(function($provide) {
$provide.value('$uibModal', {
open: function() {
return {
result: makePromise()
};
}
});
$provide.value('horizon.app.core.openstack-service-api.lbaasv2', {
deleteMember: function() {
return makePromise();
}
});
$provide.value('$location', {
path: function() {
return '';
}
});
}));
beforeEach(inject(function ($injector) {
policy = $injector.get('horizon.app.core.openstack-service-api.policy');
lbaasv2Api = $injector.get('horizon.app.core.openstack-service-api.lbaasv2');
modal = $injector.get('horizon.framework.widgets.modal.deleteModalService');
$scope = $injector.get('$rootScope').$new();
$location = $injector.get('$location');
$q = $injector.get('$q');
toast = $injector.get('horizon.framework.widgets.toast.service');
service = $injector.get('horizon.dashboard.project.lbaasv2.members.actions.delete');
service.init('1', '2', '3', isActionable('active'));
$scope.$apply();
}));
it('should have the "allowed" and "perform" functions', function() {
expect(service.allowed).toBeDefined();
expect(service.perform).toBeDefined();
});
it('should allow deleting member from load balancer in ACTIVE state', function() {
expect(allowed()).toBe(true);
});
it('should not allow deleting member from load balancer in a PENDING state', function() {
service.init('1', '2', '3', isActionable('pending'));
expect(allowed()).toBe(false);
});
it('should open the delete modal', function() {
spyOn(modal, 'open');
service.perform(member);
$scope.$apply();
expect(modal.open.calls.count()).toBe(1);
var args = modal.open.calls.argsFor(0);
expect(args.length).toBe(3);
expect(args[0]).toEqual({ $emit: jasmine.any(Function) });
expect(args[1]).toEqual([member]);
expect(args[2]).toEqual(jasmine.objectContaining({
labels: jasmine.any(Object),
deleteEntity: jasmine.any(Function)
}));
expect(args[2].labels.title).toBe('Confirm Delete Member');
});
it('should pass function to modal that deletes the member', function() {
spyOn(modal, 'open').and.callThrough();
spyOn(lbaasv2Api, 'deleteMember').and.callThrough();
service.perform(member);
$scope.$apply();
expect(lbaasv2Api.deleteMember.calls.count()).toBe(1);
expect(lbaasv2Api.deleteMember).toHaveBeenCalledWith('3', '1');
});
it('should show message if any items fail to be deleted', function() {
spyOn(modal, 'open').and.callThrough();
spyOn(lbaasv2Api, 'deleteMember').and.returnValue(makePromise(true));
spyOn(toast, 'add');
service.perform(member);
$scope.$apply();
expect(modal.open).toHaveBeenCalled();
expect(lbaasv2Api.deleteMember.calls.count()).toBe(1);
expect(toast.add).toHaveBeenCalledWith('error', 'The following member could not ' +
'be deleted: Member1.');
});
it('should return to listener details after delete', function() {
spyOn($location, 'path');
spyOn(toast, 'add');
service.perform(member);
$scope.$apply();
expect($location.path).toHaveBeenCalledWith('project/load_balancer/1/listeners/2/pools/3');
expect(toast.add).toHaveBeenCalledWith('success', 'Deleted member: Member1.');
});
});
})();

View File

@ -23,6 +23,7 @@
rowActions.$inject = [
'horizon.framework.util.i18n.gettext',
'horizon.dashboard.project.lbaasv2.loadbalancers.service',
'horizon.dashboard.project.lbaasv2.members.actions.delete',
'horizon.dashboard.project.lbaasv2.members.actions.edit-member.modal.service'
];
@ -39,8 +40,8 @@
* @returns Members row actions service object.
*/
function rowActions(gettext, loadBalancersService, editMember) {
var loadBalancerIsActionable, poolId;
function rowActions(gettext, loadBalancersService, deleteService, editMember) {
var loadBalancerIsActionable, loadbalancerId, listenerId, poolId;
var service = {
actions: actions,
@ -51,9 +52,11 @@
///////////////
function init(loadbalancerId, _poolId_) {
loadBalancerIsActionable = loadBalancersService.isActionable(loadbalancerId);
function init(_loadbalancerId_, _listenerId_, _poolId_) {
loadbalancerId = _loadbalancerId_;
listenerId = _listenerId_;
poolId = _poolId_;
loadBalancerIsActionable = loadBalancersService.isActionable(loadbalancerId);
return service;
}
@ -63,6 +66,12 @@
template: {
text: gettext('Edit')
}
},{
service: deleteService.init(loadbalancerId, listenerId, poolId, loadBalancerIsActionable),
template: {
text: gettext('Delete Member'),
type: 'delete'
}
}];
}
}

View File

@ -28,15 +28,16 @@
beforeEach(inject(function ($injector) {
var rowActionsService = $injector.get(
'horizon.dashboard.project.lbaasv2.members.actions.rowActions');
actions = rowActionsService.init('1', '2').actions();
actions = rowActionsService.init('1', '2', '3').actions();
var loadbalancerService = $injector.get(
'horizon.dashboard.project.lbaasv2.loadbalancers.service');
spyOn(loadbalancerService, 'isActionable').and.returnValue(true);
}));
it('should define correct table row actions', function() {
expect(actions.length).toBe(1);
expect(actions.length).toBe(2);
expect(actions[0].template.text).toBe('Edit');
expect(actions[1].template.text).toBe('Delete Member');
});
it('should have the "allowed" and "perform" functions', function() {

View File

@ -50,7 +50,8 @@
ctrl.loading = true;
ctrl.error = false;
ctrl.actions = rowActions.init($routeParams.loadbalancerId, $routeParams.poolId).actions;
ctrl.actions = rowActions.init($routeParams.loadbalancerId,
$routeParams.listenerId, $routeParams.poolId).actions;
ctrl.loadbalancerId = $routeParams.loadbalancerId;
ctrl.listenerId = $routeParams.listenerId;
ctrl.poolId = $routeParams.poolId;

View File

@ -101,7 +101,7 @@
expect(ctrl.operatingStatus).toBeDefined();
expect(ctrl.provisioningStatus).toBeDefined();
expect(ctrl.actions).toBe('member-actions');
expect(actions.init).toHaveBeenCalledWith('loadbalancerId', 'poolId');
expect(actions.init).toHaveBeenCalledWith('loadbalancerId', 'listenerId', 'poolId');
});
it('should throw error on API fail', function() {

View File

@ -55,7 +55,7 @@
ctrl.loadbalancerId = $routeParams.loadbalancerId;
ctrl.listenerId = $routeParams.listenerId;
ctrl.poolId = $routeParams.poolId;
ctrl.rowActions = rowActions.init(ctrl.loadbalancerId, ctrl.poolId);
ctrl.rowActions = rowActions.init(ctrl.loadbalancerId, ctrl.listenerId, ctrl.poolId);
ctrl.batchActions = batchActions.init(ctrl.loadbalancerId);
ctrl.operatingStatus = loadBalancersService.operatingStatus;
ctrl.provisioningStatus = loadBalancersService.provisioningStatus;

View File

@ -18,7 +18,7 @@
'use strict';
describe('LBaaS v2 Members Table Controller', function() {
var controller, lbaasv2API, scope;
var controller, lbaasv2API, scope, actions;
var items = [{ foo: 'bar' }];
var apiFail = false;
@ -43,19 +43,28 @@
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function($provide) {
$provide.value('$uibModal', {});
$provide.value('horizon.dashboard.project.lbaasv2.members.actions.rowActions', {
init: function() {
return {
actions: 'member-actions'
};
}
});
}));
beforeEach(inject(function($injector) {
lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2');
actions = $injector.get('horizon.dashboard.project.lbaasv2.members.actions.rowActions');
controller = $injector.get('$controller');
spyOn(lbaasv2API, 'getMembers').and.callFake(fakeAPI);
spyOn(actions, 'init').and.callThrough();
}));
function createController() {
return controller('MembersTableController', {
$scope: scope,
$routeParams: {
loadbalancerId: 'loadbaancerId',
loadbalancerId: 'loadbalancerId',
listenerId: 'listenerId',
poolId: 'poolId'
}});