From 9a88246fe654acef5c9b386dcdc6d7c6036073a3 Mon Sep 17 00:00:00 2001 From: Lucas Palm Date: Fri, 12 Feb 2016 14:46:20 -0600 Subject: [PATCH] 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 --- neutron_lbaas_dashboard/api/rest/lbaasv2.py | 249 +++++++- .../openstack-service-api/lbaasv2.service.js | 29 +- .../lbaasv2.service.spec.js | 32 +- .../dashboard/project/lbaasv2/lbaasv2.scss | 4 + .../actions/edit/wizard.controller.js | 83 +++ .../actions/edit/wizard.controller.spec.js | 84 +++ .../listeners/actions/row-actions.service.js | 95 +++ .../actions/row-actions.service.spec.js | 113 ++++ .../lbaasv2/listeners/detail.controller.js | 6 +- .../listeners/detail.controller.spec.js | 6 +- .../project/lbaasv2/listeners/detail.html | 1 + .../lbaasv2/listeners/table.controller.js | 8 +- .../listeners/table.controller.spec.js | 17 +- .../project/lbaasv2/listeners/table.html | 7 + .../lbaasv2/workflow/listener/listener.html | 6 +- .../workflow/loadbalancer/loadbalancer.html | 4 +- .../lbaasv2/workflow/members/members.html | 12 +- .../project/lbaasv2/workflow/model.service.js | 172 +++++- .../lbaasv2/workflow/model.service.spec.js | 579 ++++++++++++++---- .../lbaasv2/workflow/monitor/monitor.html | 6 +- .../project/lbaasv2/workflow/pool/pool.html | 6 +- .../lbaasv2/workflow/workflow.service.js | 22 +- .../lbaasv2/workflow/workflow.service.spec.js | 8 + 23 files changed, 1359 insertions(+), 190 deletions(-) create mode 100644 neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/edit/wizard.controller.js create mode 100644 neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/edit/wizard.controller.spec.js create mode 100644 neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/row-actions.service.js create mode 100644 neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/row-actions.service.spec.js diff --git a/neutron_lbaas_dashboard/api/rest/lbaasv2.py b/neutron_lbaas_dashboard/api/rest/lbaasv2.py index b297830..5e77b46 100644 --- a/neutron_lbaas_dashboard/api/rest/lbaasv2.py +++ b/neutron_lbaas_dashboard/api/rest/lbaasv2.py @@ -118,8 +118,19 @@ def add_member(request, **kwargs): """ data = request.DATA - members = data['members'] - index = kwargs['index'] + members = data.get('members') + + if kwargs.get('members_to_add'): + members_to_add = kwargs['members_to_add'] + index = [members.index(member) for member in members + if member['id'] == members_to_add[0]][0] + pool_id = data['pool'].get('id') + loadbalancer_id = data.get('loadbalancer_id') + else: + index = kwargs.get('index') + pool_id = kwargs.get('pool_id') + loadbalancer_id = kwargs.get('loadbalancer_id') + member = members[index] memberSpec = { 'address': member['address'], @@ -128,23 +139,55 @@ def add_member(request, **kwargs): } if member.get('weight'): memberSpec['weight'] = member['weight'] + member = neutronclient(request).create_lbaas_member( - kwargs['pool_id'], {'member': memberSpec}).get('member') + pool_id, {'member': memberSpec}).get('member') index += 1 - if len(members) > index: - args = (request, kwargs['loadbalancer_id'], add_member) - kwargs = {'callback_kwargs': {'pool_id': kwargs['pool_id'], + if kwargs.get('members_to_add'): + args = (request, loadbalancer_id, update_member_list) + members_to_add = kwargs['members_to_add'] + members_to_add.pop(0) + kwargs = {'callback_kwargs': { + 'existing_members': kwargs.get('existing_members'), + 'members_to_add': members_to_add, + 'members_to_delete': kwargs.get('members_to_delete')}} + thread.start_new_thread(poll_loadbalancer_status, args, kwargs) + elif len(members) > index: + args = (request, loadbalancer_id, add_member) + kwargs = {'callback_kwargs': {'pool_id': pool_id, 'index': index}} thread.start_new_thread(poll_loadbalancer_status, args, kwargs) elif data.get('monitor'): - args = (request, kwargs['loadbalancer_id'], add_monitor) - kwargs = {'callback_kwargs': {'pool_id': kwargs['pool_id']}} + args = (request, loadbalancer_id, add_monitor) + kwargs = {'callback_kwargs': {'pool_id': pool_id}} thread.start_new_thread(poll_loadbalancer_status, args, kwargs) return member +def remove_member(request, **kwargs): + """Remove a member from the pool. + + """ + data = request.DATA + loadbalancer_id = data.get('loadbalancer_id') + pool_id = data['pool']['id'] + + if kwargs.get('members_to_delete'): + members_to_delete = kwargs['members_to_delete'] + member_id = members_to_delete.pop(0) + + neutronclient(request).delete_lbaas_member(member_id, pool_id) + + args = (request, loadbalancer_id, update_member_list) + kwargs = {'callback_kwargs': { + 'existing_members': kwargs.get('existing_members'), + 'members_to_add': kwargs.get('members_to_add'), + 'members_to_delete': members_to_delete}} + thread.start_new_thread(poll_loadbalancer_status, args, kwargs) + + def add_monitor(request, **kwargs): """Create a new health monitor for a pool. @@ -167,6 +210,140 @@ def add_monitor(request, **kwargs): {'healthmonitor': monitorSpec}).get('healthmonitor') +def update_loadbalancer(request, **kwargs): + """Update a load balancer. + + """ + data = request.DATA + spec = {} + loadbalancer_id = kwargs.get('loadbalancer_id') + + if data['loadbalancer'].get('name'): + spec['name'] = data['loadbalancer']['name'] + if data['loadbalancer'].get('description'): + spec['description'] = data['loadbalancer']['description'] + return neutronclient(request).update_loadbalancer( + loadbalancer_id, {'loadbalancer': spec}).get('loadbalancer') + + +def update_listener(request, **kwargs): + """Update a listener. + + """ + data = request.DATA + listener_spec = {} + listener_id = data['listener'].get('id') + loadbalancer_id = data.get('loadbalancer_id') + + if data['listener'].get('name'): + listener_spec['name'] = data['listener']['name'] + if data['listener'].get('description'): + listener_spec['description'] = data['listener']['description'] + + listener = neutronclient(request).update_listener( + listener_id, {'listener': listener_spec}).get('listener') + + if data.get('pool'): + args = (request, loadbalancer_id, update_pool) + thread.start_new_thread(poll_loadbalancer_status, args) + + return listener + + +def update_pool(request, **kwargs): + """Update a pool. + + """ + data = request.DATA + pool_spec = {} + pool_id = data['pool'].get('id') + loadbalancer_id = data.get('loadbalancer_id') + + if data['pool'].get('name'): + pool_spec['name'] = data['pool']['name'] + if data['pool'].get('description'): + pool_spec['description'] = data['pool']['description'] + + pools = neutronclient(request).update_lbaas_pool( + pool_id, {'pool': pool_spec}).get('pools') + + # Assemble the lists of member id's to add and remove, if any exist + tenant_id = request.user.project_id + new_members = data.get('members', []) + existing_members = neutronclient(request).list_lbaas_members( + pool_id, tenant_id=tenant_id).get('members') + new_member_ids = [member['id'] for member in new_members] + existing_member_ids = [member['id'] for member in existing_members] + members_to_add = [member_id for member_id in new_member_ids + if member_id not in existing_member_ids] + members_to_delete = [member_id for member_id in existing_member_ids + if member_id not in new_member_ids] + + if members_to_add or members_to_delete: + args = (request, loadbalancer_id, update_member_list) + kwargs = {'callback_kwargs': {'existing_members': existing_members, + 'members_to_add': members_to_add, + 'members_to_delete': members_to_delete}} + thread.start_new_thread(poll_loadbalancer_status, args, kwargs) + elif data.get('monitor'): + args = (request, loadbalancer_id, update_monitor) + thread.start_new_thread(poll_loadbalancer_status, args) + + return pools + + +def update_monitor(request, **kwargs): + """Update a health monitor. + + """ + data = request.DATA + monitor_spec = {} + monitor_id = data['monitor']['id'] + + if data['monitor'].get('interval'): + monitor_spec['delay'] = data['monitor']['interval'] + if data['monitor'].get('timeout'): + monitor_spec['timeout'] = data['monitor']['timeout'] + if data['monitor'].get('retry'): + monitor_spec['max_retries'] = data['monitor']['retry'] + if data['monitor'].get('method'): + monitor_spec['http_method'] = data['monitor']['method'] + if data['monitor'].get('path'): + monitor_spec['url_path'] = data['monitor']['path'] + if data['monitor'].get('status'): + monitor_spec['expected_codes'] = data['monitor']['status'] + + healthmonitor = neutronclient(request).update_lbaas_healthmonitor( + monitor_id, {'healthmonitor': monitor_spec}).get('healthmonitor') + + return healthmonitor + + +def update_member_list(request, **kwargs): + """Update the list of members by adding or removing the necessary members. + + """ + data = request.DATA + loadbalancer_id = data.get('loadbalancer_id') + existing_members = kwargs.get('existing_members') + members_to_add = kwargs.get('members_to_add') + members_to_delete = kwargs.get('members_to_delete') + + if members_to_add: + kwargs = {'existing_members': existing_members, + 'members_to_add': members_to_add, + 'members_to_delete': members_to_delete} + add_member(request, **kwargs) + elif members_to_delete: + kwargs = {'existing_members': existing_members, + 'members_to_add': members_to_add, + 'members_to_delete': members_to_delete} + remove_member(request, **kwargs) + elif data.get('monitor'): + args = (request, loadbalancer_id, update_monitor) + thread.start_new_thread(poll_loadbalancer_status, args) + + @urls.register class LoadBalancers(generic.View): """API for load balancers. @@ -217,7 +394,7 @@ class LoadBalancers(generic.View): @urls.register class LoadBalancer(generic.View): - """API for retrieving a single load balancer. + """API for retrieving, updating, and deleting a single load balancer. """ url_regex = r'lbaas/loadbalancers/(?P[^/]+)/$' @@ -236,14 +413,8 @@ class LoadBalancer(generic.View): """Edit a load balancer. """ - data = request.DATA - spec = {} - if data['loadbalancer'].get('name'): - spec['name'] = data['loadbalancer']['name'] - if data['loadbalancer'].get('description'): - spec['description'] = data['loadbalancer']['description'] - return neutronclient(request).update_loadbalancer( - loadbalancer_id, {'loadbalancer': spec}).get('loadbalancer') + kwargs = {'loadbalancer_id': loadbalancer_id} + update_loadbalancer(request, **kwargs) @urls.register @@ -280,7 +451,7 @@ class Listeners(generic.View): @urls.register class Listener(generic.View): - """API for retrieving a single listener. + """API for retrieving, updating, and deleting a single listener. """ url_regex = r'lbaas/listeners/(?P[^/]+)/$' @@ -289,10 +460,48 @@ class Listener(generic.View): def get(self, request, listener_id): """Get a specific listener. + If the param 'includeChildResources' is passed in as true, the details + of all resources that exist under the listener will be returned along + with the listener details. + http://localhost/api/lbaas/listeners/cc758c90-3d98-4ea1-af44-aab405c9c915 """ - lb = neutronclient(request).show_listener(listener_id) - return lb.get('listener') + listener = neutronclient(request).show_listener( + listener_id).get('listener') + + if request.GET.get('includeChildResources'): + resources = {} + resources['listener'] = listener + + if listener.get('default_pool_id'): + pool_id = listener['default_pool_id'] + pool = neutronclient(request).show_lbaas_pool( + pool_id).get('pool') + resources['pool'] = pool + + if pool.get('members'): + tenant_id = request.user.project_id + members = neutronclient(request).list_lbaas_members( + pool_id, tenant_id=tenant_id).get('members') + resources['members'] = members + + if pool.get('healthmonitor_id'): + monitor_id = pool['healthmonitor_id'] + monitor = neutronclient(request).show_lbaas_healthmonitor( + monitor_id).get('healthmonitor') + resources['monitor'] = monitor + + return resources + else: + return listener + + @rest_utils.ajax() + def put(self, request, listener_id): + """Edit a listener as well as any resources below it. + + """ + kwargs = {'listener_id': listener_id} + update_listener(request, **kwargs) @urls.register diff --git a/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.js b/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.js index 3c05e07..b2cbeb3 100644 --- a/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.js +++ b/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.js @@ -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 /** diff --git a/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.spec.js b/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.spec.js index 70fdb15..dd20d3d 100644 --- a/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.spec.js +++ b/neutron_lbaas_dashboard/static/app/core/openstack-service-api/lbaasv2.service.spec.js @@ -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" } + ] } ]; diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/lbaasv2.scss b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/lbaasv2.scss index 945ece1..9fa3dbe 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/lbaasv2.scss +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/lbaasv2.scss @@ -44,6 +44,10 @@ div.tab-pane > dl { margin-top: 12px; } + + actions + dl { + clear: both; + } } /* Load Balancer Wizard */ diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/edit/wizard.controller.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/edit/wizard.controller.js new file mode 100644 index 0000000..75c94aa --- /dev/null +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/edit/wizard.controller.js @@ -0,0 +1,83 @@ +/* + * 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(); + } + } + +})(); diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/edit/wizard.controller.spec.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/edit/wizard.controller.spec.js new file mode 100644 index 0000000..abc3813 --- /dev/null +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/edit/wizard.controller.spec.js @@ -0,0 +1,84 @@ +/* + * 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'}); + }); + }); + +})(); diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/row-actions.service.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/row-actions.service.js new file mode 100644 index 0000000..5702a9e --- /dev/null +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/row-actions.service.js @@ -0,0 +1,95 @@ +/* + * 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(); + } + } + +})(); diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/row-actions.service.spec.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/row-actions.service.spec.js new file mode 100644 index 0000000..e10b1a1 --- /dev/null +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/actions/row-actions.service.spec.js @@ -0,0 +1,113 @@ +/* + * 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(); + }); + + }); +})(); diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.js index bc0f41b..13b4f2c 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.js +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.js @@ -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(); //////////////////////////////// diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.spec.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.spec.js index 05ac034..d0070c4 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.spec.js +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.spec.js @@ -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); diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/detail.html b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/detail.html index 3c87247..ff20f58 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/detail.html +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/detail.html @@ -8,6 +8,7 @@

{$ ::ctrl.listener.description $}

+
Listener ID
diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/table.controller.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/table.controller.js index 9412e0e..8d18423 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/table.controller.js +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/table.controller.js @@ -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(); diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/table.controller.spec.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/table.controller.spec.js index 7e3016b..6e98c98 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/table.controller.spec.js +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/table.controller.spec.js @@ -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(); + }); + }); })(); diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/table.html b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/table.html index 076a626..d2da814 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/table.html +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/listeners/table.html @@ -70,6 +70,13 @@ {$ ::item.description | noValue $} {$ ::item.protocol$} {$ ::item.protocol_port$} + + + + diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.html b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.html index 6329331..cf6c0d9 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.html +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/listener/listener.html @@ -35,7 +35,8 @@
@@ -52,7 +53,8 @@ + ng-required="model.context.resource === 'listener'" + ng-disabled="model.context.id"> diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.html b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.html index 83ca0a6..f168b6c 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.html +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/loadbalancer/loadbalancer.html @@ -40,7 +40,7 @@ + ng-disabled="model.context.id"> @@ -51,7 +51,7 @@ id="loadbalancer-subnet" ng-options="subnet.name for subnet in model.subnets" ng-model="model.spec.loadbalancer.subnet" ng-required="true" - ng-disabled="model.context.resource === 'loadbalancer' && model.context.id"> + ng-disabled="model.context.id"> diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/members/members.html b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/members/members.html index 6573258..7db5066 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/members/members.html +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/members/members.html @@ -46,7 +46,8 @@ ng-class="{ 'has-error': memberDetailsForm['{$ ::row.id $}-address'].$invalid && memberDetailsForm['{$ ::row.id $}-address'].$dirty }"> + ng-required="true" + ng-disabled="model.context.id && row.allocatedMember"> {$ ctrl.getSubnetName(row) $} @@ -79,7 +81,8 @@ ng-class="{ 'has-error': memberDetailsForm['{$ ::row.id $}-port'].$invalid && memberDetailsForm['{$ ::row.id $}-port'].$dirty }"> + ng-required="true" + ng-disabled="model.context.id && row.allocatedMember"> + ng-model="row.weight" ng-pattern="/^\d+$/" min="1" max="256" + ng-disabled="model.context.id && row.allocatedMember"> + ng-model="model.spec.monitor.type" + ng-disabled="model.context.id"> @@ -105,7 +106,8 @@ popover-trigger="hover"> + ng-model="model.spec.monitor.status" ng-pattern="::ctrl.statusPattern" + ng-disabled="model.context.id"> diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.html b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.html index f567df8..073fd7a 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.html +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/pool/pool.html @@ -36,7 +36,8 @@ id="pool-protocol" ng-options="protocol for protocol in model.poolProtocols" ng-model="model.spec.pool.protocol" - ng-change="ctrl.protocolChange(model.spec.pool.protocol)"> + ng-change="ctrl.protocolChange(model.spec.pool.protocol)" + ng-disabled="model.context.id"> @@ -47,7 +48,8 @@ diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/workflow.service.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/workflow.service.js index a0d45ce..00c6951 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/workflow.service.js +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/workflow.service.js @@ -67,7 +67,22 @@ return initWorkflow; - function initWorkflow(title, icon, steps) { + function initWorkflow(title, icon, steps, promise) { + + var filteredSteps = steps ? workflowSteps.filter(function(step) { + return steps.indexOf(step.id) > -1; + }) : workflowSteps; + + // If a promise is provided then add a checkReadiness function to the first step so + // that the workflow will not show until the promise is resolved. There must always + // be at least one step in the workflow. + if (promise) { + filteredSteps[0] = angular.copy(filteredSteps[0]); + filteredSteps[0].checkReadiness = function() { + return promise; + }; + } + return dashboardWorkflow({ title: title, btnText: { @@ -76,9 +91,8 @@ btnIcon: { finish: icon }, - steps: steps ? workflowSteps.filter(function(step) { - return steps.indexOf(step.id) > -1; - }) : workflowSteps + steps: filteredSteps, + allSteps: workflowSteps }); } } diff --git a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/workflow.service.spec.js b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/workflow.service.spec.js index 2058b15..18b7cc4 100644 --- a/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/workflow.service.spec.js +++ b/neutron_lbaas_dashboard/static/dashboard/project/lbaasv2/workflow/workflow.service.spec.js @@ -62,6 +62,7 @@ var workflow = workflowService('My Workflow', 'foo', ['listener', 'pool']); expect(workflow.steps).toBeDefined(); expect(workflow.steps.length).toBe(2); + expect(workflow.steps[0].checkReadiness).not.toBeDefined(); var forms = [ 'listenerDetailsForm', @@ -73,6 +74,13 @@ }); }); + it('can wait for all steps to be ready', function () { + var workflow = workflowService('My Workflow', 'foo', null, 'promise'); + + expect(workflow.steps[0].checkReadiness).toBeDefined(); + expect(workflow.steps[0].checkReadiness()).toBe('promise'); + }); + it('can be extended', function () { var workflow = workflowService('My Workflow'); expect(workflow.append).toBeDefined();