Add members tab to create load balancer workflow
This adds the members tab to the create load balancer workflow and allows adding multiple members (nova servers) to the pool. Partially-Implements: blueprint horizon-lbaas-v2-ui Change-Id: I469dd9ff4d9aef9b91316699d1edec278957f091
This commit is contained in:
parent
32166ee5a8
commit
6c4828d8f6
|
@ -97,9 +97,45 @@ def create_pool(request, **kwargs):
|
||||||
poolSpec['name'] = data['pool']['name']
|
poolSpec['name'] = data['pool']['name']
|
||||||
if data['pool'].get('description'):
|
if data['pool'].get('description'):
|
||||||
poolSpec['description'] = data['pool']['description']
|
poolSpec['description'] = data['pool']['description']
|
||||||
return neutronclient(request).create_lbaas_pool(
|
pool = neutronclient(request).create_lbaas_pool(
|
||||||
{'pool': poolSpec}).get('pool')
|
{'pool': poolSpec}).get('pool')
|
||||||
|
|
||||||
|
if data.get('members'):
|
||||||
|
args = (request, kwargs['loadbalancer_id'], add_member)
|
||||||
|
kwargs = {'callback_kwargs': {'pool_id': pool['id'],
|
||||||
|
'index': 0}}
|
||||||
|
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
|
||||||
|
|
||||||
|
return pool
|
||||||
|
|
||||||
|
|
||||||
|
def add_member(request, **kwargs):
|
||||||
|
"""Add a member to a pool.
|
||||||
|
|
||||||
|
"""
|
||||||
|
data = request.DATA
|
||||||
|
members = data['members']
|
||||||
|
index = kwargs['index']
|
||||||
|
member = members[index]
|
||||||
|
memberSpec = {
|
||||||
|
'address': member['address'],
|
||||||
|
'protocol_port': member['port'],
|
||||||
|
'subnet_id': member['subnet']
|
||||||
|
}
|
||||||
|
if member.get('weight'):
|
||||||
|
memberSpec['weight'] = member['weight']
|
||||||
|
member = neutronclient(request).create_lbaas_member(
|
||||||
|
kwargs['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'],
|
||||||
|
'index': index}}
|
||||||
|
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
|
||||||
|
|
||||||
|
return member
|
||||||
|
|
||||||
|
|
||||||
@urls.register
|
@urls.register
|
||||||
class LoadBalancers(generic.View):
|
class LoadBalancers(generic.View):
|
||||||
|
|
|
@ -47,7 +47,11 @@ ADD_JS_FILES = [
|
||||||
('dashboard/project/lbaasv2/loadbalancers/actions/create/details/'
|
('dashboard/project/lbaasv2/loadbalancers/actions/create/details/'
|
||||||
'details.controller.js'),
|
'details.controller.js'),
|
||||||
('dashboard/project/lbaasv2/loadbalancers/actions/create/listener/'
|
('dashboard/project/lbaasv2/loadbalancers/actions/create/listener/'
|
||||||
'listener.controller.js')
|
'listener.controller.js'),
|
||||||
|
('dashboard/project/lbaasv2/loadbalancers/actions/create/pool/'
|
||||||
|
'pool.controller.js'),
|
||||||
|
('dashboard/project/lbaasv2/loadbalancers/actions/create/members/'
|
||||||
|
'members.controller.js')
|
||||||
]
|
]
|
||||||
|
|
||||||
ADD_JS_SPEC_FILES = [
|
ADD_JS_SPEC_FILES = [
|
||||||
|
@ -70,7 +74,11 @@ ADD_JS_SPEC_FILES = [
|
||||||
('dashboard/project/lbaasv2/loadbalancers/actions/create/details/'
|
('dashboard/project/lbaasv2/loadbalancers/actions/create/details/'
|
||||||
'details.controller.spec.js'),
|
'details.controller.spec.js'),
|
||||||
('dashboard/project/lbaasv2/loadbalancers/actions/create/listener/'
|
('dashboard/project/lbaasv2/loadbalancers/actions/create/listener/'
|
||||||
'listener.controller.spec.js')
|
'listener.controller.spec.js'),
|
||||||
|
('dashboard/project/lbaasv2/loadbalancers/actions/create/pool/'
|
||||||
|
'pool.controller.spec.js'),
|
||||||
|
('dashboard/project/lbaasv2/loadbalancers/actions/create/members/'
|
||||||
|
'members.controller.spec.js')
|
||||||
]
|
]
|
||||||
|
|
||||||
ADD_SCSS_FILES = [
|
ADD_SCSS_FILES = [
|
||||||
|
|
|
@ -50,7 +50,7 @@ module.exports = function (config) {
|
||||||
|
|
||||||
// Sets up module to process templates.
|
// Sets up module to process templates.
|
||||||
ngHtml2JsPreprocessor: {
|
ngHtml2JsPreprocessor: {
|
||||||
prependPrefix: '/static/',
|
prependPrefix: '/',
|
||||||
moduleName: 'templates'
|
moduleName: 'templates'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -33,8 +33,11 @@
|
||||||
.constant('horizon.dashboard.project.lbaasv2.patterns', {
|
.constant('horizon.dashboard.project.lbaasv2.patterns', {
|
||||||
ipv4: '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$',
|
ipv4: '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$',
|
||||||
ipv6: '^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?$'
|
ipv6: '^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?$'
|
||||||
});
|
})
|
||||||
/* eslint-enable max-len */
|
/* eslint-enable max-len */
|
||||||
|
.constant('horizon.dashboard.project.lbaasv2.popovers', {
|
||||||
|
ipAddresses: '<ul><li ng-repeat="addr in member.addresses">{$ addr.ip $}</li></ul>'
|
||||||
|
});
|
||||||
|
|
||||||
config.$inject = [
|
config.$inject = [
|
||||||
'$provide',
|
'$provide',
|
||||||
|
|
|
@ -45,3 +45,25 @@
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transfer-table {
|
||||||
|
.member-weight,
|
||||||
|
.member-port {
|
||||||
|
display: inline-block;
|
||||||
|
width: 6em;
|
||||||
|
}
|
||||||
|
.member-address {
|
||||||
|
width: 20em;
|
||||||
|
}
|
||||||
|
span.invalid {
|
||||||
|
vertical-align: top;
|
||||||
|
margin: 8px 0px 0px 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addresses-popover + .popover {
|
||||||
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* 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('AddMembersController', AddMembersController);
|
||||||
|
|
||||||
|
AddMembersController.$inject = [
|
||||||
|
'$scope',
|
||||||
|
'$compile',
|
||||||
|
'horizon.dashboard.project.lbaasv2.popovers',
|
||||||
|
'horizon.framework.util.i18n.gettext'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc controller
|
||||||
|
* @name AddMembersController
|
||||||
|
* @description
|
||||||
|
* The `AddMembersController` controller provides functions for adding members to a pool.
|
||||||
|
* @param $scope The angular scope object.
|
||||||
|
* @param $compile The angular compile service.
|
||||||
|
* @param popovers LBaaS v2 popover templates constant.
|
||||||
|
* @param gettext The horizon gettext function for translation.
|
||||||
|
* @returns undefined
|
||||||
|
*/
|
||||||
|
|
||||||
|
function AddMembersController($scope, $compile, popoverTemplates, gettext) {
|
||||||
|
|
||||||
|
var ctrl = this;
|
||||||
|
|
||||||
|
// Error text for invalid fields
|
||||||
|
ctrl.portError = gettext('The port must be a number between 1 and 65535.');
|
||||||
|
ctrl.weightError = gettext('The weight must be a number between 1 and 256.');
|
||||||
|
|
||||||
|
// Table widget properties
|
||||||
|
ctrl.tableData = {
|
||||||
|
available: $scope.model.members,
|
||||||
|
allocated: $scope.model.spec.members,
|
||||||
|
displayedAvailable: [],
|
||||||
|
displayedAllocated: []
|
||||||
|
};
|
||||||
|
ctrl.tableLimits = {
|
||||||
|
maxAllocation: -1
|
||||||
|
};
|
||||||
|
ctrl.tableHelp = {
|
||||||
|
availHelpText: '',
|
||||||
|
noneAllocText: gettext('Select members from the available members below'),
|
||||||
|
noneAvailText: gettext('No available members')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Functions to control the IP address popover
|
||||||
|
ctrl.showAddressPopover = showAddressPopover;
|
||||||
|
ctrl.hideAddressPopover = hideAddressPopover;
|
||||||
|
ctrl.addressPopoverTarget = addressPopoverTarget;
|
||||||
|
|
||||||
|
//////////
|
||||||
|
|
||||||
|
function showAddressPopover(event, member) {
|
||||||
|
var element = angular.element(event.target);
|
||||||
|
var scope = $scope.$new(true);
|
||||||
|
scope.member = member;
|
||||||
|
element.popover({
|
||||||
|
content: $compile(popoverTemplates.ipAddresses)(scope),
|
||||||
|
html: true,
|
||||||
|
placement: 'top',
|
||||||
|
title: interpolate(gettext('IP Addresses (%(count)s)'),
|
||||||
|
{ count: member.addresses.length }, true)
|
||||||
|
});
|
||||||
|
element.popover('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideAddressPopover(event) {
|
||||||
|
var element = angular.element(event.target);
|
||||||
|
element.popover('hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
function addressPopoverTarget(member) {
|
||||||
|
return interpolate(gettext('%(ip)s...'), { ip: member.address.ip }, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
* 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('Create Load Balancer Add Members Step', function() {
|
||||||
|
var members = [{
|
||||||
|
id: '1',
|
||||||
|
name: 'foo',
|
||||||
|
description: 'bar',
|
||||||
|
weight: 1,
|
||||||
|
port: 80,
|
||||||
|
address: { ip: '1.2.3.4', subnet: '1' },
|
||||||
|
addresses: [{ ip: '1.2.3.4', subnet: '1' },
|
||||||
|
{ ip: '2.3.4.5', subnet: '2' }]
|
||||||
|
}];
|
||||||
|
|
||||||
|
beforeEach(module('horizon.framework.util.i18n'));
|
||||||
|
beforeEach(module('horizon.dashboard.project.lbaasv2'));
|
||||||
|
|
||||||
|
describe('AddMembersController', function() {
|
||||||
|
var ctrl, scope;
|
||||||
|
|
||||||
|
beforeEach(inject(function($controller) {
|
||||||
|
scope = {
|
||||||
|
model: {
|
||||||
|
spec: {
|
||||||
|
members: []
|
||||||
|
},
|
||||||
|
members: members
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ctrl = $controller('AddMembersController', { $scope: scope });
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should define error messages for invalid fields', function() {
|
||||||
|
expect(ctrl.portError).toBeDefined();
|
||||||
|
expect(ctrl.weightError).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should define transfer table properties', function() {
|
||||||
|
expect(ctrl.tableData).toBeDefined();
|
||||||
|
expect(ctrl.tableLimits).toBeDefined();
|
||||||
|
expect(ctrl.tableHelp).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have available members', function() {
|
||||||
|
expect(ctrl.tableData.available).toBeDefined();
|
||||||
|
expect(ctrl.tableData.available.length).toBe(1);
|
||||||
|
expect(ctrl.tableData.available[0].id).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not have allocated members', function() {
|
||||||
|
expect(ctrl.tableData.allocated).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow adding multiple members', function() {
|
||||||
|
expect(ctrl.tableLimits.maxAllocation).toBe(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly format address popover target', function() {
|
||||||
|
var target = ctrl.addressPopoverTarget(members[0]);
|
||||||
|
expect(target).toBe('1.2.3.4...');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Add Members Template', function() {
|
||||||
|
var $scope, $element, popoverContent;
|
||||||
|
|
||||||
|
beforeEach(module('templates'));
|
||||||
|
beforeEach(module('horizon.dashboard.project.lbaasv2'));
|
||||||
|
|
||||||
|
beforeEach(inject(function($injector) {
|
||||||
|
var $compile = $injector.get('$compile');
|
||||||
|
var $templateCache = $injector.get('$templateCache');
|
||||||
|
var basePath = $injector.get('horizon.dashboard.project.lbaasv2.basePath');
|
||||||
|
var popoverTemplates = $injector.get('horizon.dashboard.project.lbaasv2.popovers');
|
||||||
|
var markup = $templateCache.get(
|
||||||
|
basePath + 'loadbalancers/actions/create/members/members.html');
|
||||||
|
$scope = $injector.get('$rootScope').$new();
|
||||||
|
$scope.model = {
|
||||||
|
spec: {
|
||||||
|
members: []
|
||||||
|
},
|
||||||
|
members: members
|
||||||
|
};
|
||||||
|
$element = $compile(markup)($scope);
|
||||||
|
var popoverScope = $injector.get('$rootScope').$new();
|
||||||
|
popoverScope.member = members[0];
|
||||||
|
popoverContent = $compile(popoverTemplates.ipAddresses)(popoverScope);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should show IP addresses popover on hover', function() {
|
||||||
|
var ctrl = $element.scope().ctrl;
|
||||||
|
ctrl.tableData.displayedAvailable = members;
|
||||||
|
$scope.$apply();
|
||||||
|
|
||||||
|
var popoverElement = $element.find('span.addresses-popover');
|
||||||
|
expect(popoverElement.length).toBe(1);
|
||||||
|
|
||||||
|
$.fn.popover = angular.noop;
|
||||||
|
spyOn($.fn, 'popover');
|
||||||
|
spyOn(ctrl, 'showAddressPopover').and.callThrough();
|
||||||
|
popoverElement.trigger('mouseover');
|
||||||
|
|
||||||
|
expect(ctrl.showAddressPopover).toHaveBeenCalledWith(
|
||||||
|
jasmine.objectContaining({type: 'mouseover'}), members[0]);
|
||||||
|
expect($.fn.popover.calls.count()).toBe(2);
|
||||||
|
expect($.fn.popover.calls.argsFor(0)[0]).toEqual({
|
||||||
|
content: popoverContent,
|
||||||
|
html: true,
|
||||||
|
placement: 'top',
|
||||||
|
title: 'IP Addresses (2)'
|
||||||
|
});
|
||||||
|
expect($.fn.popover.calls.argsFor(1)[0]).toBe('show');
|
||||||
|
|
||||||
|
spyOn(ctrl, 'hideAddressPopover').and.callThrough();
|
||||||
|
popoverElement.trigger('mouseleave');
|
||||||
|
|
||||||
|
expect(ctrl.hideAddressPopover)
|
||||||
|
.toHaveBeenCalledWith(jasmine.objectContaining({type: 'mouseleave'}));
|
||||||
|
expect($.fn.popover.calls.count()).toBe(3);
|
||||||
|
expect($.fn.popover.calls.argsFor(2)[0]).toBe('hide');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
})();
|
|
@ -0,0 +1,3 @@
|
||||||
|
<h1 translate>Pool Members Help</h1>
|
||||||
|
|
||||||
|
<p translate>Add members to the load balancer pool. The Available Members table contains existing instances that can be added to the pool.</p>
|
|
@ -0,0 +1,191 @@
|
||||||
|
<div ng-controller="AddMembersController as ctrl">
|
||||||
|
<h1 translate>Pool Members</h1>
|
||||||
|
|
||||||
|
<!--content-->
|
||||||
|
<div class="content">
|
||||||
|
<div translate class="subtitle">Add members to the load balancer pool. All required listener and pool details must also be provided.</div>
|
||||||
|
|
||||||
|
<transfer-table tr-model="ctrl.tableData"
|
||||||
|
limits="::ctrl.tableLimits"
|
||||||
|
help-text="::ctrl.tableHelp">
|
||||||
|
|
||||||
|
<!-- Allocated-->
|
||||||
|
<allocated>
|
||||||
|
<table st-table="ctrl.tableData.displayedAllocated"
|
||||||
|
st-safe-src="ctrl.tableData.allocated" hz-table
|
||||||
|
class="table-striped table-rsp table-detail modern form-group">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="expander"></th>
|
||||||
|
<th class="rsp-p2" translate>Name</th>
|
||||||
|
<th class="rsp-p3" translate>Description</th>
|
||||||
|
<th class="rsp-p1"
|
||||||
|
ng-class="{ 'required': ctrl.tableData.displayedAllocated.length > 0 }">
|
||||||
|
<label translate>IP Address</label>
|
||||||
|
</th>
|
||||||
|
<th class="rsp-p1" translate>Weight</th>
|
||||||
|
<th class="rsp-p1"
|
||||||
|
ng-class="{ 'required': ctrl.tableData.displayedAllocated.length > 0 }">
|
||||||
|
<label translate>Port</label>
|
||||||
|
</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-if="ctrl.tableData.allocated.length === 0">
|
||||||
|
<td colspan="0">
|
||||||
|
<div class="no-rows-help">
|
||||||
|
{$ ::trCtrl.helpText.noneAllocText $}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-repeat-start="row in ctrl.tableData.displayedAllocated track by row.id">
|
||||||
|
<td class="expander">
|
||||||
|
<i class="fa fa-chevron-right"
|
||||||
|
hz-expand-detail
|
||||||
|
duration="200">
|
||||||
|
</i>
|
||||||
|
</td>
|
||||||
|
<td class="rsp-p2">{$ ::row.name $}</td>
|
||||||
|
<td class="rsp-p3">{$ ::row.description | noValue $}</td>
|
||||||
|
<td class="rsp-p1">
|
||||||
|
<span ng-if="row.addresses.length === 1">{$ row.address.ip $}</span>
|
||||||
|
<div ng-if="row.addresses.length > 1"
|
||||||
|
class="form-field member-address">
|
||||||
|
<select class="form-control input-sm"
|
||||||
|
ng-options="addr.ip for addr in row.addresses"
|
||||||
|
ng-model="row.address" ng-required="true">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="rsp-p1">
|
||||||
|
<div class="form-field member-weight"
|
||||||
|
ng-class="{ 'has-error': createLoadBalancerMembersForm['{$ ::row.id $}-weight'].$invalid && createLoadBalancerMembersForm['{$ ::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">
|
||||||
|
</div>
|
||||||
|
<span class="fa fa-exclamation-triangle invalid"
|
||||||
|
ng-show="createLoadBalancerMembersForm['{$ ::row.id $}-weight'].$invalid && createLoadBalancerMembersForm.$dirty"
|
||||||
|
popover="{$ ::ctrl.weightError $}"
|
||||||
|
popover-placement="top" popover-append-to-body="true"
|
||||||
|
popover-trigger="hover"></span>
|
||||||
|
</td>
|
||||||
|
<td class="rsp-p1">
|
||||||
|
<div class="form-field member-port"
|
||||||
|
ng-class="{ 'has-error': createLoadBalancerMembersForm['{$ ::row.id $}-port'].$invalid && createLoadBalancerMembersForm['{$ ::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">
|
||||||
|
</div>
|
||||||
|
<span class="fa fa-exclamation-triangle invalid"
|
||||||
|
ng-show="createLoadBalancerMembersForm['{$ ::row.id $}-port'].$invalid && createLoadBalancerMembersForm.$dirty"
|
||||||
|
popover="{$ ::ctrl.portError $}"
|
||||||
|
popover-placement="top" popover-append-to-body="true"
|
||||||
|
popover-trigger="hover"></span>
|
||||||
|
</td>
|
||||||
|
<td class="action-col">
|
||||||
|
<action-list>
|
||||||
|
<action action-classes="'btn btn-sm btn-default'"
|
||||||
|
callback="trCtrl.deallocate" item="row">
|
||||||
|
<span class="fa fa-minus"></span>
|
||||||
|
</action>
|
||||||
|
</action-list>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-repeat-end class="detail-row">
|
||||||
|
<td class="detail" colspan="0">
|
||||||
|
<div class="row">
|
||||||
|
<dl class="rsp-alt-p2 col-sm-2">
|
||||||
|
<dt translate>Name</dt>
|
||||||
|
<dd>{$ ::row.name $}</dd>
|
||||||
|
</dl>
|
||||||
|
<dl class="rsp-alt-p3 col-sm-2">
|
||||||
|
<dt translate>Description</dt>
|
||||||
|
<dd>{$ ::row.description | noValue $}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</allocated>
|
||||||
|
|
||||||
|
<!-- Available -->
|
||||||
|
<available>
|
||||||
|
<table st-table="ctrl.tableData.displayedAvailable"
|
||||||
|
st-safe-src="ctrl.tableData.available"
|
||||||
|
hz-table class="table-striped table-rsp table-detail modern">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="search-header" colspan="0">
|
||||||
|
<hz-search-bar group-classes="input-group-sm" icon-classes="fa-search">
|
||||||
|
</hz-search-bar>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="expander"></th>
|
||||||
|
<th st-sort="name" st-sort-default class="rsp-p2" translate>Name</th>
|
||||||
|
<th class="rsp-p3" translate>Description</th>
|
||||||
|
<th class="rsp-p1" translate>IP Address</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-if="trCtrl.numAvailable() === 0">
|
||||||
|
<td colspan="0">
|
||||||
|
<div class="no-rows-help">
|
||||||
|
{$ ::trCtrl.helpText.noneAvailText $}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-repeat-start="row in ctrl.tableData.displayedAvailable track by row.id"
|
||||||
|
ng-if="!trCtrl.allocatedIds[row.id]">
|
||||||
|
<td class="expander">
|
||||||
|
<i class="fa fa-chevron-right"
|
||||||
|
hz-expand-detail
|
||||||
|
duration="200">
|
||||||
|
</i>
|
||||||
|
</td>
|
||||||
|
<td class="rsp-p2">{$ ::row.name $}</td>
|
||||||
|
<td class="rsp-p3">{$ ::row.description | noValue $}</td>
|
||||||
|
<td class="rsp-p1">
|
||||||
|
<span ng-if="row.addresses.length === 1">{$ row.address.ip $}</span>
|
||||||
|
<!-- The current version of the popover directive doesn't seem to allow HTML content (0.11.0) -->
|
||||||
|
<span ng-if="row.addresses.length > 1"
|
||||||
|
class="addresses-popover"
|
||||||
|
ng-mouseover="ctrl.showAddressPopover($event, row)"
|
||||||
|
ng-mouseleave="ctrl.hideAddressPopover($event)">
|
||||||
|
{$ ctrl.addressPopoverTarget(row) $}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="action-col">
|
||||||
|
<action-list>
|
||||||
|
<action action-classes="'btn btn-sm btn-default'"
|
||||||
|
callback="trCtrl.allocate" item="row">
|
||||||
|
<span class="fa fa-plus"></span>
|
||||||
|
</action>
|
||||||
|
</action-list>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-repeat-end class="detail-row">
|
||||||
|
<td class="detail" colspan="0">
|
||||||
|
<div class="row">
|
||||||
|
<dl class="rsp-alt-p2 col-sm-2">
|
||||||
|
<dt translate>Name</dt>
|
||||||
|
<dd>{$ ::row.name $}</dd>
|
||||||
|
</dl>
|
||||||
|
<dl class="rsp-alt-p3 col-sm-2">
|
||||||
|
<dt translate>Description</dt>
|
||||||
|
<dd>{$ ::row.description | noValue $}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</available>
|
||||||
|
|
||||||
|
</transfer-table> <!-- End Transfer Table -->
|
||||||
|
|
||||||
|
</div> <!-- end content -->
|
||||||
|
</div>
|
|
@ -24,7 +24,6 @@
|
||||||
modalService.$inject = [
|
modalService.$inject = [
|
||||||
'$modal',
|
'$modal',
|
||||||
'$location',
|
'$location',
|
||||||
'$window',
|
|
||||||
'horizon.framework.widgets.toast.service',
|
'horizon.framework.widgets.toast.service',
|
||||||
'horizon.framework.util.i18n.gettext',
|
'horizon.framework.util.i18n.gettext',
|
||||||
'horizon.app.core.openstack-service-api.policy'
|
'horizon.app.core.openstack-service-api.policy'
|
||||||
|
@ -39,14 +38,13 @@
|
||||||
*
|
*
|
||||||
* @param $modal The angular bootstrap $modal service.
|
* @param $modal The angular bootstrap $modal service.
|
||||||
* @param $location The angular $location service.
|
* @param $location The angular $location service.
|
||||||
* @param $window The angular reference to the browser window object.
|
|
||||||
* @param toastService The horizon toast service.
|
* @param toastService The horizon toast service.
|
||||||
* @param gettext The horizon gettext function for translation.
|
* @param gettext The horizon gettext function for translation.
|
||||||
* @param policy The horizon policy service.
|
* @param policy The horizon policy service.
|
||||||
* @returns The modal service for the create load balancer workflow.
|
* @returns The modal service for the create load balancer workflow.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function modalService($modal, $location, $window, toastService, gettext, policy) {
|
function modalService($modal, $location, toastService, gettext, policy) {
|
||||||
|
|
||||||
var service = {
|
var service = {
|
||||||
allowed: allowed,
|
allowed: allowed,
|
||||||
|
@ -77,7 +75,7 @@
|
||||||
var modal = $modal.open(spec);
|
var modal = $modal.open(spec);
|
||||||
modal.result.then(function(response) {
|
modal.result.then(function(response) {
|
||||||
toastService.add('success', gettext('A new load balancer is being created.'));
|
toastService.add('success', gettext('A new load balancer is being created.'));
|
||||||
$location.path($window.WEBROOT + 'project/ngloadbalancersv2/detail/' + response.data.id);
|
$location.path('project/ngloadbalancersv2/detail/' + response.data.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@
|
||||||
modalService.perform();
|
modalService.perform();
|
||||||
|
|
||||||
expect(toastService.add).toHaveBeenCalledWith('success', jasmine.any(String));
|
expect(toastService.add).toHaveBeenCalledWith('success', jasmine.any(String));
|
||||||
expect($location.path).toHaveBeenCalledWith('/project/ngloadbalancersv2/detail/1');
|
expect($location.path).toHaveBeenCalledWith('project/ngloadbalancersv2/detail/1');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var push = Array.prototype.push;
|
||||||
|
|
||||||
angular
|
angular
|
||||||
.module('horizon.dashboard.project.lbaasv2.loadbalancers')
|
.module('horizon.dashboard.project.lbaasv2.loadbalancers')
|
||||||
.factory('horizon.dashboard.project.lbaasv2.loadbalancers.actions.create.model',
|
.factory('horizon.dashboard.project.lbaasv2.loadbalancers.actions.create.model',
|
||||||
|
@ -24,6 +26,7 @@
|
||||||
createLoadBalancerModel.$inject = [
|
createLoadBalancerModel.$inject = [
|
||||||
'$q',
|
'$q',
|
||||||
'horizon.app.core.openstack-service-api.neutron',
|
'horizon.app.core.openstack-service-api.neutron',
|
||||||
|
'horizon.app.core.openstack-service-api.nova',
|
||||||
'horizon.app.core.openstack-service-api.lbaasv2',
|
'horizon.app.core.openstack-service-api.lbaasv2',
|
||||||
'horizon.framework.util.i18n.gettext'
|
'horizon.framework.util.i18n.gettext'
|
||||||
];
|
];
|
||||||
|
@ -40,13 +43,14 @@
|
||||||
*
|
*
|
||||||
* @param $q The angular service for promises.
|
* @param $q The angular service for promises.
|
||||||
* @param neutronAPI The neutron service API.
|
* @param neutronAPI The neutron service API.
|
||||||
|
* @param novaAPI The nova service API.
|
||||||
* @param lbaasv2API The LBaaS V2 service API.
|
* @param lbaasv2API The LBaaS V2 service API.
|
||||||
* @param gettext The horizon gettext function for translation.
|
* @param gettext The horizon gettext function for translation.
|
||||||
* @returns The model service for the create load balancer workflow.
|
* @returns The model service for the create load balancer workflow.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function createLoadBalancerModel($q, neutronAPI, lbaasv2API, gettext) {
|
function createLoadBalancerModel($q, neutronAPI, novaAPI, lbaasv2API, gettext) {
|
||||||
var initPromise;
|
var initPromise, ports;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @ngdoc model api object
|
* @ngdoc model api object
|
||||||
|
@ -71,6 +75,7 @@
|
||||||
spec: null,
|
spec: null,
|
||||||
|
|
||||||
subnets: [],
|
subnets: [],
|
||||||
|
members: [],
|
||||||
listenerProtocols: ['TCP', 'HTTP', 'HTTPS'],
|
listenerProtocols: ['TCP', 'HTTP', 'HTTPS'],
|
||||||
poolProtocols: ['TCP', 'HTTP', 'HTTPS'],
|
poolProtocols: ['TCP', 'HTTP', 'HTTPS'],
|
||||||
methods: ['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP'],
|
methods: ['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP'],
|
||||||
|
@ -113,7 +118,8 @@
|
||||||
description: null,
|
description: null,
|
||||||
protocol: null,
|
protocol: null,
|
||||||
method: null
|
method: null
|
||||||
}
|
},
|
||||||
|
members: []
|
||||||
};
|
};
|
||||||
|
|
||||||
if (model.initializing) {
|
if (model.initializing) {
|
||||||
|
@ -123,7 +129,9 @@
|
||||||
|
|
||||||
promise = $q.all([
|
promise = $q.all([
|
||||||
lbaasv2API.getLoadBalancers().then(onGetLoadBalancers),
|
lbaasv2API.getLoadBalancers().then(onGetLoadBalancers),
|
||||||
neutronAPI.getSubnets().then(onGetSubnets)
|
neutronAPI.getSubnets().then(onGetSubnets),
|
||||||
|
neutronAPI.getPorts().then(onGetPorts),
|
||||||
|
novaAPI.getServers().then(onGetServers)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
promise.then(onInitSuccess, onInitFail);
|
promise.then(onInitSuccess, onInitFail);
|
||||||
|
@ -133,6 +141,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function onInitSuccess() {
|
function onInitSuccess() {
|
||||||
|
initMemberAddresses();
|
||||||
model.initializing = false;
|
model.initializing = false;
|
||||||
model.initialized = true;
|
model.initialized = true;
|
||||||
}
|
}
|
||||||
|
@ -166,9 +175,24 @@
|
||||||
delete finalSpec.pool;
|
delete finalSpec.pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Members require a pool, address, subnet, and port but the wizard requires the address,
|
||||||
|
// subnet, and port so we can assume those exist here.
|
||||||
|
if (!finalSpec.pool || finalSpec.members.length === 0) {
|
||||||
|
delete finalSpec.members;
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.forEach(finalSpec.members, function cleanMember(member) {
|
||||||
|
delete member.id;
|
||||||
|
delete member.addresses;
|
||||||
|
delete member.name;
|
||||||
|
delete member.description;
|
||||||
|
member.subnet = member.address.subnet;
|
||||||
|
member.address = member.address.ip;
|
||||||
|
});
|
||||||
|
|
||||||
// Delete null properties
|
// Delete null properties
|
||||||
angular.forEach(finalSpec, function(group, groupName) {
|
angular.forEach(finalSpec, function deleteNullsForGroup(group, groupName) {
|
||||||
angular.forEach(group, function(value, key) {
|
angular.forEach(group, function deleteNullValue(value, key) {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
delete finalSpec[groupName][key];
|
delete finalSpec[groupName][key];
|
||||||
}
|
}
|
||||||
|
@ -195,9 +219,43 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function onGetSubnets(response) {
|
function onGetSubnets(response) {
|
||||||
model.subnets = [];
|
model.subnets.length = 0;
|
||||||
angular.forEach(response.data.items, function(subnet) {
|
push.apply(model.subnets, response.data.items);
|
||||||
model.subnets.push(subnet);
|
}
|
||||||
|
|
||||||
|
function onGetServers(response) {
|
||||||
|
model.members.length = 0;
|
||||||
|
var members = [];
|
||||||
|
angular.forEach(response.data.items, function(server) {
|
||||||
|
members.push({
|
||||||
|
id: server.id,
|
||||||
|
name: server.name,
|
||||||
|
description: server.description,
|
||||||
|
weight: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
push.apply(model.members, members);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGetPorts(response) {
|
||||||
|
ports = response.data.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMemberAddresses() {
|
||||||
|
angular.forEach(model.members, function(member) {
|
||||||
|
var memberPorts = ports.filter(function(port) {
|
||||||
|
return port.device_id === member.id;
|
||||||
|
});
|
||||||
|
member.addresses = [];
|
||||||
|
angular.forEach(memberPorts, function(port) {
|
||||||
|
angular.forEach(port.fixed_ips, function(ip) {
|
||||||
|
member.addresses.push({
|
||||||
|
ip: ip.ip_address,
|
||||||
|
subnet: ip.subnet_id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
member.address = member.addresses[0];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
describe('LBaaS v2 Create Load Balancer Workflow Model Service', function() {
|
describe('LBaaS v2 Create Load Balancer Workflow Model Service', function() {
|
||||||
var model, $q, scope;
|
var model, $q, scope;
|
||||||
|
|
||||||
|
beforeEach(module('horizon.framework.util.i18n'));
|
||||||
beforeEach(module('horizon.dashboard.project.lbaasv2'));
|
beforeEach(module('horizon.dashboard.project.lbaasv2'));
|
||||||
|
|
||||||
beforeEach(module(function($provide) {
|
beforeEach(module(function($provide) {
|
||||||
|
@ -44,6 +45,31 @@
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
deferred.resolve({ data: { items: subnets } });
|
deferred.resolve({ data: { items: subnets } });
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
},
|
||||||
|
getPorts: function() {
|
||||||
|
var ports = [ { device_id: '1',
|
||||||
|
fixed_ips: [{ ip_address: '1.2.3.4', subnet_id: '1' },
|
||||||
|
{ ip_address: '2.3.4.5', subnet_id: '2' }] },
|
||||||
|
{ device_id: '2',
|
||||||
|
fixed_ips: [{ ip_address: '3.4.5.6', subnet_id: '1' },
|
||||||
|
{ ip_address: '4.5.6.7', subnet_id: '2' }] } ];
|
||||||
|
|
||||||
|
var deferred = $q.defer();
|
||||||
|
deferred.resolve({ data: { items: ports } });
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$provide.value('horizon.app.core.openstack-service-api.nova', {
|
||||||
|
getServers: function() {
|
||||||
|
var servers = [ { id: '1', name: 'server-1' },
|
||||||
|
{ id: '2', name: 'server-2' } ];
|
||||||
|
|
||||||
|
var deferred = $q.defer();
|
||||||
|
deferred.resolve({ data: { items: servers } });
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -76,6 +102,10 @@
|
||||||
expect(model.subnets).toEqual([]);
|
expect(model.subnets).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has empty members array', function() {
|
||||||
|
expect(model.members).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('has array of pool protocols', function() {
|
it('has array of pool protocols', function() {
|
||||||
expect(model.poolProtocols).toEqual(['TCP', 'HTTP', 'HTTPS']);
|
expect(model.poolProtocols).toEqual(['TCP', 'HTTP', 'HTTPS']);
|
||||||
});
|
});
|
||||||
|
@ -108,10 +138,12 @@
|
||||||
expect(model.initializing).toBe(false);
|
expect(model.initializing).toBe(false);
|
||||||
expect(model.initialized).toBe(true);
|
expect(model.initialized).toBe(true);
|
||||||
expect(model.subnets.length).toBe(2);
|
expect(model.subnets.length).toBe(2);
|
||||||
|
expect(model.members.length).toBe(2);
|
||||||
expect(model.spec).toBeDefined();
|
expect(model.spec).toBeDefined();
|
||||||
expect(model.spec.loadbalancer).toBeDefined();
|
expect(model.spec.loadbalancer).toBeDefined();
|
||||||
expect(model.spec.listener).toBeDefined();
|
expect(model.spec.listener).toBeDefined();
|
||||||
expect(model.spec.pool).toBeDefined();
|
expect(model.spec.pool).toBeDefined();
|
||||||
|
expect(model.spec.members).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize names', function() {
|
it('should initialize names', function() {
|
||||||
|
@ -156,7 +188,7 @@
|
||||||
// This is here to ensure that as people add/change spec properties, they don't forget
|
// This is here to ensure that as people add/change spec properties, they don't forget
|
||||||
// to implement tests for them.
|
// to implement tests for them.
|
||||||
it('has the right number of properties', function() {
|
it('has the right number of properties', function() {
|
||||||
expect(Object.keys(model.spec).length).toBe(3);
|
expect(Object.keys(model.spec).length).toBe(4);
|
||||||
expect(Object.keys(model.spec.loadbalancer).length).toBe(4);
|
expect(Object.keys(model.spec.loadbalancer).length).toBe(4);
|
||||||
expect(Object.keys(model.spec.listener).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.pool).length).toBe(4);
|
||||||
|
@ -227,6 +259,16 @@
|
||||||
model.spec.pool.description = 'pool description';
|
model.spec.pool.description = 'pool description';
|
||||||
model.spec.pool.protocol = 'HTTP';
|
model.spec.pool.protocol = 'HTTP';
|
||||||
model.spec.pool.method = 'LEAST_CONNECTIONS';
|
model.spec.pool.method = 'LEAST_CONNECTIONS';
|
||||||
|
model.spec.members = [{
|
||||||
|
address: { ip: '1.2.3.4', subnet: '1' },
|
||||||
|
addresses: [{ ip: '1.2.3.4', subnet: '1' },
|
||||||
|
{ ip: '2.3.4.5', subnet: '2' }],
|
||||||
|
id: '1',
|
||||||
|
name: 'foo',
|
||||||
|
description: 'bar',
|
||||||
|
port: 80,
|
||||||
|
weight: 1
|
||||||
|
}];
|
||||||
|
|
||||||
var finalSpec = model.createLoadBalancer();
|
var finalSpec = model.createLoadBalancer();
|
||||||
|
|
||||||
|
@ -242,6 +284,15 @@
|
||||||
expect(finalSpec.pool.description).toBe('pool description');
|
expect(finalSpec.pool.description).toBe('pool description');
|
||||||
expect(finalSpec.pool.protocol).toBe('HTTP');
|
expect(finalSpec.pool.protocol).toBe('HTTP');
|
||||||
expect(finalSpec.pool.method).toBe('LEAST_CONNECTIONS');
|
expect(finalSpec.pool.method).toBe('LEAST_CONNECTIONS');
|
||||||
|
expect(finalSpec.members.length).toBe(1);
|
||||||
|
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].addresses).toBeUndefined();
|
||||||
|
expect(finalSpec.members[0].id).toBeUndefined();
|
||||||
|
expect(finalSpec.members[0].name).toBeUndefined();
|
||||||
|
expect(finalSpec.members[0].description).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete listener if any required property is not set', function() {
|
it('should delete listener if any required property is not set', function() {
|
||||||
|
@ -268,6 +319,22 @@
|
||||||
expect(finalSpec.listener).toBeDefined();
|
expect(finalSpec.listener).toBeDefined();
|
||||||
expect(finalSpec.pool).toBeUndefined();
|
expect(finalSpec.pool).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should delete members if none selected', function() {
|
||||||
|
model.spec.loadbalancer.ip = '1.2.3.4';
|
||||||
|
model.spec.loadbalancer.subnet = model.subnets[0];
|
||||||
|
model.spec.listener.protocol = 'HTTPS';
|
||||||
|
model.spec.listener.port = 80;
|
||||||
|
model.spec.pool.protocol = 'HTTP';
|
||||||
|
model.spec.pool.method = 'LEAST_CONNECTIONS';
|
||||||
|
|
||||||
|
var finalSpec = model.createLoadBalancer();
|
||||||
|
|
||||||
|
expect(finalSpec.loadbalancer).toBeDefined();
|
||||||
|
expect(finalSpec.listener).toBeDefined();
|
||||||
|
expect(finalSpec.pool).toBeDefined();
|
||||||
|
expect(finalSpec.members).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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('CreatePoolDetailsController', CreatePoolDetailsController);
|
||||||
|
|
||||||
|
CreatePoolDetailsController.$inject = [
|
||||||
|
'$scope'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc controller
|
||||||
|
* @name CreatePoolDetailsController
|
||||||
|
* @description
|
||||||
|
* The `CreatePoolDetailsController` controller provides functions for configuring
|
||||||
|
* pool details.
|
||||||
|
* @param $scope The angular scope object.
|
||||||
|
* @returns undefined
|
||||||
|
*/
|
||||||
|
|
||||||
|
function CreatePoolDetailsController($scope) {
|
||||||
|
|
||||||
|
var ctrl = this;
|
||||||
|
|
||||||
|
ctrl.protocolChange = protocolChange;
|
||||||
|
|
||||||
|
//////////
|
||||||
|
|
||||||
|
function protocolChange(protocol) {
|
||||||
|
var port = '';
|
||||||
|
if (protocol === 'HTTP') {
|
||||||
|
port = 80;
|
||||||
|
} else if (protocol === 'HTTPS') {
|
||||||
|
port = 443;
|
||||||
|
}
|
||||||
|
$scope.model.members.forEach(function setPort(member) {
|
||||||
|
member.port = port;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* 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('Create Pool Details Step', function() {
|
||||||
|
var ctrl;
|
||||||
|
var members = [{port: ''}, {port: ''}];
|
||||||
|
|
||||||
|
beforeEach(module('horizon.dashboard.project.lbaasv2'));
|
||||||
|
|
||||||
|
beforeEach(inject(function($controller) {
|
||||||
|
var scope = {
|
||||||
|
model: {
|
||||||
|
members: members
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ctrl = $controller('CreatePoolDetailsController', { $scope: scope });
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should define protocolChange function', function() {
|
||||||
|
expect(ctrl.protocolChange).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update member ports on protocol change to HTTP', function() {
|
||||||
|
ctrl.protocolChange('HTTP');
|
||||||
|
|
||||||
|
members.forEach(function(member) {
|
||||||
|
expect(member.port).toBe(80);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update member ports on protocol change to HTTPS', function() {
|
||||||
|
ctrl.protocolChange('HTTPS');
|
||||||
|
|
||||||
|
members.forEach(function(member) {
|
||||||
|
expect(member.port).toBe(443);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update member ports on protocol change to TCP', function() {
|
||||||
|
ctrl.protocolChange('TCP');
|
||||||
|
|
||||||
|
members.forEach(function(member) {
|
||||||
|
expect(member.port).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
|
@ -1,4 +1,4 @@
|
||||||
<div>
|
<div ng-controller="CreatePoolDetailsController as ctrl">
|
||||||
<h1 translate>Pool Details</h1>
|
<h1 translate>Pool Details</h1>
|
||||||
|
|
||||||
<!--content-->
|
<!--content-->
|
||||||
|
@ -35,7 +35,8 @@
|
||||||
<select class="form-control input-sm" name="pool-protocol"
|
<select class="form-control input-sm" name="pool-protocol"
|
||||||
id="pool-protocol"
|
id="pool-protocol"
|
||||||
ng-options="protocol for protocol in model.poolProtocols"
|
ng-options="protocol for protocol in model.poolProtocols"
|
||||||
ng-model="model.spec.pool.protocol">
|
ng-model="model.spec.pool.protocol"
|
||||||
|
ng-change="ctrl.protocolChange(model.spec.pool.protocol)">
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -52,6 +52,13 @@
|
||||||
templateUrl: basePath + 'loadbalancers/actions/create/pool/pool.html',
|
templateUrl: basePath + 'loadbalancers/actions/create/pool/pool.html',
|
||||||
helpUrl: basePath + 'loadbalancers/actions/create/pool/pool.help.html',
|
helpUrl: basePath + 'loadbalancers/actions/create/pool/pool.help.html',
|
||||||
formName: 'createLoadBalancerPoolForm'
|
formName: 'createLoadBalancerPoolForm'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'members',
|
||||||
|
title: gettext('Pool Members'),
|
||||||
|
templateUrl: basePath + 'loadbalancers/actions/create/members/members.html',
|
||||||
|
helpUrl: basePath + 'loadbalancers/actions/create/members/members.help.html',
|
||||||
|
formName: 'createLoadBalancerMembersForm'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -41,12 +41,13 @@
|
||||||
|
|
||||||
it('should have steps defined', function () {
|
it('should have steps defined', function () {
|
||||||
expect(createLoadBalancerWorkflow.steps).toBeDefined();
|
expect(createLoadBalancerWorkflow.steps).toBeDefined();
|
||||||
expect(createLoadBalancerWorkflow.steps.length).toBe(3);
|
expect(createLoadBalancerWorkflow.steps.length).toBe(4);
|
||||||
|
|
||||||
var forms = [
|
var forms = [
|
||||||
'createLoadBalancerDetailsForm',
|
'createLoadBalancerDetailsForm',
|
||||||
'createLoadBalancerListenerForm',
|
'createLoadBalancerListenerForm',
|
||||||
'createLoadBalancerPoolForm'
|
'createLoadBalancerPoolForm',
|
||||||
|
'createLoadBalancerMembersForm'
|
||||||
];
|
];
|
||||||
|
|
||||||
forms.forEach(function(expectedForm, idx) {
|
forms.forEach(function(expectedForm, idx) {
|
||||||
|
|
|
@ -22,8 +22,7 @@
|
||||||
|
|
||||||
LoadBalancerDetailController.$inject = [
|
LoadBalancerDetailController.$inject = [
|
||||||
'horizon.app.core.openstack-service-api.lbaasv2',
|
'horizon.app.core.openstack-service-api.lbaasv2',
|
||||||
'$routeParams',
|
'$routeParams'
|
||||||
'$window'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,15 +34,13 @@
|
||||||
*
|
*
|
||||||
* @param api The LBaaS v2 API service.
|
* @param api The LBaaS v2 API service.
|
||||||
* @param $routeParams The angular $routeParams service.
|
* @param $routeParams The angular $routeParams service.
|
||||||
* @param $window The angular reference to the browser window object.
|
|
||||||
* @returns undefined
|
* @returns undefined
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function LoadBalancerDetailController(api, $routeParams, $window) {
|
function LoadBalancerDetailController(api, $routeParams) {
|
||||||
|
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
ctrl.loadbalancer = {};
|
ctrl.loadbalancer = {};
|
||||||
ctrl.webroot = $window.webroot;
|
|
||||||
|
|
||||||
var loadbalancerId = $routeParams.loadbalancerId;
|
var loadbalancerId = $routeParams.loadbalancerId;
|
||||||
|
|
||||||
|
|
|
@ -39,13 +39,13 @@
|
||||||
<div>
|
<div>
|
||||||
<dt translate>Subnet ID</dt>
|
<dt translate>Subnet ID</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<a target="_self" ng-href="{$ ::ctrl.webroot $}project/networks/subnets/{$ ::ctrl.loadbalancer.vip_subnet_id $}/detail">{$ ::ctrl.loadbalancer.vip_subnet_id $}</a>
|
<a target="_self" ng-href="project/networks/subnets/{$ ::ctrl.loadbalancer.vip_subnet_id $}/detail">{$ ::ctrl.loadbalancer.vip_subnet_id $}</a>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt translate>Port ID</dt>
|
<dt translate>Port ID</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<a target="_self" ng-href="{$ ::ctrl.webroot $}project/networks/ports/{$ ::ctrl.loadbalancer.vip_port_id $}/detail">{$ ::ctrl.loadbalancer.vip_port_id $}</a>
|
<a target="_self" ng-href="project/networks/ports/{$ ::ctrl.loadbalancer.vip_port_id $}/detail">{$ ::ctrl.loadbalancer.vip_port_id $}</a>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
|
@ -21,8 +21,6 @@
|
||||||
.controller('LoadBalancersTableController', LoadBalancersTableController);
|
.controller('LoadBalancersTableController', LoadBalancersTableController);
|
||||||
|
|
||||||
LoadBalancersTableController.$inject = [
|
LoadBalancersTableController.$inject = [
|
||||||
'$scope',
|
|
||||||
'$window',
|
|
||||||
'horizon.app.core.openstack-service-api.lbaasv2',
|
'horizon.app.core.openstack-service-api.lbaasv2',
|
||||||
'horizon.dashboard.project.lbaasv2.loadbalancers.actions.batchActions'
|
'horizon.dashboard.project.lbaasv2.loadbalancers.actions.batchActions'
|
||||||
];
|
];
|
||||||
|
@ -34,20 +32,17 @@
|
||||||
* @description
|
* @description
|
||||||
* Controller for the LBaaS v2 load balancers table. Serves as the focal point for table actions.
|
* Controller for the LBaaS v2 load balancers table. Serves as the focal point for table actions.
|
||||||
*
|
*
|
||||||
* @param $scope The angular $scope object.
|
|
||||||
* @param $window The angular reference to the browser window object.
|
|
||||||
* @param api The LBaaS V2 service API.
|
* @param api The LBaaS V2 service API.
|
||||||
* @param batchActions The load balancer batch actions service.
|
* @param batchActions The load balancer batch actions service.
|
||||||
* @returns undefined
|
* @returns undefined
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function LoadBalancersTableController($scope, $window, api, batchActions) {
|
function LoadBalancersTableController(api, batchActions) {
|
||||||
|
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
ctrl.items = [];
|
ctrl.items = [];
|
||||||
ctrl.src = [];
|
ctrl.src = [];
|
||||||
ctrl.checked = {};
|
ctrl.checked = {};
|
||||||
ctrl.webroot = $window.webroot;
|
|
||||||
ctrl.batchActions = batchActions;
|
ctrl.batchActions = batchActions;
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
Table-batch-actions:
|
Table-batch-actions:
|
||||||
This is where batch actions like searching, creating, and deleting.
|
This is where batch actions like searching, creating, and deleting.
|
||||||
-->
|
-->
|
||||||
<th colspan="100" class="search-header">
|
<th colspan="0" class="search-header">
|
||||||
<hz-search-bar group-classes="input-group-sm" icon-classes="fa-search">
|
<hz-search-bar group-classes="input-group-sm" icon-classes="fa-search">
|
||||||
<actions allowed="table.batchActions.actions" type="batch"></actions>
|
<actions allowed="table.batchActions.actions" type="batch"></actions>
|
||||||
</hz-search-bar>
|
</hz-search-bar>
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
duration="200">
|
duration="200">
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="rsp-p1"><a ng-href="{$ ::ctrl.webroot $}project/ngloadbalancersv2/detail/{$ ::item.id $}">{$ item.name $}</a></td>
|
<td class="rsp-p1"><a ng-href="project/ngloadbalancersv2/detail/{$ ::item.id $}">{$ item.name $}</a></td>
|
||||||
<td class="rsp-p1">{$ item.description | noValue $}</td>
|
<td class="rsp-p1">{$ item.description | noValue $}</td>
|
||||||
<td class="rsp-p1">{$ item.operating_status | operatingStatus $}</td>
|
<td class="rsp-p1">{$ item.operating_status | operatingStatus $}</td>
|
||||||
<td class="rsp-p1">{$ item.provisioning_status | provisioningStatus $}</td>
|
<td class="rsp-p1">{$ item.provisioning_status | provisioningStatus $}</td>
|
||||||
|
@ -86,7 +86,7 @@
|
||||||
Can be toggled using the chevron button.
|
Can be toggled using the chevron button.
|
||||||
Ensure colspan is greater or equal to number of column-headers.
|
Ensure colspan is greater or equal to number of column-headers.
|
||||||
-->
|
-->
|
||||||
<td class="detail" colspan="100">
|
<td class="detail" colspan="0">
|
||||||
<!--
|
<!--
|
||||||
The responsive columns that disappear typically should reappear here
|
The responsive columns that disappear typically should reappear here
|
||||||
with the same responsive priority that they disappear.
|
with the same responsive priority that they disappear.
|
||||||
|
|
Loading…
Reference in New Issue