Commit 9a88246f authored by Lucas Palm's avatar Lucas Palm Committed by Justin Pomeroy

Add the angular LBaaS V2 Edit Action for Listeners

This change implements the edit row action service for the listeners
table.  The edit action of a listener includes the ability to not
only edit the listener itself, but all resources underneath it in the
load balancer hierarchy.

Partially-Implements: blueprint horizon-lbaas-v2-ui
Change-Id: I5fcb20eecbee580f9db5c71da5a2ec84a6f359f9
parent d424350b
This diff is collapsed.
......@@ -42,6 +42,7 @@
editLoadBalancer: editLoadBalancer,
getListeners: getListeners,
getListener: getListener,
editListener: editListener,
getPool: getPool,
getMembers: getMembers,
getMember: getMember,
......@@ -110,7 +111,7 @@
*/
function editLoadBalancer(id, spec) {
return apiService.put('/api/lbaas/loadbalancers/' + id + '/', spec)
return apiService.put('/api/lbaas/loadbalancers/' + id, spec)
.error(function () {
toastService.add('error', gettext('Unable to update load balancer.'));
});
......@@ -146,15 +147,37 @@
* Get a single listener by ID.
* @param {string} id
* Specifies the id of the listener to request.
* @param {boolean} includeChildResources
* If true, all child resources below the listener will be included in the response.
*/
function getListener(id) {
return apiService.get('/api/lbaas/listeners/' + id)
function getListener(id, includeChildResources) {
var params = includeChildResources
? {'params': {'includeChildResources': includeChildResources}}
: {};
return apiService.get('/api/lbaas/listeners/' + id, params)
.error(function () {
toastService.add('error', gettext('Unable to retrieve listener.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.editListener
* @description
* Edit a listener
* @param {string} id
* Specifies the id of the listener to update.
* @param {object} spec
* Specifies the data used to update the listener.
*/
function editListener(id, spec) {
return apiService.put('/api/lbaas/listeners/' + id, spec)
.error(function () {
toastService.add('error', gettext('Unable to update listener.'));
});
}
// Pools
/**
......
......@@ -76,9 +76,26 @@
"func": "getListener",
"method": "get",
"path": "/api/lbaas/listeners/1234",
"data": {
"params": {
"includeChildResources": true
}
},
"error": "Unable to retrieve listener.",
"testInput": [
'1234'
'1234',
true
]
},
{
"func": "getListener",
"method": "get",
"path": "/api/lbaas/listeners/1234",
"data": {},
"error": "Unable to retrieve listener.",
"testInput": [
'1234',
false
]
},
{
......@@ -131,13 +148,24 @@
{
"func": "editLoadBalancer",
"method": "put",
"path": "/api/lbaas/loadbalancers/1234/",
"path": "/api/lbaas/loadbalancers/1234",
"error": "Unable to update load balancer.",
"data": { "name": "loadbalancer-1" },
"testInput": [
"1234",
{ "name": "loadbalancer-1" }
]
},
{
"func": "editListener",
"method": "put",
"path": "/api/lbaas/listeners/1234",
"error": "Unable to update listener.",
"data": { "name": "listener-1" },
"testInput": [
"1234",
{ "name": "listener-1" }
]
}
];
......
......@@ -44,6 +44,10 @@
div.tab-pane > dl {
margin-top: 12px;
}
actions + dl {
clear: both;
}
}
/* Load Balancer Wizard */
......
/*
* 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.loadbalancers')
.controller('EditListenerWizardController', EditListenerWizardController);
EditListenerWizardController.$inject = [
'$scope',
'$q',
'horizon.dashboard.project.lbaasv2.workflow.model',
'horizon.dashboard.project.lbaasv2.workflow.workflow',
'horizon.framework.util.i18n.gettext'
];
/**
* @ngdoc controller
* @name EditListenerWizardController
*
* @description
* Controller for the LBaaS v2 edit listener wizard.
*
* @param $scope The angular scope object.
* @param $q The angular service for promises.
* @param model The LBaaS V2 workflow model service.
* @param workflowService The LBaaS V2 workflow service.
* @param gettext The horizon gettext function for translation.
* @returns undefined
*/
function EditListenerWizardController($scope, $q, model, workflowService, gettext) {
var scope = $scope;
var defer = $q.defer();
scope.model = model;
scope.submit = scope.model.submit;
scope.workflow = workflowService(
gettext('Update Listener'),
'fa fa-pencil', ['listener'],
defer.promise);
scope.model.initialize('listener', scope.launchContext.id).then(addSteps).then(ready);
function addSteps() {
var steps = scope.model.visibleResources;
steps.map(getStep).forEach(function addStep(step) {
if (!stepExists(step.id)) {
scope.workflow.append(step);
}
});
}
function getStep(id) {
return scope.workflow.allSteps.filter(function findStep(step) {
return step.id === id;
})[0];
}
function stepExists(id) {
return scope.workflow.steps.some(function exists(step) {
return step.id === id;
});
}
function ready() {
defer.resolve();
}
}
})();
/*
* 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 Edit Listener Wizard Controller', function() {
var ctrl, workflowSpy, $q, scope;
var model = {
submit: function() {
return 'updated';
},
initialize: function() {
var defer = $q.defer();
defer.resolve();
return defer.promise;
}
};
var workflow = {
steps: [{id: 'listener'}],
allSteps: [{id: 'listener'}, {id: 'pool'}, {id: 'monitor'}],
append: angular.noop
};
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function ($provide) {
workflowSpy = jasmine.createSpy('workflow').and.returnValue(workflow);
$provide.value('horizon.dashboard.project.lbaasv2.workflow.model', model);
$provide.value('horizon.dashboard.project.lbaasv2.workflow.workflow', workflowSpy);
}));
beforeEach(inject(function ($controller, $injector) {
$q = $injector.get('$q');
scope = $injector.get('$rootScope').$new();
scope.launchContext = { id: '1234' };
spyOn(model, 'initialize').and.callThrough();
ctrl = $controller('EditListenerWizardController', { $scope: scope });
}));
it('defines the controller', function() {
expect(ctrl).toBeDefined();
});
it('calls initialize on the given model', function() {
expect(model.initialize).toHaveBeenCalledWith('listener', '1234');
});
it('sets scope.workflow to the given workflow', function() {
expect(scope.workflow).toBe(workflow);
});
it('initializes workflow with correct properties', function() {
expect(workflowSpy).toHaveBeenCalledWith('Update Listener',
'fa fa-pencil', ['listener'], jasmine.any(Object));
});
it('defines scope.submit', function() {
expect(scope.submit).toBe(model.submit);
expect(scope.submit()).toBe('updated');
});
it('adds necessary steps after initializing', function() {
model.visibleResources = ['listener', 'pool', 'monitor'];
spyOn(workflow, 'append');
scope.$apply();
expect(workflow.append).toHaveBeenCalledWith({id: 'pool'});
expect(workflow.append).toHaveBeenCalledWith({id: 'monitor'});
});
});
})();
/*
* 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.rowActions',
tableRowActions);
tableRowActions.$inject = [
'$q',
'$route',
'horizon.dashboard.project.lbaasv2.workflow.modal',
'horizon.app.core.openstack-service-api.policy',
'horizon.framework.util.i18n.gettext',
'horizon.dashboard.project.lbaasv2.loadbalancers.service'
];
/**
* @ngdoc service
* @ngname horizon.dashboard.project.lbaasv2.listeners.actions.rowActions
*
* @description
* Provides the service for the Listener table row actions.
*
* @param $q The angular service for promises.
* @param $route The angular $route service.
* @param workflowModal The LBaaS workflow modal service.
* @param policy The horizon policy service.
* @param gettext The horizon gettext function for translation.
* @param loadBalancersService The LBaaS v2 load balancers service.
* @returns Listeners row actions service object.
*/
function tableRowActions($q, $route, workflowModal, policy, gettext, loadBalancersService) {
var edit = workflowModal.init({
controller: 'EditListenerWizardController',
message: gettext('The listener has been updated.'),
handle: onEdit,
allowed: canEdit
});
var service = {
actions: actions,
init: init
};
var loadBalancerIsActive;
return service;
///////////////
function init(loadbalancerId) {
loadBalancerIsActive = loadBalancersService.isActive(loadbalancerId);
return service;
}
function actions() {
return [{
service: edit,
template: {
text: gettext('Edit')
}
}];
}
function canEdit(/*item*/) {
return $q.all([
loadBalancerIsActive,
policy.ifAllowed({ rules: [['neutron', 'update_listener']] })
]);
}
function onEdit(/*response*/) {
$route.reload();
}
}
})();
/*
* 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 Table Row Actions Service', function() {
var scope, $route, $q, actions, policy, init;
function canEdit(item) {
spyOn(policy, 'ifAllowed').and.returnValue(true);
var promise = actions[0].service.allowed(item);
var allowed;
promise.then(function() {
allowed = true;
}, function() {
allowed = false;
});
scope.$apply();
expect(policy.ifAllowed).toHaveBeenCalledWith({rules: [['neutron', 'update_listener']]});
return allowed;
}
function isActiveMock(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.toast'));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function($provide) {
var response = {
data: {
id: '1'
}
};
var modal = {
open: function() {
return {
result: {
then: function(func) {
func(response);
}
}
};
}
};
$provide.value('$modal', modal);
}));
beforeEach(inject(function ($injector) {
scope = $injector.get('$rootScope').$new();
$q = $injector.get('$q');
$route = $injector.get('$route');
policy = $injector.get('horizon.app.core.openstack-service-api.policy');
var rowActionsService = $injector.get(
'horizon.dashboard.project.lbaasv2.listeners.actions.rowActions');
actions = rowActionsService.actions();
init = rowActionsService.init;
var loadbalancerService = $injector.get(
'horizon.dashboard.project.lbaasv2.loadbalancers.service');
spyOn(loadbalancerService, 'isActive').and.callFake(isActiveMock);
}));
it('should define correct table row actions', function() {
expect(actions.length).toBe(1);
expect(actions[0].template.text).toBe('Edit');
});
it('should allow editing a listener of an ACTIVE load balancer', function() {
init('active');
expect(canEdit({listenerId: '1234'})).toBe(true);
});
it('should not allow editing a listener of a non-ACTIVE load balancer', function() {
init('non-active');
expect(canEdit({listenerId: '1234'})).toBe(false);
});
it('should have the "allowed" and "perform" functions', function() {
actions.forEach(function(action) {
expect(action.service.allowed).toBeDefined();
expect(action.service.perform).toBeDefined();
});
});
it('should reload table after edit', function() {
spyOn($route, 'reload').and.callThrough();
actions[0].service.perform();
expect($route.reload).toHaveBeenCalled();
});
});
})();
......@@ -22,6 +22,7 @@
ListenerDetailController.$inject = [
'horizon.app.core.openstack-service-api.lbaasv2',
'horizon.dashboard.project.lbaasv2.listeners.actions.rowActions',
'$routeParams'
];
......@@ -33,13 +34,16 @@
* Controller for the LBaaS v2 listener detail page.
*
* @param api The LBaaS v2 API service.
* @param rowActions The listener row actions service.
* @param $routeParams The angular $routeParams service.
* @returns undefined
*/
function ListenerDetailController(api, $routeParams) {
function ListenerDetailController(api, rowActions, $routeParams) {
var ctrl = this;
ctrl.actions = rowActions.actions;
init();
////////////////////////////////
......
......@@ -29,12 +29,16 @@
///////////////////////
beforeEach(module('horizon.framework.util.http'));
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.framework.widgets.toast'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function($provide) {
$provide.value('$modal', {});
}));
beforeEach(inject(function($injector) {
lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2');
spyOn(lbaasv2API, 'getListener').and.callFake(fakeAPI);
......
......@@ -8,6 +8,7 @@
<p ng-if="::ctrl.listener.description">{$ ::ctrl.listener.description $}</p>
</div>
<div class="detail-page">
<actions allowed="ctrl.actions" type="row" item="ctrl.listener" ng-if="ctrl.listener" class="pull-right"></actions>
<dl class="dl-horizontal">
<div>
<dt translate>Listener ID</dt>
......
......@@ -23,7 +23,7 @@
ListenersTableController.$inject = [
'horizon.app.core.openstack-service-api.lbaasv2',
'$routeParams',
'horizon.dashboard.project.lbaasv2.loadbalancers.actions.batchActions'
'horizon.dashboard.project.lbaasv2.listeners.actions.rowActions'
];
/**
......@@ -35,18 +35,18 @@
*
* @param api The LBaaS V2 service API.
* @param $routeParams The angular $routeParams service.
* @param batchActions The load balancer batch actions service.
* @param rowActions The listener row actions service.
* @returns undefined
*/
function ListenersTableController(api, $routeParams, batchActions) {
function ListenersTableController(api, $routeParams, rowActions) {
var ctrl = this;
ctrl.items = [];
ctrl.src = [];
ctrl.checked = {};
ctrl.batchActions = batchActions;
ctrl.loadbalancerId = $routeParams.loadbalancerId;
ctrl.rowActions = rowActions.init(ctrl.loadbalancerId);
init();
......
......@@ -17,7 +17,7 @@
'use strict';
describe('LBaaS v2 Listeners Table Controller', function() {
var controller, lbaasv2API;
var controller, lbaasv2API, rowActions;
var items = [];
function fakeAPI() {
......@@ -28,6 +28,10 @@
};
}
function initMock() {
return rowActions;
}
///////////////////////
beforeEach(module('horizon.framework.widgets.toast'));
......@@ -43,6 +47,8 @@
beforeEach(inject(function($injector) {
lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2');
controller = $injector.get('$controller');
rowActions = $injector.get('horizon.dashboard.project.lbaasv2.listeners.actions.rowActions');
spyOn(rowActions, 'init').and.callFake(initMock);
spyOn(lbaasv2API, 'getListeners').and.callFake(fakeAPI);
}));
......@@ -57,7 +63,9 @@
expect(ctrl.items).toEqual([]);
expect(ctrl.src).toEqual(items);
expect(ctrl.checked).toEqual({});
expect(ctrl.batchActions).toBeDefined();
expect(ctrl.loadbalancerId).toEqual('1234');
expect(rowActions.init).toHaveBeenCalledWith(ctrl.loadbalancerId);
expect(ctrl.rowActions).toEqual(rowActions);
});
it('should invoke lbaasv2 apis', function() {
......@@ -65,5 +73,10 @@
expect(lbaasv2API.getListeners).toHaveBeenCalled();
});
it('should init the rowactions', function() {
createController();
expect(lbaasv2API.getListeners).toHaveBeenCalled();
});
});
})();
......@@ -70,6 +70,13 @@
<td class="rsp-p1">{$ ::item.description | noValue $}</td>
<td class="rsp-p1">{$ ::item.protocol$}</td>
<td class="rsp-p1">{$ ::item.protocol_port$}</td>
<td class="action-col">
<!--
Table-row-action-column:
Actions taken here apply to a single item/row.
-->
<actions allowed="table.rowActions.actions" type="row" item="item"></actions>
</td>
</tr>
<tr ng-repeat-end class="detail-row">
......
......@@ -35,7 +35,8 @@
<select class="form-control input-sm" name="listener-protocol"
id="listener-protocol"
ng-options="protocol for protocol in model.listenerProtocols"
ng-model="model.spec.listener.protocol" ng-required="model.context.resource === 'listener'">
ng-model="model.spec.listener.protocol" ng-required="model.context.resource === 'listener'"