Commit d3c75ce0 authored by Justin Pomeroy's avatar Justin Pomeroy

Add listener delete actions

Adds the single and multiple delete actions for listeners.

Partially-Implements: blueprint horizon-lbaas-v2-ui
Change-Id: I3a05319e4858e1c903b54c46831c1e323e585c61
parent af63722e
......@@ -551,6 +551,14 @@ class Listener(generic.View):
kwargs = {'listener_id': listener_id}
update_listener(request, **kwargs)
@rest_utils.ajax()
def delete(self, request, listener_id):
"""Delete a specific listener.
http://localhost/api/lbaas/listeners/cc758c90-3d98-4ea1-af44-aab405c9c915
"""
neutronclient(request).delete_listener(listener_id)
@urls.register
class Pool(generic.View):
......
......@@ -45,6 +45,7 @@
getListener: getListener,
createListener: createListener,
editListener: editListener,
deleteListener: deleteListener,
getPool: getPool,
getMembers: getMembers,
getMember: getMember,
......@@ -214,6 +215,22 @@
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.deleteListener
* @description
* Delete a single listener by ID
* @param {string} id
* @param {boolean} quiet
* Specifies the id of the listener to delete.
*/
function deleteListener(id, quiet) {
var promise = apiService.delete('/api/lbaas/listeners/' + id);
return quiet ? promise : promise.error(function () {
toastService.add('error', gettext('Unable to delete listener.'));
});
}
// Pools
/**
......
......@@ -149,6 +149,13 @@
error: 'Unable to update listener.',
data: { name: 'listener-1' },
testInput: [ '1234', { name: 'listener-1' } ]
},
{
func: 'deleteListener',
method: 'delete',
path: '/api/lbaas/listeners/1234',
error: 'Unable to delete listener.',
testInput: [ '1234' ]
}
];
......@@ -165,6 +172,11 @@
expect(service.deleteLoadBalancer("whatever", true)).toBe("promise");
});
it('supresses the error if instructed for deleteListener', function() {
spyOn(apiService, 'delete').and.returnValue("promise");
expect(service.deleteListener("whatever", true)).toBe("promise");
});
});
})();
......@@ -27,7 +27,8 @@
'horizon.dashboard.project.lbaasv2.workflow.modal',
'horizon.app.core.openstack-service-api.policy',
'horizon.framework.util.i18n.gettext',
'horizon.dashboard.project.lbaasv2.loadbalancers.service'
'horizon.dashboard.project.lbaasv2.loadbalancers.service',
'horizon.dashboard.project.lbaasv2.listeners.actions.delete'
];
/**
......@@ -43,11 +44,14 @@
* @param policy The horizon policy service.
* @param gettext The horizon gettext function for translation.
* @param loadBalancersService The LBaaS v2 load balancers service.
* @param deleteService The LBaaS v2 listeners delete service.
* @returns Listeners table batch actions service object.
*/
function tableBatchActions($q, $location, workflowModal, policy, gettext, loadBalancersService) {
var loadBalancerIsActive, loadBalancerId;
function tableBatchActions(
$q, $location, workflowModal, policy, gettext, loadBalancersService, deleteService
) {
var loadBalancerIsActionable, loadBalancerId, handler;
var create = workflowModal.init({
controller: 'CreateListenerWizardController',
......@@ -65,9 +69,10 @@
///////////////
function init(loadbalancerId) {
loadBalancerId = loadbalancerId;
loadBalancerIsActive = loadBalancersService.isActive(loadbalancerId);
function init(_loadBalancerId_, _handler_) {
loadBalancerId = _loadBalancerId_;
handler = _handler_;
loadBalancerIsActionable = loadBalancersService.isActionable(loadBalancerId);
return service;
}
......@@ -78,12 +83,18 @@
type: 'create',
text: gettext('Create Listener')
}
},{
service: deleteService.init(loadBalancerId, loadBalancerIsActionable, handler),
template: {
text: gettext('Delete Listeners'),
type: 'delete-selected'
}
}];
}
function canCreate() {
return $q.all([
loadBalancerIsActive,
loadBalancerIsActionable,
policy.ifAllowed({ rules: [['neutron', 'create_listener']] })
]);
}
......
......@@ -21,7 +21,7 @@
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.framework.widgets.toast'));
beforeEach(module('horizon.framework.widgets'));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
......@@ -44,7 +44,7 @@
};
$provide.value('$modal', modal);
$provide.value('horizon.dashboard.project.lbaasv2.loadbalancers.service', {
isActive: function() {
isActionable: function() {
return $q.when();
}
});
......@@ -67,8 +67,9 @@
}));
it('should define correct table batch actions', function() {
expect(actions.length).toBe(1);
expect(actions.length).toBe(2);
expect(actions[0].template.text).toBe('Create Listener');
expect(actions[1].template.text).toBe('Delete Listeners');
});
it('should have the "allowed" and "perform" functions', function() {
......
/*
* Copyright 2016 IBM Corp.
*
* 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.listeners')
.factory('horizon.dashboard.project.lbaasv2.listeners.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.widgets.toast.service',
'horizon.framework.util.q.extensions',
'horizon.framework.util.i18n.gettext'
];
/**
* @ngDoc factory
* @name horizon.dashboard.project.lbaasv2.listeners.actions.deleteService
* @description
* Brings up the delete listeners confirmation modal dialog.
* On submit, deletes selected listeners.
* 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 toast The horizon message service.
* @param qExtensions Horizon extensions to the $q service.
* @param gettext The horizon gettext function for translation.
* @returns The load balancers table delete service.
*/
function deleteService(
$q, $location, $route, deleteModal, api, policy, toast, qExtensions, gettext
) {
var loadbalancerId, statePromise, handler;
var context = {
labels: {
title: gettext('Confirm Delete Listeners'),
message: gettext('You have selected "%s". Please confirm your selection. Deleted ' +
'listeners are not recoverable.'),
submit: gettext('Delete Listeners'),
success: gettext('Deleted listeners: %s.'),
error: gettext('The following listeners could not be deleted, possibly due to ' +
'existing pools: %s.')
},
deleteEntity: deleteItem,
successEvent: 'success',
failedEvent: 'error'
};
var service = {
perform: perform,
allowed: allowed,
init: init
};
return service;
//////////////
function init(_loadbalancerId_, _statePromise_, _handler_) {
loadbalancerId = _loadbalancerId_;
statePromise = _statePromise_;
handler = _handler_;
return service;
}
function perform(items) {
if (!angular.isArray(items)) {
items = [items];
}
deleteModal.open({ $emit: actionComplete }, items, 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', 'delete_listener']] })
]);
}
function deleteItem(id) {
return api.deleteListener(id, true);
}
function actionComplete(eventType) {
if (angular.isFunction(handler)) {
handler();
} else if (eventType === context.failedEvent) {
// Action failed, reload the page
$route.reload();
} else {
// If the user is on the listeners table then just reload the page, otherwise they
// are on the details page and we return to the table.
var regex = new RegExp('project\/ngloadbalancersv2\/' + loadbalancerId + '(\/)?$');
if (regex.test($location.path())) {
$route.reload();
} else {
$location.path('project/ngloadbalancersv2/' + loadbalancerId);
}
}
}
}
})();
/*
* Copyright 2016 IBM Corp.
*
* 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 Listeners Delete Service', function() {
var service, policy, modal, lbaasv2Api, $scope, $route, $location, $q, toast, items, path;
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', 'delete_listener']]});
return allowed;
}
function makePromise(reject) {
var def = $q.defer();
def[reject ? 'reject' : 'resolve']();
return def.promise;
}
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() {
items = [{ id: '1', name: 'First' },
{ id: '2', name: 'Second' }];
});
beforeEach(module(function($provide) {
$provide.value('$modal', {
open: function() {
return {
result: makePromise()
};
}
});
$provide.value('horizon.app.core.openstack-service-api.lbaasv2', {
deleteListener: function() {
return makePromise();
}
});
$provide.value('$location', {
path: function() {
return path;
}
});
}));
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();
$route = $injector.get('$route');
$location = $injector.get('$location');
$q = $injector.get('$q');
toast = $injector.get('horizon.framework.widgets.toast.service');
service = $injector.get('horizon.dashboard.project.lbaasv2.listeners.actions.delete');
service.init('1');
}));
it('should have the "allowed" and "perform" functions', function() {
expect(service.allowed).toBeDefined();
expect(service.perform).toBeDefined();
});
it('should check policy to allow deleting a listener (single)', function() {
expect(allowed(items[0])).toBe(true);
});
it('should check policy to allow deleting a listener (batch)', function() {
expect(allowed()).toBe(true);
});
it('should open the delete modal', function() {
spyOn(modal, 'open');
service.perform(items[0]);
$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([jasmine.objectContaining({ id: '1' })]);
expect(args[2]).toEqual(jasmine.objectContaining({
labels: jasmine.any(Object),
deleteEntity: jasmine.any(Function)
}));
expect(args[2].labels.title).toBe('Confirm Delete Listeners');
});
it('should pass function to modal that deletes listeners', function() {
spyOn(modal, 'open').and.callThrough();
spyOn(lbaasv2Api, 'deleteListener').and.callThrough();
service.perform(items[0]);
$scope.$apply();
expect(lbaasv2Api.deleteListener.calls.count()).toBe(1);
expect(lbaasv2Api.deleteListener).toHaveBeenCalledWith('1', true);
});
it('should show message if any items fail to be deleted', function() {
spyOn(modal, 'open').and.callThrough();
spyOn(lbaasv2Api, 'deleteListener').and.returnValue(makePromise(true));
spyOn(toast, 'add');
items.splice(1, 1);
service.perform(items);
$scope.$apply();
expect(modal.open).toHaveBeenCalled();
expect(lbaasv2Api.deleteListener.calls.count()).toBe(1);
expect(toast.add).toHaveBeenCalledWith('error', 'The following listeners could not ' +
'be deleted, possibly due to existing pools: First.');
});
it('should reload table after delete', function() {
path = 'project/ngloadbalancersv2/1';
spyOn($route, 'reload');
service.perform(items);
$scope.$apply();
expect($route.reload).toHaveBeenCalled();
});
it('should return to table after delete if on detail page', function() {
path = 'project/ngloadbalancersv2/1/listeners/2';
spyOn($location, 'path');
spyOn(toast, 'add');
service.perform(items[0]);
$scope.$apply();
expect($location.path).toHaveBeenCalledWith('project/ngloadbalancersv2/1');
expect(toast.add).toHaveBeenCalledWith('success', 'Deleted listeners: First.');
});
it('should call handler function if provided', function() {
var handler = { handler: angular.noop };
spyOn(handler, 'handler');
service.init('1', null, handler.handler);
service.perform(items);
$scope.$apply();
expect(handler.handler).toHaveBeenCalled();
});
});
})();
......@@ -27,7 +27,8 @@
'horizon.dashboard.project.lbaasv2.workflow.modal',
'horizon.app.core.openstack-service-api.policy',
'horizon.framework.util.i18n.gettext',
'horizon.dashboard.project.lbaasv2.loadbalancers.service'
'horizon.dashboard.project.lbaasv2.loadbalancers.service',
'horizon.dashboard.project.lbaasv2.listeners.actions.delete'
];
/**
......@@ -43,10 +44,14 @@
* @param policy The horizon policy service.
* @param gettext The horizon gettext function for translation.
* @param loadBalancersService The LBaaS v2 load balancers service.
* @param deleteService The LBaaS v2 listeners delete service.
* @returns Listeners row actions service object.
*/
function tableRowActions($q, $route, workflowModal, policy, gettext, loadBalancersService) {
function tableRowActions(
$q, $route, workflowModal, policy, gettext, loadBalancersService, deleteService
) {
var loadbalancerId, loadBalancerIsActionable, handler;
var edit = workflowModal.init({
controller: 'EditListenerWizardController',
......@@ -60,14 +65,14 @@
init: init
};
var loadBalancerIsActive;
return service;
///////////////
function init(loadbalancerId) {
loadBalancerIsActive = loadBalancersService.isActive(loadbalancerId);
function init(_loadbalancerId_, _handler_) {
loadbalancerId = _loadbalancerId_;
handler = _handler_;
loadBalancerIsActionable = loadBalancersService.isActionable(loadbalancerId);
return service;
}
......@@ -77,12 +82,18 @@
template: {
text: gettext('Edit')
}
},{
service: deleteService.init(loadbalancerId, loadBalancerIsActionable, handler),
template: {
text: gettext('Delete Listener'),
type: 'delete'
}
}];
}
function canEdit(/*item*/) {
return $q.all([
loadBalancerIsActive,
loadBalancerIsActionable,
policy.ifAllowed({ rules: [['neutron', 'update_listener']] })
]);
}
......
......@@ -33,7 +33,7 @@
return allowed;
}
function isActiveMock(id) {
function isActionableMock(id) {
if (id === 'active') {
return $q.when();
} else {
......@@ -43,7 +43,7 @@
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.framework.widgets.toast'));
beforeEach(module('horizon.framework.widgets'));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
......@@ -78,12 +78,13 @@
init = rowActionsService.init;
var loadbalancerService = $injector.get(
'horizon.dashboard.project.lbaasv2.loadbalancers.service');
spyOn(loadbalancerService, 'isActive').and.callFake(isActiveMock);
spyOn(loadbalancerService, 'isActionable').and.callFake(isActionableMock);
}));
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 Listener');
});
it('should allow editing a listener of an ACTIVE load balancer', function() {
......
......@@ -42,7 +42,7 @@
function ListenerDetailController(api, rowActions, $routeParams) {
var ctrl = this;
ctrl.actions = rowActions.actions;
ctrl.actions = rowActions.init($routeParams.loadbalancerId).actions;
init();
......
......@@ -27,10 +27,22 @@
};
}
function loadbalancerAPI() {
var loadbalancer = { provisioning_status: 'ACTIVE' };
return {
success: function(callback) {
callback(loadbalancer);
},
then: function(callback) {
callback({ data: loadbalancer });
}
};
}
///////////////////////
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.framework.widgets.toast'));
beforeEach(module('horizon.framework.widgets'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
......@@ -42,7 +54,7 @@
beforeEach(inject(function($injector) {
lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2');
spyOn(lbaasv2API, 'getListener').and.callFake(fakeAPI);
spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(fakeAPI);
spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(loadbalancerAPI);
var controller = $injector.get('$controller');
ctrl = controller('ListenerDetailController', {
$routeParams: {
......@@ -55,7 +67,7 @@
it('should invoke lbaasv2 apis', function() {
expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId');
expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('loadbalancerId');
expect(ctrl.loadbalancer).toBe('foo');
expect(ctrl.loadbalancer).toEqual({ provisioning_status: 'ACTIVE' });
expect(ctrl.listener).toBe('foo');
});
......
......@@ -48,8 +48,8 @@
ctrl.src = [];
ctrl.checked = {};
ctrl.loadbalancerId = $routeParams.loadbalancerId;
ctrl.batchActions = batchActions.init(ctrl.loadbalancerId);
ctrl.rowActions = rowActions.init(ctrl.loadbalancerId);
ctrl.batchActions = batchActions.init(ctrl.loadbalancerId, init);
ctrl.rowActions = rowActions.init(ctrl.loadbalancerId, init);
init();
......
......@@ -66,7 +66,7 @@
expect(ctrl.src).toEqual(items);
expect(ctrl.checked).toEqual({});
expect(ctrl.loadbalancerId).toEqual('1234');
expect(rowActions.init).toHaveBeenCalledWith(ctrl.loadbalancerId);
expect(rowActions.init).toHaveBeenCalledWith(ctrl.loadbalancerId, jasmine.any(Function));
expect(ctrl.rowActions).toBeDefined();
expect(ctrl.rowActions).toEqual(rowActions);
expect(ctrl.batchActions).toBeDefined();
......
......@@ -47,6 +47,7 @@
*/
function tableBatchActions($location, workflowModal, basePath, deleteService, policy, gettext) {
var handler;
var create = workflowModal.init({
controller: 'CreateLoadBalancerWizardController',
......@@ -56,13 +57,19 @@
});
var service = {
actions: actions
actions: actions,
init: init
};
return service;
///////////////