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
This commit is contained in:
Lucas Palm 2016-02-12 14:46:20 -06:00 committed by Justin Pomeroy
parent d424350bfa
commit 9a88246fe6
23 changed files with 1359 additions and 190 deletions

View File

@ -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<loadbalancer_id>[^/]+)/$'
@ -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<listener_id>[^/]+)/$'
@ -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

View File

@ -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
/**

View File

@ -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" }
]
}
];

View File

@ -44,6 +44,10 @@
div.tab-pane > dl {
margin-top: 12px;
}
actions + dl {
clear: both;
}
}
/* Load Balancer Wizard */

View File

@ -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();
}
}
})();

View File

@ -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'});
});
});
})();

View File

@ -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();
}
}
})();

View File

@ -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();
});
});
})();

View File

@ -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();
////////////////////////////////

View File

@ -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);

View File

@ -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>

View File

@ -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();

View File

@ -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();
});
});
})();

View File

@ -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">

View File

@ -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'"
ng-disabled="model.context.id">
</select>
</div>
</div>
@ -52,7 +53,8 @@
<input name="listener-port" id="listener-port"
type="number" class="form-control input-sm"
ng-model="model.spec.listener.port" ng-pattern="/^\d+$/" min="1" max="65535"
ng-required="model.context.resource === 'listener'">
ng-required="model.context.resource === 'listener'"
ng-disabled="model.context.id">
</div>
</div>

View File

@ -40,7 +40,7 @@
<input name="loadbalancer-ip" id="loadbalancer-ip"
type="text" class="form-control input-sm"
ng-model="model.spec.loadbalancer.ip" ng-pattern="::ctrl.ipPattern"
ng-disabled="model.context.resource === 'loadbalancer' && model.context.id">
ng-disabled="model.context.id">
</div>
</div>
@ -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">
</select>
</div>
</div>

View File

@ -46,7 +46,8 @@
ng-class="{ 'has-error': memberDetailsForm['{$ ::row.id $}-address'].$invalid && memberDetailsForm['{$ ::row.id $}-address'].$dirty }">
<input name="{$ ::row.id $}-address" type="text" class="form-control input-sm"
ng-model="row.address" ng-pattern="::ctrl.ipPattern"
ng-required="true">
ng-required="true"
ng-disabled="model.context.id && row.allocatedMember">
</div>
<span ng-if="!row.addresses"
class="fa fa-exclamation-triangle invalid"
@ -69,7 +70,8 @@
class="form-field">
<select name="{$ ::row.id $}-subnet" class="form-control input-sm"
ng-options="subnet.name for subnet in model.subnets"
ng-model="row.subnet">
ng-model="row.subnet"
ng-disabled="model.context.id && row.allocatedMember">
</select>
</div>
<span ng-if="row.addresses">{$ ctrl.getSubnetName(row) $}</span>
@ -79,7 +81,8 @@
ng-class="{ 'has-error': memberDetailsForm['{$ ::row.id $}-port'].$invalid && memberDetailsForm['{$ ::row.id $}-port'].$dirty }">
<input name="{$ ::row.id $}-port" type="number" class="form-control input-sm"
ng-model="row.port" ng-pattern="/^\d+$/" min="1" max="65535"
ng-required="true">
ng-required="true"
ng-disabled="model.context.id && row.allocatedMember">
</div>
<span class="fa fa-exclamation-triangle invalid"
ng-show="memberDetailsForm['{$ ::row.id $}-port'].$invalid && memberDetailsForm.$dirty"
@ -91,7 +94,8 @@
<div class="form-field member-weight"
ng-class="{ 'has-error': memberDetailsForm['{$ ::row.id $}-weight'].$invalid && memberDetailsForm['{$ ::row.id $}-weight'].$dirty }">
<input name="{$ ::row.id $}-weight" type="number" class="form-control input-sm"
ng-model="row.weight" ng-pattern="/^\d+$/" min="1" max="256">
ng-model="row.weight" ng-pattern="/^\d+$/" min="1" max="256"
ng-disabled="model.context.id && row.allocatedMember">
</div>
<span class="fa fa-exclamation-triangle invalid"
ng-show="memberDetailsForm['{$ ::row.id $}-weight'].$invalid && memberDetailsForm.$dirty"

View File

@ -48,7 +48,7 @@
*/
function workflowModel($q, neutronAPI, novaAPI, lbaasv2API, gettext) {
var initPromise, ports;
var ports;
/**
* @ngdoc model api object
@ -70,6 +70,7 @@
spec: null,
visibleResources: [],
subnets: [],
members: [],
listenerProtocols: ['TCP', 'HTTP', 'HTTPS'],
@ -86,6 +87,8 @@
submit: submit
};
return model;
/**
* @ngdoc method
* @name workflowModel.initialize
@ -108,7 +111,10 @@
submit: null
};
model.visibleResources = [];
model.spec = {
loadbalancer_id: null,
loadbalancer: {
name: null,
description: null,
@ -116,18 +122,21 @@
subnet: null
},
listener: {
id: null,
name: gettext('Listener 1'),
description: null,
protocol: null,
port: null
},
pool: {
id: null,
name: gettext('Pool 1'),
description: null,
protocol: null,
method: null
},
monitor: {
id: null,
type: null,
interval: null,
retry: null,
@ -140,35 +149,40 @@
};
if (model.initializing) {
promise = initPromise;
} else {
model.initializing = true;
return promise;
}
model.initializing = true;
switch ((id ? 'edit' : 'create') + resource) {
case 'createloadbalancer':
promise = $q.all([
lbaasv2API.getLoadBalancers().then(onGetLoadBalancers),
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers)
]).then(initMemberAddresses);
model.context.submit = createLoadBalancer;
break;
case 'editloadbalancer':
promise = $q.all([
lbaasv2API.getLoadBalancer(model.context.id).then(onGetLoadBalancer),
neutronAPI.getSubnets().then(onGetSubnets)
]).then(initSubnet);
model.context.submit = editLoadBalancer;
break;
default:
throw Error('Invalid resource context: ' + (id ? 'edit' : 'create') + resource);
}
promise.then(onInitSuccess, onInitFail);
switch ((id ? 'edit' : 'create') + resource) {
case 'createloadbalancer':
promise = $q.all([
lbaasv2API.getLoadBalancers().then(onGetLoadBalancers),
neutronAPI.getSubnets().then(onGetSubnets),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers)
]).then(initMemberAddresses);
model.context.submit = createLoadBalancer;
break;
case 'editloadbalancer':
promise = $q.all([
lbaasv2API.getLoadBalancer(model.context.id).then(onGetLoadBalancer),
neutronAPI.getSubnets().then(onGetSubnets)
]).then(initSubnet);
model.context.submit = editLoadBalancer;
break;
case 'editlistener':
promise = $q.all([
neutronAPI.getSubnets().then(onGetSubnets).then(getListener).then(onGetListener),
neutronAPI.getPorts().then(onGetPorts),
novaAPI.getServers().then(onGetServers)
]).then(initMemberAddresses);
model.context.submit = editListener;
break;
default:
throw Error('Invalid resource context: ' + (id ? 'edit' : 'create') + resource);
}
return promise;
return promise.then(onInitSuccess, onInitFail);
}
function onInitSuccess() {
@ -211,6 +225,10 @@
return lbaasv2API.editLoadBalancer(model.context.id, spec);
}
function editListener(spec) {
return lbaasv2API.editListener(model.context.id, spec);
}
function cleanFinalSpecLoadBalancer(finalSpec) {
var context = model.context;
@ -259,7 +277,8 @@
var members = [];
angular.forEach(finalSpec.members, function cleanMember(member) {
if (member.address && member.port) {
['id', 'name', 'description', 'addresses'].forEach(function deleteProperty(prop) {
['name', 'description', 'addresses', 'allocatedMember'].forEach(
function deleteProperty(prop) {
if (angular.isDefined(member[prop])) {
delete member[prop];
}
@ -362,18 +381,101 @@
});
});
member.address = member.addresses[0];
if (model.spec.pool.protocol) {
member.port = {'HTTP': 80}[model.spec.pool.protocol];
}
});
}
function getListener() {
return lbaasv2API.getListener(model.context.id, true);
}
function onGetLoadBalancer(response) {
var loadbalancer = response.data;
setLoadBalancerSpec(loadbalancer);
model.visibleResources.push('loadbalancer');
}
function onGetListener(response) {
var resources = response.data;
setListenerSpec(resources.listener);
model.visibleResources.push('listener');
model.spec.loadbalancer_id = resources.listener.loadbalancers[0].id;
if (resources.pool) {
setPoolSpec(resources.pool);
model.visibleResources.push('pool');
model.visibleResources.push('members');
if (resources.members) {
setMembersSpec(resources.members);
}
if (resources.monitor) {
setMonitorSpec(resources.monitor);
model.visibleResources.push('monitor');
}
}
}
function setLoadBalancerSpec(loadbalancer) {
var spec = model.spec.loadbalancer;
spec.name = loadbalancer.name || '';
spec.description = loadbalancer.description || '';
spec.ip = loadbalancer.vip_address || '';
spec.name = loadbalancer.name;
spec.description = loadbalancer.description;
spec.ip = loadbalancer.vip_address;
spec.subnet = loadbalancer.vip_subnet_id;
}
function setListenerSpec(listener) {
var spec = model.spec.listener;
spec.id = listener.id;
spec.name = listener.name;
spec.description = listener.description;
spec.protocol = listener.protocol;
spec.port = listener.protocol_port;
}
function setPoolSpec(pool) {
var spec = model.spec.pool;
spec.id = pool.id;
spec.name = pool.name;
spec.description = pool.description;
spec.protocol = pool.protocol;
spec.method = pool.lb_algorithm;
}
function setMembersSpec(membersList) {
model.spec.members.length = 0;
var members = [];
angular.forEach(membersList, function addMember(member) {
members.push({
id: member.id,
address: member.address,
subnet: mapSubnetObj(member.subnet_id),
port: member.protocol_port,
weight: member.weight,
allocatedMember: true
});
});
push.apply(model.spec.members, members);
}
function setMonitorSpec(monitor) {
var spec = model.spec.monitor;
spec.id = monitor.id;
spec.type = monitor.type;
spec.interval = monitor.delay;
spec.timeout = monitor.timeout;
spec.retry = monitor.max_retries;
spec.method = monitor.http_method;
spec.status = monitor.expected_codes;
spec.path = monitor.url_path;
}
function initSubnet() {
var subnet = model.subnets.filter(function filterSubnetsByLoadBalancer(s) {
return s.id === model.spec.loadbalancer.subnet;
@ -381,7 +483,13 @@
model.spec.loadbalancer.subnet = subnet;
}
return model;
function mapSubnetObj(subnetId) {
var subnet = model.subnets.filter(function mapSubnet(subnet) {
return subnet.id === subnetId;
});
return subnet[0];
}
}
})();

View File

@ -17,15 +17,64 @@
'use strict';
describe('LBaaS v2 Workflow Model Service', function() {
var model, $q, scope;
var model, $q, scope, listenerResources;
beforeEach(module('horizon.framework.util.i18n'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(function() {
listenerResources = {
listener: {
id: '1234',
name: 'Listener 1',
description: 'listener description',
protocol: 'HTTP',
protocol_port: 80,
loadbalancers: [ { id: '1234' } ]
},
pool: {
id: '1234',
name: 'Pool 1',
protocol: 'HTTP',
lb_algorithm: 'ROUND_ROBIN',
description: 'pool description'
},
members: [
{
id: '1234',
address: '1.2.3.4',
subnet_id: 'subnet-1',
protocol_port: 80,
weight: 1
},
{
id: '5678',
address: '5.6.7.8',
subnet_id: 'subnet-1',
protocol_port: 80,
weight: 1
}
],
monitor: {
id: '1234',
type: 'HTTP',
delay: 1,
timeout: 1,
max_retries: 1,
http_method: 'POST',
expected_codes: '200',
url_path: '/test'
}
};
});
beforeEach(module(function($provide) {
$provide.value('horizon.app.core.openstack-service-api.lbaasv2', {
getLoadBalancers: function() {
var loadbalancers = [ { name: 'Load Balancer 1' }, { name: 'Load Balancer 2' } ];
var loadbalancers = [
{ id: '1234', name: 'Load Balancer 1' },
{ id: '5678', name: 'Load Balancer 2' }
];
var deferred = $q.defer();
deferred.resolve({ data: { items: loadbalancers } });
@ -33,10 +82,74 @@
return deferred.promise;
},
getLoadBalancer: function() {
var loadbalancer = { vip_subnet_id: 'subnet-1' };
var loadbalancer = {
id: '1234',
name: 'Load Balancer 1',
vip_address: '1.2.3.4',
vip_subnet_id: 'subnet-1',
description: ''
};
var deferred = $q.defer();
deferred.resolve({ data: loadbalancer });
return deferred.promise;
},
getListener: function() {
var deferred = $q.defer();
deferred.resolve({ data: listenerResources });
return deferred.promise;
},
getPool: function() {
var pool = {
id: '1234',
name: 'Pool 1',
protocol: 'HTTP',
lb_algorithm: 'ROUND_ROBIN',
description: 'pool description'
};
var deferred = $q.defer();
deferred.resolve({ data: loadbalancer });
deferred.resolve({ data: pool });
return deferred.promise;
},
getMembers: function() {
var members = [
{
id: '1234',
address: '1.2.3.4',
subnet_id: 'subnet-1',
protocol_port: 80,
weight: 1
},
{
id: '5678',
address: '5.6.7.8',
subnet_id: 'subnet-1',
protocol_port: 80,
weight: 1
}];
var deferred = $q.defer();
deferred.resolve({ data: { items: members } });
return deferred.promise;
},
getHealthMonitor: function() {
var monitor = {
id: '1234',
type: 'HTTP',
delay: 1,
timeout: 1,
max_retries: 1,
http_method: 'POST',
expected_codes: '200',
url_path: '/test'
};
var deferred = $q.defer();
deferred.resolve({ data: monitor });
return deferred.promise;
},
@ -45,6 +158,9 @@
},
editLoadBalancer: function(id, spec) {
return spec;
},
editListener: function(id, spec) {
return spec;
}
});
@ -94,7 +210,7 @@
scope = $injector.get('$rootScope').$new();
}));
describe('Initial object (pre-initialize)', function() {
describe('Initial model (pre-initialize)', function() {
it('is defined', function() {
expect(model).toBeDefined();
@ -150,7 +266,7 @@
});
});
describe('Post initialize model', function() {
describe('Post initialize model (create loadbalancer)', function() {
beforeEach(function() {
model.initialize('loadbalancer');
@ -163,6 +279,7 @@
expect(model.subnets.length).toBe(2);
expect(model.members.length).toBe(2);
expect(model.spec).toBeDefined();
expect(model.spec.loadbalancer_id).toBeNull();
expect(model.spec.loadbalancer).toBeDefined();
expect(model.spec.listener).toBeDefined();
expect(model.spec.pool).toBeDefined();
@ -176,19 +293,269 @@
expect(model.spec.pool.name).toBe('Pool 1');
});
it('should initialize monitor fields', function() {
it('should initialize context properties', function() {
expect(model.context.resource).toBe('loadbalancer');
expect(model.context.id).toBeUndefined();
expect(model.context.submit).toBeDefined();
});
});
describe('Post initialize model (edit loadbalancer)', function() {
beforeEach(function() {
model.initialize('loadbalancer', '1234');
scope.$apply();
});
it('should initialize model properties', function() {
expect(model.initializing).toBe(false);
expect(model.initialized).toBe(true);
expect(model.subnets.length).toBe(2);
expect(model.members).toEqual([]);
expect(model.spec).toBeDefined();
expect(model.spec.loadbalancer_id).toBeNull();
expect(model.spec.loadbalancer).toBeDefined();
expect(model.spec.listener).toBeDefined();
expect(model.spec.pool).toBeDefined();
expect(model.spec.members).toEqual([]);
expect(model.spec.monitor).toBeDefined();
});
it('should initialize loadbalancer model spec properties', function() {
expect(model.spec.loadbalancer.name).toEqual('Load Balancer 1');
expect(model.spec.loadbalancer.description).toEqual('');
expect(model.spec.loadbalancer.ip).toEqual('1.2.3.4');
expect(model.spec.loadbalancer.subnet).toEqual({ id: 'subnet-1', name: 'subnet-1' });
});
it('should not initialize listener model spec properties', function() {
expect(model.spec.listener.id).toBeNull();
expect(model.spec.listener.name).toBe('Listener 1');
expect(model.spec.listener.description).toBeNull();
expect(model.spec.listener.protocol).toBeNull();
expect(model.spec.listener.port).toBeNull();
});
it('should not initialize pool model spec properties', function() {
expect(model.spec.pool.id).toBeNull();
expect(model.spec.pool.name).toBe('Pool 1');
expect(model.spec.pool.description).toBeNull();
expect(model.spec.pool.protocol).toBeNull();
expect(model.spec.pool.method).toBeNull();
});
it('should initialize monitor model spec properties to null', function() {
expect(model.spec.monitor.type).toBeNull();
expect(model.spec.monitor.interval).toBeNull();
expect(model.spec.monitor.retry).toBeNull();
expect(model.spec.monitor.timeout).toBeNull();
expect(model.spec.monitor.method).toBe('GET');
expect(model.spec.monitor.status).toBe('200');
expect(model.spec.monitor.path).toBe('/');
});
it('should not initialize any members in the model spec', function() {
expect(model.spec.members).toEqual([]);
});
it('should initialize context', function() {
expect(model.context.resource).toBe('loadbalancer');
expect(model.context.id).toBeUndefined();
expect(model.context.id).toBe('1234');
expect(model.context.submit).toBeDefined();
});
});
describe('Post initialize model (edit listener)', function() {
beforeEach(function() {
model.initialize('listener', '1234');
scope.$apply();
});
it('should initialize model properties', function() {
expect(model.initializing).toBe(false);
expect(model.initialized).toBe(true);
expect(model.subnets.length).toBe(2);
expect(model.members.length).toEqual(2);
expect(model.spec).toBeDefined();
expect(model.spec.loadbalancer_id).toBeDefined();
expect(model.spec.loadbalancer).toBeDefined();
expect(model.spec.listener).toBeDefined();
expect(model.spec.pool).toBeDefined();
expect(model.subnets.length).toBe(2);
expect(model.spec.monitor).toBeDefined();
});
it('should initialize the loadbalancer_id property', function() {
expect(model.spec.loadbalancer_id).toBe('1234');
});
it('should initialize all loadbalancer properties to null', function() {
expect(model.spec.loadbalancer.name).toBeNull();
expect(model.spec.loadbalancer.description).toBeNull();
expect(model.spec.loadbalancer.ip).toBeNull();
expect(model.spec.loadbalancer.subnet).toBeNull();
});
it('should initialize all listener properties', function() {
expect(model.spec.listener.id).toBe('1234');
expect(model.spec.listener.name).toBe('Listener 1');
expect(model.spec.listener.description).toBe('listener description');
expect(model.spec.listener.protocol).toBe('HTTP');
expect(model.spec.listener.port).toBe(80);
});
it('should initialize all pool properties', function() {
expect(model.spec.pool.id).toBe('1234');
expect(model.spec.pool.name).toBe('Pool 1');
expect(model.spec.pool.description).toBe('pool description');
expect(model.spec.pool.protocol).toBe('HTTP');
expect(model.spec.pool.method).toBe('ROUND_ROBIN');
});
it('should initialize all monitor properties', function() {
expect(model.spec.monitor.id).toBe('1234');
expect(model.spec.monitor.type).toBe('HTTP');
expect(model.spec.monitor.interval).toBe(1);
expect(model.spec.monitor.retry).toBe(1);
expect(model.spec.monitor.timeout).toBe(1);
expect(model.spec.monitor.method).toBe('POST');
expect(model.spec.monitor.status).toBe('200');
expect(model.spec.monitor.path).toBe('/test');
});
it('should initialize members and properties', function() {
expect(model.spec.members[0].id).toBe('1234');
expect(model.spec.members[0].address).toBe('1.2.3.4');
expect(model.spec.members[0].subnet).toEqual({ id: 'subnet-1', name: 'subnet-1' });
expect(model.spec.members[0].port).toBe(80);
expect(model.spec.members[0].weight).toBe(1);
expect(model.spec.members[1].id).toBe('5678');
expect(model.spec.members[1].address).toBe('5.6.7.8');
expect(model.spec.members[1].subnet).toEqual({ id: 'subnet-1', name: 'subnet-1' });
expect(model.spec.members[1].port).toBe(80);
expect(model.spec.members[1].weight).toBe(1);
});
it('should initialize context', function() {
expect(model.context.resource).toBe('listener');
expect(model.context.id).toBeDefined();
expect(model.context.submit).toBeDefined();
});
});
describe('Post initialize model - Initializing', function() {
beforeEach(function() {
model.initializing = true;
model.initialize('loadbalancer');
scope.$apply();
});
// This is here to ensure that as people add/change spec properties, they don't forget
// to implement tests for them.
it('has the right number of properties', function() {
expect(Object.keys(model.spec).length).toBe(6);
expect(Object.keys(model.spec.loadbalancer).length).toBe(4);
expect(Object.keys(model.spec.listener).length).toBe(5);
expect(Object.keys(model.spec.pool).length).toBe(5);
expect(Object.keys(model.spec.monitor).length).toBe(8);
expect(model.spec.members).toEqual([]);
});
it('sets load balancer ID to null', function() {
expect(model.spec.loadbalancer_id).toBeNull();
});
it('sets load balancer name to null', function() {
expect(model.spec.loadbalancer.name).toBeNull();
});
it('sets load balancer description to null', function() {
expect(model.spec.loadbalancer.description).toBeNull();
});
it('sets load balancer ip address to null', function() {
expect(model.spec.loadbalancer.ip).toBeNull();
});
it('sets load balancer subnet to null', function() {
expect(model.spec.loadbalancer.subnet).toBeNull();
});
it('sets listener id to null', function() {
expect(model.spec.listener.id).toBeNull();
});
it('sets listener name to reasonable default', function() {
expect(model.spec.listener.name).toBe('Listener 1');
});
it('sets listener description to null', function() {
expect(model.spec.listener.description).toBeNull();
});
it('sets listener protocol to null', function() {
expect(model.spec.listener.protocol).toBeNull();
});
it('sets listener port to null', function() {
expect(model.spec.listener.port).toBeNull();
});
it('sets pool id to null', function() {
expect(model.spec.pool.id).toBeNull();
});
it('sets pool name to reasonable default', function() {
expect(model.spec.pool.name).toBe('Pool 1');
});
it('sets pool description to null', function() {
expect(model.spec.pool.description).toBeNull();
});
it('sets pool protocol to null', function() {
expect(model.spec.pool.protocol).toBeNull();
});
it('sets pool method to null', function() {
expect(model.spec.pool.method).toBeNull();
});
it('sets monitor id to null', function() {
expect(model.spec.monitor.id).toBeNull();
});
it('sets monitor type to null', function() {
expect(model.spec.monitor.type).toBeNull();
});
it('sets monitor interval to null', function() {
expect(model.spec.monitor.interval).toBeNull();
});
it('sets monitor retry count to null', function() {
expect(model.spec.monitor.retry).toBeNull();
});
it('sets monitor timeout to null', function() {
expect(model.spec.monitor.timeout).toBeNull();
});
it('sets monitor method to default', function() {
expect(model.spec.monitor.method).toBe('GET');
});
it('sets monitor status code to default', function() {
expect(model.spec.monitor.status).toBe('200');
});
it('sets monitor URL path to default', function() {
expect(model.spec.monitor.path).toBe('/');
});
});
describe('Initialization failure', function() {
beforeEach(inject(function ($injector) {
@ -231,101 +598,6 @@
});
});
describe('Post initialize model - Initializing', function() {
beforeEach(function() {
model.initializing = true;
model.initialize('loadbalancer');
scope.$apply();
});
// This is here to ensure that as people add/change spec properties, they don't forget
// to implement tests for them.
it('has the right number of properties', function() {
expect(Object.keys(model.spec).length).toBe(5);
expect(Object.keys(model.spec.loadbalancer).length).toBe(4);
expect(Object.keys(model.spec.listener).length).toBe(4);
expect(Object.keys(model.spec.pool).length).toBe(4);
expect(Object.keys(model.spec.monitor).length).toBe(7);
});
it('sets load balancer name to null', function() {
expect(model.spec.loadbalancer.name).toBeNull();
});
it('sets load balancer description to null', function() {
expect(model.spec.loadbalancer.description).toBeNull();
});
it('sets load balancer ip address to null', function() {
expect(model.spec.loadbalancer.ip).toBeNull();
});
it('sets load balancer subnet to null', function() {
expect(model.spec.loadbalancer.subnet).toBeNull();
});
it('sets listener name to reasonable default', function() {
expect(model.spec.listener.name).toBe('Listener 1');
});
it('sets listener description to null', function() {
expect(model.spec.listener.description).toBeNull();
});
it('sets listener protocol to null', function() {
expect(model.spec.listener.protocol).toBeNull();
});
it('sets listener port to null', function() {
expect(model.spec.listener.port).toBeNull();
});
it('sets pool name to reasonable default', function() {
expect(model.spec.pool.name).toBe('Pool 1');
});
it('sets pool description to null', function() {
expect(model.spec.pool.description).toBeNull();
});
it('sets pool protocol to null', function() {
expect(model.spec.pool.protocol).toBeNull();
});
it('sets pool method to null', function() {
expect(model.spec.pool.method).toBeNull();
});
it('sets monitor type to null', function() {
expect(model.spec.monitor.type).toBeNull();
});
it('sets monitor interval to null', function() {
expect(model.spec.monitor.interval).toBeNull();
});
it('sets monitor retry count to null', function() {
expect(model.spec.monitor.retry).toBeNull();
});
it('sets monitor timeout to null', function() {
expect(model.spec.monitor.timeout).toBeNull();
});
it('sets monitor method to default', function() {
expect(model.spec.monitor.method).toBe('GET');
});
it('sets monitor status code to default', function() {
expect(model.spec.monitor.status).toBe('200');
});
it('sets monitor URL path to default', function() {
expect(model.spec.monitor.path).toBe('/');
});
});
describe('context (create loadbalancer)', function() {
beforeEach(function() {
@ -354,6 +626,20 @@
});
});
describe('context (edit listener)', function() {
beforeEach(function() {
model.initialize('listener', '1');
scope.$apply();
});
it('should initialize context', function() {
expect(model.context.resource).toBe('listener');
expect(model.context.id).toBe('1');
expect(model.context.submit.name).toBe('editListener');
});
});
describe('Model submit function (create loadbalancer)', function() {
beforeEach(function() {
@ -409,32 +695,39 @@
expect(finalSpec.loadbalancer.description).toBeUndefined();
expect(finalSpec.loadbalancer.ip).toBe('1.2.3.4');
expect(finalSpec.loadbalancer.subnet).toBe(model.subnets[0].id);
expect(finalSpec.listener.name).toBe('Listener 1');
expect(finalSpec.listener.description).toBeUndefined();
expect(finalSpec.listener.protocol).toBe('HTTPS');
expect(finalSpec.listener.port).toBe(80);
expect(finalSpec.pool.name).toBe('pool name');
expect(finalSpec.pool.description).toBe('pool description');
expect(finalSpec.pool.protocol).toBe('HTTP');
expect(finalSpec.pool.method).toBe('LEAST_CONNECTIONS');
expect(finalSpec.members.length).toBe(3);
expect(finalSpec.members[0].address).toBe('1.2.3.4');
expect(finalSpec.members[0].subnet).toBe('1');
expect(finalSpec.members[0].port).toBe(80);
expect(finalSpec.members[0].weight).toBe(1);
expect(finalSpec.members[0].id).toBe('1');
expect(finalSpec.members[0].addresses).toBeUndefined();
expect(finalSpec.members[0].id).toBeUndefined();
expect(finalSpec.members[0].name).toBeUndefined();
expect(finalSpec.members[1].id).toBeUndefined();
expect(finalSpec.members[0].allocatedMember).toBeUndefined();
expect(finalSpec.members[1].id).toBe('external-member-0');
expect(finalSpec.members[1].address).toBe('2.3.4.5');
expect(finalSpec.members[1].subnet).toBeUndefined();
expect(finalSpec.members[1].port).toBe(80);
expect(finalSpec.members[1].weight).toBe(1);
expect(finalSpec.members[2].id).toBeUndefined();
expect(finalSpec.members[1].allocatedMember).toBeUndefined();
expect(finalSpec.members[2].id).toBe('external-member-2');
expect(finalSpec.members[2].address).toBe('3.4.5.6');
expect(finalSpec.members[2].subnet).toBe('1');
expect(finalSpec.members[2].port).toBe(80);
expect(finalSpec.members[2].weight).toBe(1);
expect(finalSpec.members[2].allocatedMember).toBeUndefined();
expect(finalSpec.monitor.type).toBe('PING');
expect(finalSpec.monitor.interval).toBe(1);
expect(finalSpec.monitor.retry).toBe(1);
@ -535,7 +828,7 @@
describe('Model submit function (edit loadbalancer)', function() {
beforeEach(function() {
model.initialize('loadbalancer', '1');
model.initialize('loadbalancer', '1234');
scope.$apply();
});
@ -544,12 +837,80 @@
var finalSpec = model.submit();
expect(finalSpec.loadbalancer.name).toBe('');
expect(finalSpec.loadbalancer.name).toBe('Load Balancer 1');
expect(finalSpec.loadbalancer.description).toBe('new description');
expect(finalSpec.loadbalancer.ip).toBe('');
expect(finalSpec.loadbalancer.ip).toBe('1.2.3.4');
expect(finalSpec.loadbalancer.subnet).toBe('subnet-1');
});
});
describe('Model submit function (edit listener)', function() {
beforeEach(function() {
model.initialize('listener', '1234');
scope.$apply();
});
it('should set final spec properties', function() {
var finalSpec = model.submit();
expect(finalSpec.loadbalancer).toBeUndefined();
expect(finalSpec.listener.name).toBe('Listener 1');
expect(finalSpec.listener.description).toBe('listener description');
expect(finalSpec.listener.protocol).toBe('HTTP');
expect(finalSpec.listener.port).toBe(80);
expect(finalSpec.pool.name).toBe('Pool 1');
expect(finalSpec.pool.description).toBe('pool description');
expect(finalSpec.pool.protocol).toBe('HTTP');
expect(finalSpec.pool.method).toBe('ROUND_ROBIN');
expect(finalSpec.members.length).toBe(2);
expect(finalSpec.members[0].id).toBe('1234');
expect(finalSpec.members[0].address).toBe('1.2.3.4');
expect(finalSpec.members[0].subnet).toBe('subnet-1');
expect(finalSpec.members[0].port).toBe(80);
expect(finalSpec.members[0].weight).toBe(1);
expect(finalSpec.members[1].id).toBe('5678');
expect(finalSpec.members[1].address).toBe('5.6.7.8');
expect(finalSpec.members[1].subnet).toBe('subnet-1');
expect(finalSpec.members[1].port).toBe(80);
expect(finalSpec.members[1].weight).toBe(1);
expect(finalSpec.monitor.type).toBe('HTTP');
expect(finalSpec.monitor.interval).toBe(1);
expect(finalSpec.monitor.retry).toBe(1);
expect(finalSpec.monitor.timeout).toBe(1);
});
});
describe('Model visible resources (edit listener, no pool)', function() {
beforeEach(function() {
delete listenerResources.pool;
model.initialize('listener', '1234');
scope.$apply();
});
it('should only show listener details', function() {
expect(model.visibleResources).toEqual(['listener']);
});
});
describe('Model visible resources (edit listener, no monitor or existing members)', function() {
beforeEach(function() {
delete listenerResources.members;
delete listenerResources.monitor;
model.initialize('listener', '1234');
scope.$apply();
});
it('should only show listener, pool, and member details', function() {
expect(model.visibleResources).toEqual(['listener', 'pool', 'members']);
});
});
});
})();

View File

@ -13,7 +13,8 @@
<select class="form-control input-sm" name="monitor-type"
id="monitor-type"
ng-options="type for type in model.monitorTypes"
ng-model="model.spec.monitor.type">
ng-model="model.spec.monitor.type"
ng-disabled="model.context.id">
</select>
</div>
</div>
@ -105,7 +106,8 @@
popover-trigger="hover"></span>
<input name="monitor-status" id="monitor-status"
type="text" class="form-control input-sm"
ng-model="model.spec.monitor.status" ng-pattern="::ctrl.statusPattern">
ng-model="model.spec.monitor.status" ng-pattern="::ctrl.statusPattern"
ng-disabled="model.context.id">
</div>
</div>

View File

@ -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">
</select>
</div>
</div>
@ -47,7 +48,8 @@
<select class="form-control input-sm" name="pool-method"
id="pool-method"
ng-options="method for method in model.methods"
ng-model="model.spec.pool.method">
ng-model="model.spec.pool.method"
ng-disabled="model.context.id">
</select>
</div>
</div>

View File

@ -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
});
}
}

View File

@ -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();