Add monitor tab to create load balancer workflow

This adds the monitor tab to the create load balancer workflow and
allows adding a monitor to the pool.

Partially-Implements: blueprint horizon-lbaas-v2-ui
Change-Id: I23ee916f283b2b3f782327f16ca1624a98568cb0
This commit is contained in:
Justin Pomeroy 2015-12-14 15:49:46 -06:00
parent 9c4ca8d5f9
commit 729c223f77
14 changed files with 469 additions and 36 deletions

View File

@ -105,6 +105,10 @@ def create_pool(request, **kwargs):
kwargs = {'callback_kwargs': {'pool_id': pool['id'],
'index': 0}}
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': pool['id']}}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
return pool
@ -133,10 +137,36 @@ def add_member(request, **kwargs):
kwargs = {'callback_kwargs': {'pool_id': kwargs['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']}}
thread.start_new_thread(poll_loadbalancer_status, args, kwargs)
return member
def add_monitor(request, **kwargs):
"""Create a new health monitor for a pool.
"""
data = request.DATA
monitorSpec = {
'type': data['monitor']['type'],
'delay': data['monitor']['interval'],
'timeout': data['monitor']['timeout'],
'max_retries': data['monitor']['retry'],
'pool_id': kwargs['pool_id']
}
if data['monitor'].get('method'):
monitorSpec['http_method'] = data['monitor']['method']
if data['monitor'].get('path'):
monitorSpec['url_path'] = data['monitor']['path']
if data['monitor'].get('status'):
monitorSpec['expected_codes'] = data['monitor']['status']
return neutronclient(request).create_lbaas_healthmonitor(
{'healthmonitor': monitorSpec}).get('healthmonitor')
@urls.register
class LoadBalancers(generic.View):
"""API for load balancers.

View File

@ -51,7 +51,9 @@ ADD_JS_FILES = [
('dashboard/project/lbaasv2/loadbalancers/actions/create/pool/'
'pool.controller.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/members/'
'members.controller.js')
'members.controller.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/monitor/'
'monitor.controller.js')
]
ADD_JS_SPEC_FILES = [
@ -78,7 +80,9 @@ ADD_JS_SPEC_FILES = [
('dashboard/project/lbaasv2/loadbalancers/actions/create/pool/'
'pool.controller.spec.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/members/'
'members.controller.spec.js')
'members.controller.spec.js'),
('dashboard/project/lbaasv2/loadbalancers/actions/create/monitor/'
'monitor.controller.spec.js')
]
ADD_SCSS_FILES = [

View File

@ -29,12 +29,16 @@
'horizon.dashboard.project.lbaasv2.loadbalancers'
])
.config(config)
/* eslint-disable max-len */
.constant('horizon.dashboard.project.lbaasv2.patterns', {
/* eslint-disable max-len */
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 */
// HTTP status codes - comma separated numbers or ranges
httpStatusCodes: /^\d+(\s*-\s*\d+)?(,\s*\d+(\s*-\s*\d+)?)*$/,
// URL path - must start with "/" and can include anything after that
urlPath: /^((\/)|(\/[^/]+)+)$/
})
/* eslint-enable max-len */
.constant('horizon.dashboard.project.lbaasv2.popovers', {
ipAddresses: '<ul><li ng-repeat="addr in member.addresses">{$ addr.ip $}</li></ul>'
});
@ -42,13 +46,10 @@
config.$inject = [
'$provide',
'$windowProvider',
'$routeProvider',
'$locationProvider'
'$routeProvider'
];
function config($provide, $windowProvider, $routeProvider, $locationProvider) {
$locationProvider.html5Mode(true).hashPrefix('!');
function config($provide, $windowProvider, $routeProvider) {
var href = '/project/ngloadbalancersv2/';
var basePath = $windowProvider.$get().STATIC_URL + 'dashboard/project/lbaasv2/';
$provide.constant('horizon.dashboard.project.lbaasv2.basePath', basePath);

View File

@ -41,19 +41,52 @@
});
});
describe('LBaaS v2 Module Constants', function () {
var patterns, popovers;
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(inject(function ($injector) {
patterns = $injector.get('horizon.dashboard.project.lbaasv2.patterns');
popovers = $injector.get('horizon.dashboard.project.lbaasv2.popovers');
}));
it('should define patterns', function () {
expect(patterns).toBeDefined();
});
it('should define expected patterns', function () {
expect(Object.keys(patterns).length).toBe(4);
var keys = ['ipv4', 'ipv6', 'httpStatusCodes', 'urlPath'];
angular.forEach(keys, function(key) {
expect(patterns[key]).toBeDefined();
});
});
it('should define popovers', function () {
expect(popovers).toBeDefined();
});
it('should define expected popover templates', function () {
expect(Object.keys(popovers).length).toBe(1);
var keys = ['ipAddresses'];
angular.forEach(keys, function(key) {
expect(popovers[key]).toBeDefined();
});
});
});
describe('LBaaS v2 Module Config', function () {
var $routeProvider, $locationProvider, basePath;
var $routeProvider, basePath;
beforeEach(function() {
// Create a dummy module so that we can test $routeProvider and $locationProvider calls
// in our actual config block.
// Create a dummy module so that we can test $routeProvider calls in our actual
// config block.
angular.module('configTest', [])
.config(function(_$routeProvider_, _$locationProvider_, $windowProvider) {
.config(function(_$routeProvider_, $windowProvider) {
$routeProvider = _$routeProvider_;
$locationProvider = _$locationProvider_;
basePath = $windowProvider.$get().STATIC_URL + 'dashboard/project/lbaasv2/';
spyOn($routeProvider, 'when').and.callThrough();
spyOn($locationProvider, 'html5Mode').and.callThrough();
});
module('ngRoute');
module('configTest');
@ -61,10 +94,6 @@
inject();
});
it('should use html5 mode', function () {
expect($locationProvider.html5Mode).toHaveBeenCalledWith(true);
});
it('should route URLs', function () {
var href = '/project/ngloadbalancersv2/';
var routes = [

View File

@ -33,7 +33,7 @@
</thead>
<tbody>
<tr ng-if="ctrl.tableData.allocated.length === 0">
<td colspan="0">
<td colspan="100">
<div class="no-rows-help">
{$ ::trCtrl.helpText.noneAllocText $}
</div>
@ -93,7 +93,7 @@
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td class="detail" colspan="0">
<td class="detail" colspan="100">
<div class="row">
<dl class="rsp-alt-p2 col-sm-2">
<dt translate>Name</dt>
@ -117,7 +117,7 @@
hz-table class="table-striped table-rsp table-detail modern">
<thead>
<tr>
<th class="search-header" colspan="0">
<th class="search-header" colspan="100">
<hz-search-bar group-classes="input-group-sm" icon-classes="fa-search">
</hz-search-bar>
</th>
@ -132,7 +132,7 @@
</thead>
<tbody>
<tr ng-if="trCtrl.numAvailable() === 0">
<td colspan="0">
<td colspan="100">
<div class="no-rows-help">
{$ ::trCtrl.helpText.noneAvailText $}
</div>
@ -168,7 +168,7 @@
</td>
</tr>
<tr ng-repeat-end class="detail-row">
<td class="detail" colspan="0">
<td class="detail" colspan="100">
<div class="row">
<dl class="rsp-alt-p2 col-sm-2">
<dt translate>Name</dt>

View File

@ -79,6 +79,8 @@
listenerProtocols: ['TCP', 'HTTP', 'HTTPS'],
poolProtocols: ['TCP', 'HTTP', 'HTTPS'],
methods: ['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP'],
monitorTypes: ['HTTP', 'HTTPS', 'PING', 'TCP'],
monitorMethods: ['GET', 'HEAD'],
/**
* api methods for UI controllers
@ -119,6 +121,15 @@
protocol: null,
method: null
},
monitor: {
type: null,
interval: null,
retry: null,
timeout: null,
method: 'GET',
status: '200',
path: '/'
},
members: []
};
@ -175,6 +186,17 @@
delete finalSpec.pool;
}
cleanFinalSpecMembers(finalSpec);
cleanFinalSpecMonitor(finalSpec);
removeNulls(finalSpec);
finalSpec.loadbalancer.subnet = finalSpec.loadbalancer.subnet.id;
return lbaasv2API.createLoadBalancer(finalSpec);
}
function cleanFinalSpecMembers(finalSpec) {
// 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) {
@ -189,8 +211,27 @@
member.subnet = member.address.subnet;
member.address = member.address.ip;
});
}
// Delete null properties
function cleanFinalSpecMonitor(finalSpec) {
// Monitor requires a pool, interval, retry count, and timeout
if (!finalSpec.pool ||
!angular.isNumber(finalSpec.monitor.interval) ||
!angular.isNumber(finalSpec.monitor.retry) ||
!angular.isNumber(finalSpec.monitor.timeout)) {
delete finalSpec.monitor;
}
// Only include necessary monitor properties
if (finalSpec.monitor && finalSpec.monitor.type !== 'HTTP') {
delete finalSpec.monitor.method;
delete finalSpec.monitor.status;
delete finalSpec.monitor.path;
}
}
function removeNulls(finalSpec) {
angular.forEach(finalSpec, function deleteNullsForGroup(group, groupName) {
angular.forEach(group, function deleteNullValue(value, key) {
if (value === null) {
@ -198,10 +239,6 @@
}
});
});
finalSpec.loadbalancer.subnet = finalSpec.loadbalancer.subnet.id;
return lbaasv2API.createLoadBalancer(finalSpec);
}
function onGetLoadBalancers(response) {

View File

@ -114,10 +114,18 @@
expect(model.listenerProtocols).toEqual(['TCP', 'HTTP', 'HTTPS']);
});
it('has array of methods', function() {
it('has array of pool methods', function() {
expect(model.methods).toEqual(['ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP']);
});
it('has array of monitor types', function() {
expect(model.monitorTypes).toEqual(['HTTP', 'HTTPS', 'PING', 'TCP']);
});
it('has array of monitor methods', function() {
expect(model.monitorMethods).toEqual(['GET', 'HEAD']);
});
it('has an "initialize" function', function() {
expect(model.initialize).toBeDefined();
});
@ -144,6 +152,7 @@
expect(model.spec.listener).toBeDefined();
expect(model.spec.pool).toBeDefined();
expect(model.spec.members).toEqual([]);
expect(model.spec.monitor).toBeDefined();
});
it('should initialize names', function() {
@ -151,6 +160,12 @@
expect(model.spec.listener.name).toBe('Listener 1');
expect(model.spec.pool.name).toBe('Pool 1');
});
it('should initialize monitor fields', function() {
expect(model.spec.monitor.method).toBe('GET');
expect(model.spec.monitor.status).toBe('200');
expect(model.spec.monitor.path).toBe('/');
});
});
describe('Initialization failure', function() {
@ -188,10 +203,11 @@
// 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(4);
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() {
@ -241,6 +257,34 @@
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('Create Load Balancer', function() {
@ -269,6 +313,10 @@
port: 80,
weight: 1
}];
model.spec.monitor.type = 'PING';
model.spec.monitor.interval = 1;
model.spec.monitor.retry = 1;
model.spec.monitor.timeout = 1;
var finalSpec = model.createLoadBalancer();
@ -293,6 +341,10 @@
expect(finalSpec.members[0].id).toBeUndefined();
expect(finalSpec.members[0].name).toBeUndefined();
expect(finalSpec.members[0].description).toBeUndefined();
expect(finalSpec.monitor.type).toBe('PING');
expect(finalSpec.monitor.interval).toBe(1);
expect(finalSpec.monitor.retry).toBe(1);
expect(finalSpec.monitor.timeout).toBe(1);
});
it('should delete listener if any required property is not set', function() {
@ -335,6 +387,26 @@
expect(finalSpec.pool).toBeDefined();
expect(finalSpec.members).toBeUndefined();
});
it('should delete monitor if any required property not set', 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';
model.spec.monitor.type = 'PING';
model.spec.monitor.interval = 1;
model.spec.monitor.retry = 1;
var finalSpec = model.createLoadBalancer();
expect(finalSpec.loadbalancer).toBeDefined();
expect(finalSpec.listener).toBeDefined();
expect(finalSpec.pool).toBeDefined();
expect(finalSpec.members).toBeUndefined();
expect(finalSpec.monitor).toBeUndefined();
});
});
});

View File

@ -0,0 +1,66 @@
/*
* 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('CreateMonitorController', CreateMonitorController);
CreateMonitorController.$inject = [
'horizon.dashboard.project.lbaasv2.patterns',
'horizon.framework.util.i18n.gettext'
];
/**
* @ngdoc controller
* @name CreateMonitorController
* @description
* The `CreateMonitorController` controller provides functions for
* configuring the health monitor step when creating a new health monitor.
* @param patterns The LBaaS v2 patterns constant.
* @param gettext The horizon gettext function for translation.
* @returns undefined
*/
function CreateMonitorController(patterns, gettext) {
var ctrl = this;
// Error text for invalid fields
/* eslint-disable max-len */
ctrl.intervalError = gettext('The health check interval must be greater than or equal to the timeout.');
/* eslint-enable max-len */
ctrl.retryError = gettext('The max retry count must be a number between 1 and 10.');
ctrl.timeoutError = gettext('The timeout must be a number greater than or equal to 0.');
ctrl.statusError = gettext('The expected status code is not valid.');
ctrl.pathError = gettext('The URL path is not valid.');
// Field level help text
ctrl.statusHelp = gettext('Enter comma separated values or ranges.');
ctrl.intervalHelp = gettext('The delay between health check calls.');
ctrl.retryHelp = interpolate(
/* eslint-disable max-len */
gettext('The number of allowed connection failures before changing the status of the member to %(state)s.'),
/* eslint-enable max-len */
{ state: gettext('Inactive') },
true);
// HTTP status codes validation pattern
ctrl.statusPattern = patterns.httpStatusCodes;
ctrl.urlPathPattern = patterns.urlPath;
}
})();

View File

@ -0,0 +1,52 @@
/*
* 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 Monitor Step', function() {
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
describe('CreateMonitorController', function() {
var ctrl;
beforeEach(inject(function($controller) {
ctrl = $controller('CreateMonitorController');
}));
it('should define error messages for invalid fields', function() {
expect(ctrl.intervalError).toBeDefined();
expect(ctrl.retryError).toBeDefined();
expect(ctrl.timeoutError).toBeDefined();
expect(ctrl.statusError).toBeDefined();
expect(ctrl.pathError).toBeDefined();
});
it('should define field level help messages', function() {
expect(ctrl.statusHelp).toBeDefined();
expect(ctrl.intervalHelp).toBeDefined();
expect(ctrl.retryHelp).toBeDefined();
});
it('should define patterns for field validation', function() {
expect(ctrl.statusPattern).toBeDefined();
expect(ctrl.urlPathPattern).toBeDefined();
});
});
});
})();

View File

@ -0,0 +1,3 @@
<h1 translate>Monitor Help</h1>
<p translate>When adding a load balancer, you can also specify a health check monitor to use to determine the health of your instances. Health checks routinely run against each instance within a target load balancer and the result of the health check is used to determine if the instance receives new connections.</p>

View File

@ -0,0 +1,131 @@
<div ng-controller="CreateMonitorController as ctrl">
<h1 translate>Monitor</h1>
<!--content-->
<div class="content">
<div translate class="subtitle">Provide the details for the new health monitor. The monitor will only be created if values are provided for all fields marked as required. A listener and pool are also required.</div>
<div class="row form-group">
<div class="col-sm-6 col-md-3">
<div class="form-field required monitor-type">
<label translate class="on-top" for="monitor-type">Monitor type</label>
<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">
</select>
</div>
</div>
</div>
<div class="row form-group">
<div class="col-sm-6 col-md-3">
<div class="form-field required monitor-interval"
ng-class="{ 'has-error': createLoadBalancerMonitorForm['monitor-interval'].$invalid && createLoadBalancerMonitorForm['monitor-interval'].$dirty }">
<label translate class="on-top" for="monitor-interval">Health check interval (sec)</label>
<span class="fa fa-exclamation-triangle invalid"
ng-show="createLoadBalancerMonitorForm['monitor-interval'].$invalid && createLoadBalancerMonitorForm.$dirty"
popover="{$ ::ctrl.intervalError $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
<span class="fa fa-question-circle pull-right"
popover="{$ ::ctrl.intervalHelp $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
<input name="monitor-interval" id="monitor-interval"
type="number" class="form-control input-sm"
ng-model="model.spec.monitor.interval" ng-pattern="/^\d+$/" ng-min="model.spec.monitor.timeout">
</div>
</div>
<div class="col-sm-6 col-md-3">
<div class="form-field required monitor-retry"
ng-class="{ 'has-error': createLoadBalancerMonitorForm['monitor-retry'].$invalid && createLoadBalancerMonitorForm['monitor-retry'].$dirty }">
<label translate class="on-top" for="monitor-retry">Retry count before markdown</label>
<span class="fa fa-exclamation-triangle invalid"
ng-show="createLoadBalancerMonitorForm['monitor-retry'].$invalid && createLoadBalancerMonitorForm.$dirty"
popover="{$ ::ctrl.retryError $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
<span class="fa fa-question-circle pull-right"
popover="{$ ::ctrl.retryHelp $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
<input name="monitor-retry" id="monitor-retry"
type="number" class="form-control input-sm"
ng-model="model.spec.monitor.retry" ng-pattern="/^\d+$/" min="1" max="10">
</div>
</div>
<div class="col-sm-6 col-md-3">
<div class="form-field required monitor-timeout"
ng-class="{ 'has-error': createLoadBalancerMonitorForm['monitor-timeout'].$invalid && createLoadBalancerMonitorForm['monitor-timeout'].$dirty }">
<label translate class="on-top" for="monitor-timeout">Timeout (sec)</label>
<span class="fa fa-exclamation-triangle invalid"
ng-show="createLoadBalancerMonitorForm['monitor-timeout'].$invalid && createLoadBalancerMonitorForm.$dirty"
popover="{$ ::ctrl.timeoutError $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
<input name="monitor-timeout" id="monitor-timeout"
type="number" class="form-control input-sm"
ng-model="model.spec.monitor.timeout" ng-pattern="/^\d+$/" min="0">
</div>
</div>
</div>
<div class="row form-group" ng-if="model.spec.monitor.type === 'HTTP'">
<div class="col-sm-6 col-md-3">
<div class="form-field monitor-method">
<label translate class="on-top" for="monitor-method">HTTP method</label>
<select class="form-control input-sm" name="monitor-method"
id="monitor-method"
ng-options="method for method in model.monitorMethods"
ng-model="model.spec.monitor.method">
</select>
</div>
</div>
<div class="col-sm-6 col-md-3">
<div class="form-field monitor-status"
ng-class="{ 'has-error': createLoadBalancerMonitorForm['monitor-status'].$invalid && createLoadBalancerMonitorForm['monitor-status'].$dirty }">
<label translate class="on-top" for="monitor-status">Expected HTTP status code</label>
<span class="fa fa-exclamation-triangle invalid"
ng-show="createLoadBalancerMonitorForm['monitor-status'].$invalid && createLoadBalancerMonitorForm.$dirty"
popover="{$ ::ctrl.statusError $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
<span class="fa fa-question-circle pull-right"
popover="{$ ::ctrl.statusHelp $}"
popover-placement="top" popover-append-to-body="true"
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">
</div>
</div>
<div class="col-sm-6 col-md-3">
<div class="form-field monitor-path"
ng-class="{ 'has-error': createLoadBalancerMonitorForm['monitor-path'].$invalid && createLoadBalancerMonitorForm['monitor-path'].$dirty }">
<label translate class="on-top" for="monitor-path">URL path</label>
<span class="fa fa-exclamation-triangle invalid"
ng-show="createLoadBalancerMonitorForm['monitor-path'].$invalid && createLoadBalancerMonitorForm.$dirty"
popover="{$ ::ctrl.pathError $}"
popover-placement="top" popover-append-to-body="true"
popover-trigger="hover"></span>
<input name="monitor-path" id="monitor-path"
type="text" class="form-control input-sm"
ng-model="model.spec.monitor.path" ng-pattern="::ctrl.urlPathPattern">
</div>
</div>
</div>
</div>
<!-- end content -->
</div>

View File

@ -59,6 +59,13 @@
templateUrl: basePath + 'loadbalancers/actions/create/members/members.html',
helpUrl: basePath + 'loadbalancers/actions/create/members/members.help.html',
formName: 'createLoadBalancerMembersForm'
},
{
id: 'monitor',
title: gettext('Monitor'),
templateUrl: basePath + 'loadbalancers/actions/create/monitor/monitor.html',
helpUrl: basePath + 'loadbalancers/actions/create/monitor/monitor.help.html',
formName: 'createLoadBalancerMonitorForm'
}
],

View File

@ -41,13 +41,14 @@
it('should have steps defined', function () {
expect(createLoadBalancerWorkflow.steps).toBeDefined();
expect(createLoadBalancerWorkflow.steps.length).toBe(4);
expect(createLoadBalancerWorkflow.steps.length).toBe(5);
var forms = [
'createLoadBalancerDetailsForm',
'createLoadBalancerListenerForm',
'createLoadBalancerPoolForm',
'createLoadBalancerMembersForm'
'createLoadBalancerMembersForm',
'createLoadBalancerMonitorForm'
];
forms.forEach(function(expectedForm, idx) {

View File

@ -18,7 +18,7 @@
Table-batch-actions:
This is where batch actions like searching, creating, and deleting.
-->
<th colspan="0" class="search-header">
<th colspan="100" class="search-header">
<hz-search-bar group-classes="input-group-sm" icon-classes="fa-search">
<actions allowed="table.batchActions.actions" type="batch"></actions>
</hz-search-bar>
@ -86,7 +86,7 @@
Can be toggled using the chevron button.
Ensure colspan is greater or equal to number of column-headers.
-->
<td class="detail" colspan="0">
<td class="detail" colspan="100">
<!--
The responsive columns that disappear typically should reappear here
with the same responsive priority that they disappear.