Implements a filtered select
This patch implements a custom select in the load balancer creation/edit form with the following features: - The options are presented in a tabular form with: network name, network id, subnet name, subnet id - An input text filter which filters across all fields The select is implemented as a customizable AngularJS component, which allows for any of the displayed information to be changed easily. Change-Id: I6ff16cb8ffd0ebdb8c465e5197f90ba2939a28c1 Story: 2004347 Task: 27943
This commit is contained in:
parent
cdf26e99c8
commit
35f6f90d0c
3
.gitignore
vendored
3
.gitignore
vendored
@ -63,5 +63,8 @@ ChangeLog
|
|||||||
.ropeproject/
|
.ropeproject/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# IntelliJ editors
|
||||||
|
.idea
|
||||||
|
|
||||||
# Conf
|
# Conf
|
||||||
octavia_dashboard/conf
|
octavia_dashboard/conf
|
||||||
|
@ -92,3 +92,35 @@
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Filtering select widget */
|
||||||
|
.filter-select-options {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: $dropdown-bg;
|
||||||
|
min-width: 100%;
|
||||||
|
|
||||||
|
thead {
|
||||||
|
th {
|
||||||
|
color: $dropdown-header-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
tr:hover {
|
||||||
|
color: $dropdown-link-hover-color;
|
||||||
|
background-color: $dropdown-link-hover-bg;
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
color:$dropdown-link-color;
|
||||||
|
.highlighted {
|
||||||
|
background-color: darken($dropdown-link-hover-bg, 15%);
|
||||||
|
}
|
||||||
|
.empty-options {
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,266 @@
|
|||||||
|
/*
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc component
|
||||||
|
* @ngname horizon.dashboard.project.lbaasv2:filterSelect
|
||||||
|
*
|
||||||
|
* @param {function} onSelect callback invoked when a selection is made,
|
||||||
|
* receives the selected option as a parameter (required)
|
||||||
|
* @param {object} ng-model the currently selected option. Uses the ng-model
|
||||||
|
* directive to tie into angularjs validations (required)
|
||||||
|
* @param {function} shorthand a function used to create a summarizing text
|
||||||
|
* for an option object passed to it as the first parameter. This text is
|
||||||
|
* displayed in the filter input when an option is selected. (required)
|
||||||
|
* @param {boolean} disabled boolean value controlling the disabled state
|
||||||
|
* of the component (optional, defaults to false)
|
||||||
|
* @param {array} options a collection of objects to be presented for
|
||||||
|
* selection (required)
|
||||||
|
* @param {array} columns array of column defining objects. (required,
|
||||||
|
* see below for details)
|
||||||
|
* @param {boolean} loaded allows the control to be replaced by a loading bar
|
||||||
|
* if required (such as when waiting for data to be loaded) (optional,
|
||||||
|
* defaults to false)
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* The filter-select component serves as a more complicated alternative to
|
||||||
|
* the standard select control.
|
||||||
|
*
|
||||||
|
* Options in this component are presented as a customizable table where
|
||||||
|
* each row corresponds to one of the options and allows for the presented
|
||||||
|
* options to be filtered using a text input.
|
||||||
|
*
|
||||||
|
* Columns of the table are defined through the `column` attribute, which
|
||||||
|
* accepts an array of column definition objects. Each object contains two
|
||||||
|
* properties: `label` and `value`.
|
||||||
|
*
|
||||||
|
* * label {string} specifies a text value used as the given columns header
|
||||||
|
*
|
||||||
|
* The displayed text in each column for every option is created by
|
||||||
|
* applying the `value` property of the given column definition to the
|
||||||
|
* option object. It can be of two types with different behaviors:
|
||||||
|
*
|
||||||
|
* * {string} describes the value as a direct property of option objects,
|
||||||
|
* using it as key into the option object
|
||||||
|
*
|
||||||
|
* * {function} defines a callback that is expected to return the desired
|
||||||
|
* text and receives the option as it's parameter
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* $scope.options = [{
|
||||||
|
* text: "option 1",
|
||||||
|
* number: 1
|
||||||
|
* }, {
|
||||||
|
* text: "option 2",
|
||||||
|
* number: 2
|
||||||
|
* }]
|
||||||
|
* $scope.onSelect = function(option) { scope.value = option; };
|
||||||
|
* $scope.columns = [{
|
||||||
|
* label: "Column 1",
|
||||||
|
* value: "text"
|
||||||
|
* }, {
|
||||||
|
* label: "Column 2",
|
||||||
|
* value: function(option) { return option['number']; };
|
||||||
|
* }];
|
||||||
|
* $scope.shorthand = function(option) {
|
||||||
|
* return option['text'] + " => " + option['number'];
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* <filter-select
|
||||||
|
* onSelect="onSelect"
|
||||||
|
* options="options"
|
||||||
|
* columns="columns"
|
||||||
|
* shorthand="shorthand">
|
||||||
|
* </filter-select>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The rendered table would then look as follows:
|
||||||
|
*
|
||||||
|
* | Column 1 | Column 2 |
|
||||||
|
* |----------|----------|
|
||||||
|
* | Option 1 | 1 |
|
||||||
|
* |----------|----------|
|
||||||
|
* | Option 2 | 2 |
|
||||||
|
*
|
||||||
|
* If the first option is selected, the shorthand function is invoked and
|
||||||
|
* the following is displayed in the input box: 'Option1 => 1'
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
angular
|
||||||
|
.module('horizon.dashboard.project.lbaasv2')
|
||||||
|
.component('filterSelect', {
|
||||||
|
templateUrl: getTemplate,
|
||||||
|
controller: filterSelectController,
|
||||||
|
require: {
|
||||||
|
ngModelCtrl: "ngModel"
|
||||||
|
},
|
||||||
|
bindings: {
|
||||||
|
onSelect: '&',
|
||||||
|
shorthand: '<',
|
||||||
|
columns: '<',
|
||||||
|
options: '<',
|
||||||
|
disabled: '<',
|
||||||
|
loaded: '<',
|
||||||
|
ngModel: '<'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
filterSelectController.$inject = ['$document', '$scope', '$element'];
|
||||||
|
|
||||||
|
function filterSelectController($document, $scope, $element) {
|
||||||
|
var ctrl = this;
|
||||||
|
ctrl._scope = $scope;
|
||||||
|
|
||||||
|
// Used to filter rows
|
||||||
|
ctrl.textFilter = '';
|
||||||
|
// Model for the filtering text input
|
||||||
|
ctrl.text = '';
|
||||||
|
// Model for the dropdown
|
||||||
|
ctrl.isOpen = false;
|
||||||
|
// Arrays of text to be displayed
|
||||||
|
ctrl.rows = [];
|
||||||
|
|
||||||
|
// Lifecycle methods
|
||||||
|
ctrl.$onInit = function() {
|
||||||
|
$document.on('click', ctrl.externalClick);
|
||||||
|
ctrl.loaded = ctrl._setValue(ctrl.loaded, true);
|
||||||
|
ctrl.disabled = ctrl._setValue(ctrl.disabled, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.$onDestroy = function() {
|
||||||
|
$document.off('click', ctrl.externalClick);
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.$onChanges = function(changes) {
|
||||||
|
if (changes.ngModel && ctrl.options) {
|
||||||
|
var i = ctrl.options.indexOf(ctrl.ngModel);
|
||||||
|
if (i > -1) {
|
||||||
|
ctrl.textFilter = '';
|
||||||
|
ctrl.text = ctrl.shorthand(ctrl.ngModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctrl._buildRows();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handles clicking outside of the comopnent
|
||||||
|
ctrl.externalClick = function(event) {
|
||||||
|
if (!$element.find(event.target).length) {
|
||||||
|
ctrl._setOpenExternal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Template handleres
|
||||||
|
ctrl.onTextChange = function() {
|
||||||
|
ctrl.onSelect({
|
||||||
|
option: null
|
||||||
|
});
|
||||||
|
ctrl.textFilter = ctrl.text;
|
||||||
|
//ctrl._rebuildRows();
|
||||||
|
ctrl._buildRows();
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.togglePopup = function() {
|
||||||
|
ctrl.isOpen = !ctrl.isOpen;
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.openPopup = function(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
ctrl.isOpen = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.selectOption = function(index) {
|
||||||
|
var option = ctrl.options[index];
|
||||||
|
ctrl.onSelect({
|
||||||
|
option: option
|
||||||
|
});
|
||||||
|
ctrl.isOpen = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Internal/Helper methods
|
||||||
|
ctrl._buildCell = function(column, option) {
|
||||||
|
if (angular.isFunction(column.value)) {
|
||||||
|
return column.value(option);
|
||||||
|
} else {
|
||||||
|
return option[column.value];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl._buildRow = function(option) {
|
||||||
|
var row = [];
|
||||||
|
var valid = false;
|
||||||
|
angular.forEach(ctrl.columns, function(column) {
|
||||||
|
var cell = ctrl._buildCell(column, option);
|
||||||
|
var split = ctrl._splitByFilter(cell);
|
||||||
|
valid = valid || split.wasSplit;
|
||||||
|
row.push(split.values);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (valid || !ctrl.textFilter) {
|
||||||
|
return row;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl._buildRows = function() {
|
||||||
|
ctrl.rows.length = 0;
|
||||||
|
angular.forEach(ctrl.options, function(option) {
|
||||||
|
var row = ctrl._buildRow(option);
|
||||||
|
if (row) {
|
||||||
|
ctrl.rows.push(row);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl._splitByFilter = function(text) {
|
||||||
|
var split = {
|
||||||
|
values: [text, "", ""],
|
||||||
|
wasSplit: false
|
||||||
|
};
|
||||||
|
var i;
|
||||||
|
if (ctrl.textFilter && (i = text.indexOf(ctrl.textFilter)) > -1) {
|
||||||
|
split.values = [
|
||||||
|
text.substring(0, i),
|
||||||
|
ctrl.textFilter,
|
||||||
|
text.substring(i + ctrl.textFilter.length)
|
||||||
|
];
|
||||||
|
split.wasSplit = true;
|
||||||
|
}
|
||||||
|
return split;
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl._setOpenExternal = function(value) {
|
||||||
|
ctrl.isOpen = value;
|
||||||
|
$scope.$apply();
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl._isUnset = function(property) {
|
||||||
|
return angular.isUndefined(property) || property === null;
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl._setValue = function(property, defaultValue) {
|
||||||
|
return ctrl._isUnset(property) ? defaultValue : property;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getTemplate.$inject = ['horizon.dashboard.project.lbaasv2.basePath'];
|
||||||
|
|
||||||
|
function getTemplate(basePath) {
|
||||||
|
return basePath + 'widgets/filterselect/filter-select.html';
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,293 @@
|
|||||||
|
/*
|
||||||
|
* 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('Filter-Select', function() {
|
||||||
|
var mockOptions, mockColumns;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
mockOptions = [{
|
||||||
|
text: 'Option 1'
|
||||||
|
}, {
|
||||||
|
text: 'Option 2'
|
||||||
|
}];
|
||||||
|
|
||||||
|
mockColumns = [{
|
||||||
|
label: 'Key column',
|
||||||
|
value: 'text'
|
||||||
|
}, {
|
||||||
|
label: 'Function Column',
|
||||||
|
value: function(option) {
|
||||||
|
return option.text + ' extended';
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('component', function() {
|
||||||
|
var component, ctrl, child, scope, otherElement,
|
||||||
|
filterSelect, element;
|
||||||
|
|
||||||
|
beforeEach(module('templates'));
|
||||||
|
beforeEach(module('horizon.dashboard.project.lbaasv2',
|
||||||
|
function($provide) {
|
||||||
|
$provide.decorator('filterSelectDirective', function($delegate) {
|
||||||
|
component = $delegate[0];
|
||||||
|
spyOn(component, 'templateUrl').and.callThrough();
|
||||||
|
return $delegate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
beforeEach(inject(function($compile, $rootScope) {
|
||||||
|
scope = $rootScope.$new();
|
||||||
|
scope.ngModel = null;
|
||||||
|
scope.onSelect = function() {};
|
||||||
|
scope.shorthand = function(option) {
|
||||||
|
return 'Shorthand: ' + option.text;
|
||||||
|
};
|
||||||
|
scope.disabled = true;
|
||||||
|
scope.columns = mockColumns;
|
||||||
|
scope.options = mockOptions;
|
||||||
|
|
||||||
|
var html = '<filter-select ' +
|
||||||
|
'onSelect="onSelect" ' +
|
||||||
|
'ng-model="ngModel" ' +
|
||||||
|
'shorthand="shorthand" ' +
|
||||||
|
'disabled="disabled" ' +
|
||||||
|
'columns="columns" ' +
|
||||||
|
'options="options" ' +
|
||||||
|
'></filter-select>';
|
||||||
|
|
||||||
|
var parentElement = angular.element('<div></div>');
|
||||||
|
otherElement = angular.element('<div id="otherElement"></div>');
|
||||||
|
filterSelect = angular.element(html);
|
||||||
|
|
||||||
|
parentElement.append(otherElement);
|
||||||
|
parentElement.append(filterSelect);
|
||||||
|
|
||||||
|
element = $compile(parentElement)(scope);
|
||||||
|
scope.$apply();
|
||||||
|
|
||||||
|
child = element.find('input');
|
||||||
|
ctrl = filterSelect.controller('filter-select');
|
||||||
|
|
||||||
|
spyOn(ctrl, 'onSelect').and.callThrough();
|
||||||
|
spyOn(ctrl, '_buildRows').and.callThrough();
|
||||||
|
spyOn(ctrl, 'shorthand').and.callThrough();
|
||||||
|
spyOn(ctrl, '_setOpenExternal').and.callThrough();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should load the correct template', function() {
|
||||||
|
expect(component.templateUrl).toHaveBeenCalled();
|
||||||
|
expect(component.templateUrl()).toBe(
|
||||||
|
'/static/dashboard/project/lbaasv2/' +
|
||||||
|
'widgets/filterselect/filter-select.html'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should react to value change', function() {
|
||||||
|
// Change one way binding for 'value'
|
||||||
|
scope.ngModel = mockOptions[0];
|
||||||
|
scope.$apply();
|
||||||
|
|
||||||
|
expect(ctrl.textFilter).toBe('');
|
||||||
|
expect(ctrl.text).toBe('Shorthand: Option 1');
|
||||||
|
expect(ctrl._buildRows).toHaveBeenCalled();
|
||||||
|
expect(ctrl.shorthand).toHaveBeenCalledWith(mockOptions[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should react to non-option value', function() {
|
||||||
|
// Set one way binding to an impossible value
|
||||||
|
var nonOption = {};
|
||||||
|
scope.ngModel = nonOption;
|
||||||
|
scope.$apply();
|
||||||
|
|
||||||
|
expect(ctrl._buildRows).toHaveBeenCalled();
|
||||||
|
expect(ctrl.shorthand).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should react to non-value change', function() {
|
||||||
|
// Set non-value binding and trigger onChange, make sure value related
|
||||||
|
// changes aren't triggered
|
||||||
|
scope.disabled = false;
|
||||||
|
scope.$apply();
|
||||||
|
|
||||||
|
expect(ctrl._buildRows).toHaveBeenCalled();
|
||||||
|
expect(ctrl.shorthand).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should react to outside clicks', function() {
|
||||||
|
var mockChildEvent = {
|
||||||
|
target: child
|
||||||
|
};
|
||||||
|
ctrl.externalClick(mockChildEvent);
|
||||||
|
expect(ctrl._setOpenExternal).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
var mockOutsideEvent = {
|
||||||
|
target: otherElement
|
||||||
|
};
|
||||||
|
ctrl.externalClick(mockOutsideEvent);
|
||||||
|
expect(ctrl._setOpenExternal).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build rows', function() {
|
||||||
|
var expectedRows = [
|
||||||
|
[['Option 1', '', ''], ['Option 1 extended', '', '']],
|
||||||
|
[['Option 2', '', ''], ['Option 2 extended', '', '']]
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(ctrl.rows).toEqual(expectedRows);
|
||||||
|
|
||||||
|
// filtered by text
|
||||||
|
ctrl.textFilter = '1';
|
||||||
|
ctrl._buildRows();
|
||||||
|
|
||||||
|
var expectedFiltered = [
|
||||||
|
[['Option ', '1', ''], ['Option ', '1', ' extended']]
|
||||||
|
];
|
||||||
|
expect(ctrl.rows).toEqual(expectedFiltered);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build cells', function() {
|
||||||
|
// Test that normal string values are used as keys against options
|
||||||
|
var option1text = ctrl._buildCell(ctrl.columns[0], ctrl.options[0]);
|
||||||
|
expect(option1text).toBe('Option 1');
|
||||||
|
|
||||||
|
// Test that column value callbacks are called
|
||||||
|
spyOn(ctrl.columns[1], 'value');
|
||||||
|
ctrl._buildCell(ctrl.columns[1], ctrl.options[0]);
|
||||||
|
expect(ctrl.columns[1].value).toHaveBeenCalledWith(ctrl.options[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle text changes', function() {
|
||||||
|
// Test input text changes
|
||||||
|
var mockInput = 'mock input text';
|
||||||
|
ctrl.text = mockInput;
|
||||||
|
ctrl.onTextChange();
|
||||||
|
|
||||||
|
expect(ctrl.textFilter).toEqual(mockInput);
|
||||||
|
expect(ctrl._buildRows).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select options', function() {
|
||||||
|
ctrl.selectOption(1);
|
||||||
|
expect(ctrl.onSelect).toHaveBeenCalledWith({
|
||||||
|
option: mockOptions[1]
|
||||||
|
});
|
||||||
|
expect(ctrl.isOpen).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('controller', function() {
|
||||||
|
var scope, ctrl;
|
||||||
|
|
||||||
|
beforeEach(module('horizon.dashboard.project.lbaasv2'));
|
||||||
|
beforeEach(
|
||||||
|
inject(
|
||||||
|
function($rootScope, $componentController) {
|
||||||
|
scope = $rootScope.$new();
|
||||||
|
ctrl = $componentController('filterSelect', {
|
||||||
|
$scope: scope,
|
||||||
|
$element: angular.element('<span></span>')
|
||||||
|
});
|
||||||
|
ctrl.$onInit();
|
||||||
|
|
||||||
|
spyOn(scope, '$apply');
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should initialize and remove listeners', function() {
|
||||||
|
var events = $._data(document, 'events');
|
||||||
|
expect(events.click).toBeDefined();
|
||||||
|
expect(events.click.length).toBe(1);
|
||||||
|
expect(events.click[0].handler).toBe(ctrl.externalClick);
|
||||||
|
|
||||||
|
ctrl.$onDestroy();
|
||||||
|
expect(events.click).not.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize state', function() {
|
||||||
|
// Initial component state; simply bound values needn't be checked,
|
||||||
|
// angular binding is trusted
|
||||||
|
expect(ctrl.textFilter).toBe('');
|
||||||
|
expect(ctrl.text).toBe('');
|
||||||
|
expect(ctrl.isOpen).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open popup', function() {
|
||||||
|
var mockEvent = {
|
||||||
|
stopPropagation: function() {}
|
||||||
|
};
|
||||||
|
spyOn(mockEvent, 'stopPropagation');
|
||||||
|
|
||||||
|
ctrl.openPopup(mockEvent);
|
||||||
|
expect(mockEvent.stopPropagation).toHaveBeenCalled();
|
||||||
|
expect(ctrl.isOpen).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle popup', function() {
|
||||||
|
// not much to tests here; utilizes bootstrap dropdown
|
||||||
|
ctrl.togglePopup();
|
||||||
|
expect(ctrl.isOpen).toBe(true);
|
||||||
|
ctrl.togglePopup();
|
||||||
|
expect(ctrl.isOpen).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set open state from outside the digest', function() {
|
||||||
|
ctrl._setOpenExternal(true);
|
||||||
|
expect(ctrl.isOpen).toBe(true);
|
||||||
|
expect(scope.$apply).toHaveBeenCalled();
|
||||||
|
|
||||||
|
ctrl._setOpenExternal(false);
|
||||||
|
expect(ctrl.isOpen).toBe(false);
|
||||||
|
expect(scope.$apply).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check unset values', function() {
|
||||||
|
expect(ctrl._isUnset(null)).toBe(true);
|
||||||
|
expect(ctrl._isUnset(undefined)).toBe(true);
|
||||||
|
expect(ctrl._isUnset('defined_value')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set default values correctly', function() {
|
||||||
|
var defaultValue = 'default value';
|
||||||
|
var realValue = 'input value';
|
||||||
|
|
||||||
|
var firstResult = ctrl._setValue(null, defaultValue);
|
||||||
|
expect(firstResult).toBe(defaultValue);
|
||||||
|
|
||||||
|
var secondResult = ctrl._setValue(realValue, defaultValue);
|
||||||
|
expect(secondResult).toBe(realValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should split by filter', function() {
|
||||||
|
ctrl.textFilter = 'matched';
|
||||||
|
|
||||||
|
var notSplit = ctrl._splitByFilter('does not match');
|
||||||
|
expect(notSplit).toEqual({
|
||||||
|
values:['does not match', '', ''],
|
||||||
|
wasSplit: false
|
||||||
|
});
|
||||||
|
|
||||||
|
var split = ctrl._splitByFilter('this matched portion');
|
||||||
|
expect(split).toEqual({
|
||||||
|
values: ['this ', 'matched', ' portion'],
|
||||||
|
wasSplit: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
@ -0,0 +1,43 @@
|
|||||||
|
<div class="horizon-loading-bar" ng-if="!$ctrl.loaded">
|
||||||
|
<div class="progress progress-striped active">
|
||||||
|
<div class="progress-bar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div uib-dropdown is-open="$ctrl.isOpen" auto-close="disabled" ng-if="$ctrl.loaded">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" ng-model="$ctrl.text" ng-change="$ctrl.onTextChange()"
|
||||||
|
class="form-control" ng-focus="$ctrl.openPopup($event)"
|
||||||
|
ng-disabled="$ctrl.disabled">
|
||||||
|
<div class="input-group-btn">
|
||||||
|
<button type="button" class="btn btn-default dropdown-toggle"
|
||||||
|
ng-click="$ctrl.togglePopup()" ng-disabled="$ctrl.disabled">
|
||||||
|
<span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div uib-dropdown-menu ng-class="'filter-select-options'">
|
||||||
|
<table class="table" ng-if="$ctrl.loaded">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th ng-repeat="column in $ctrl.columns track by $index" scope="col">
|
||||||
|
{$ column.label $}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="row in $ctrl.rows track by $index"
|
||||||
|
ng-if="$ctrl.rows.length > 0"
|
||||||
|
ng-click="$ctrl.selectOption($index)">
|
||||||
|
<td ng-repeat="column in row track by $index">
|
||||||
|
{$ column[0] $}<span ng-class="'highlighted'">{$ column[1] $}</span>{$ column[2] $}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="$ctrl.rows.length <= 0">
|
||||||
|
<td colspan="{$ $ctrl.columns.length $}" ng-class="'empty-options'">
|
||||||
|
<translate>No matching options</translate>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -13,7 +13,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular
|
angular
|
||||||
@ -22,7 +22,8 @@
|
|||||||
|
|
||||||
LoadBalancerDetailsController.$inject = [
|
LoadBalancerDetailsController.$inject = [
|
||||||
'horizon.dashboard.project.lbaasv2.patterns',
|
'horizon.dashboard.project.lbaasv2.patterns',
|
||||||
'horizon.framework.util.i18n.gettext'
|
'horizon.framework.util.i18n.gettext',
|
||||||
|
'$scope'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,13 +32,12 @@
|
|||||||
* @description
|
* @description
|
||||||
* The `LoadBalancerDetailsController` controller provides functions for
|
* The `LoadBalancerDetailsController` controller provides functions for
|
||||||
* configuring the load balancers step of the LBaaS wizard.
|
* configuring the load balancers step of the LBaaS wizard.
|
||||||
* @param patterns The LBaaS v2 patterns constant.
|
* @param {object} patterns The LBaaS v2 patterns constant.
|
||||||
* @param gettext The horizon gettext function for translation.
|
* @param {function} gettext The horizon gettext function for translation.
|
||||||
* @returns undefined
|
* @param {object} $scope Allows access to the model
|
||||||
|
* @returns {undefined} undefined
|
||||||
*/
|
*/
|
||||||
|
function LoadBalancerDetailsController(patterns, gettext, $scope) {
|
||||||
function LoadBalancerDetailsController(patterns, gettext) {
|
|
||||||
|
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
|
||||||
// Error text for invalid fields
|
// Error text for invalid fields
|
||||||
@ -45,5 +45,81 @@
|
|||||||
|
|
||||||
// IP address validation pattern
|
// IP address validation pattern
|
||||||
ctrl.ipPattern = [patterns.ipv4, patterns.ipv6].join('|');
|
ctrl.ipPattern = [patterns.ipv4, patterns.ipv6].join('|');
|
||||||
|
|
||||||
|
// Defines columns for the subnet selection filtered pop-up
|
||||||
|
ctrl.subnetColumns = [{
|
||||||
|
label: gettext('Network'),
|
||||||
|
value: function(subnet) {
|
||||||
|
var network = $scope.model.networks[subnet.network_id];
|
||||||
|
return network ? network.name : '';
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: gettext('Network ID'),
|
||||||
|
value: 'network_id'
|
||||||
|
}, {
|
||||||
|
label: gettext('Subnet'),
|
||||||
|
value: 'name'
|
||||||
|
}, {
|
||||||
|
label: gettext('Subnet ID'),
|
||||||
|
value: 'id'
|
||||||
|
}, {
|
||||||
|
label: gettext('CIDR'),
|
||||||
|
value: 'cidr'
|
||||||
|
}];
|
||||||
|
|
||||||
|
ctrl.subnetOptions = [];
|
||||||
|
|
||||||
|
ctrl.shorthand = function(subnet) {
|
||||||
|
var network = $scope.model.networks[subnet.network_id];
|
||||||
|
|
||||||
|
var networkText = network ? network.name : subnet.network_id.substring(0, 10) + '...';
|
||||||
|
var cidrText = subnet.cidr;
|
||||||
|
var subnetText = subnet.name || subnet.id.substring(0, 10) + '...';
|
||||||
|
|
||||||
|
return networkText + ': ' + cidrText + ' (' + subnetText + ')';
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.setSubnet = function(option) {
|
||||||
|
if (option) {
|
||||||
|
$scope.model.spec.loadbalancer.vip_subnet_id = option;
|
||||||
|
} else {
|
||||||
|
$scope.model.spec.loadbalancer.vip_subnet_id = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.dataLoaded = false;
|
||||||
|
ctrl._checkLoaded = function() {
|
||||||
|
if ($scope.model.initialized) {
|
||||||
|
ctrl.buildSubnetOptions();
|
||||||
|
ctrl.dataLoaded = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
The watchers in this component are a bit of a workaround for the way
|
||||||
|
data is loaded asynchornously in the model service. First data loads
|
||||||
|
are marked by a change of 'model.initialized' from false to true, which
|
||||||
|
should replace the striped loading bar with a functional dropdown.
|
||||||
|
|
||||||
|
Additional changes to networks and subnets have to be watched even after
|
||||||
|
first loads, however, as those changes need to be applied to the select
|
||||||
|
options
|
||||||
|
*/
|
||||||
|
ctrl.$onInit = function() {
|
||||||
|
$scope.$watchCollection('model.subnets', function() {
|
||||||
|
ctrl._checkLoaded();
|
||||||
|
});
|
||||||
|
$scope.$watchCollection('model.networks', function() {
|
||||||
|
ctrl._checkLoaded();
|
||||||
|
});
|
||||||
|
$scope.$watch('model.initialized', function() {
|
||||||
|
ctrl._checkLoaded();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.buildSubnetOptions = function() {
|
||||||
|
// Subnets are sliced to maintain data immutability
|
||||||
|
ctrl.subnetOptions = $scope.model.subnets.slice(0);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -22,10 +22,46 @@
|
|||||||
beforeEach(module('horizon.dashboard.project.lbaasv2'));
|
beforeEach(module('horizon.dashboard.project.lbaasv2'));
|
||||||
|
|
||||||
describe('LoadBalancerDetailsController', function() {
|
describe('LoadBalancerDetailsController', function() {
|
||||||
var ctrl;
|
var ctrl, scope, mockSubnets;
|
||||||
|
beforeEach(inject(function($controller, $rootScope) {
|
||||||
|
mockSubnets = [{
|
||||||
|
id: '7262744a-e1e4-40d7-8833-18193e8de191',
|
||||||
|
network_id: '5d658cef-3402-4474-bb8a-0c1162efd9a9',
|
||||||
|
name: 'subnet_1',
|
||||||
|
cidr: '1.1.1.1/24'
|
||||||
|
}, {
|
||||||
|
id: 'd8056c7e-c810-4ee5-978e-177cb4154d81',
|
||||||
|
network_id: '12345678-0000-0000-0000-000000000000',
|
||||||
|
name: 'subnet_2',
|
||||||
|
cidr: '2.2.2.2/16'
|
||||||
|
}, {
|
||||||
|
id: 'd8056c7e-c810-4ee5-978e-177cb4154d81',
|
||||||
|
network_id: '12345678-0000-0000-0000-000000000000',
|
||||||
|
name: '',
|
||||||
|
cidr: '2.2.2.2/16'
|
||||||
|
}];
|
||||||
|
|
||||||
beforeEach(inject(function($controller) {
|
scope = $rootScope.$new();
|
||||||
ctrl = $controller('LoadBalancerDetailsController');
|
scope.model = {
|
||||||
|
networks: {
|
||||||
|
'5d658cef-3402-4474-bb8a-0c1162efd9a9': {
|
||||||
|
id: '5d658cef-3402-4474-bb8a-0c1162efd9a9',
|
||||||
|
name: 'network_1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
subnets: [{}, {}],
|
||||||
|
spec: {
|
||||||
|
loadbalancer: {
|
||||||
|
vip_subnet_id: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initialized: false
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl = $controller('LoadBalancerDetailsController', {$scope: scope});
|
||||||
|
|
||||||
|
spyOn(ctrl, 'buildSubnetOptions').and.callThrough();
|
||||||
|
spyOn(ctrl, '_checkLoaded').and.callThrough();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should define error messages for invalid fields', function() {
|
it('should define error messages for invalid fields', function() {
|
||||||
@ -36,6 +72,92 @@
|
|||||||
expect(ctrl.ipPattern).toBeDefined();
|
expect(ctrl.ipPattern).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create shorthand text', function() {
|
||||||
|
// Full values
|
||||||
|
expect(ctrl.shorthand(mockSubnets[0])).toBe(
|
||||||
|
'network_1: 1.1.1.1/24 (subnet_1)'
|
||||||
|
);
|
||||||
|
// No network name
|
||||||
|
expect(ctrl.shorthand(mockSubnets[1])).toBe(
|
||||||
|
'12345678-0...: 2.2.2.2/16 (subnet_2)'
|
||||||
|
);
|
||||||
|
// No network and subnet names
|
||||||
|
expect(ctrl.shorthand(mockSubnets[2])).toBe(
|
||||||
|
'12345678-0...: 2.2.2.2/16 (d8056c7e-c...)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set subnet', function() {
|
||||||
|
ctrl.setSubnet(mockSubnets[0]);
|
||||||
|
expect(scope.model.spec.loadbalancer.vip_subnet_id).toBe(mockSubnets[0]);
|
||||||
|
ctrl.setSubnet(null);
|
||||||
|
expect(scope.model.spec.loadbalancer.vip_subnet_id).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize watchers', function() {
|
||||||
|
ctrl.$onInit();
|
||||||
|
|
||||||
|
scope.model.subnets = [];
|
||||||
|
scope.$apply();
|
||||||
|
expect(ctrl._checkLoaded).toHaveBeenCalled();
|
||||||
|
|
||||||
|
scope.model.networks = {};
|
||||||
|
scope.$apply();
|
||||||
|
expect(ctrl._checkLoaded).toHaveBeenCalled();
|
||||||
|
|
||||||
|
scope.model.initialized = true;
|
||||||
|
|
||||||
|
scope.$apply();
|
||||||
|
expect(ctrl._checkLoaded).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize networks watcher', function() {
|
||||||
|
ctrl.$onInit();
|
||||||
|
|
||||||
|
scope.model.networks = {};
|
||||||
|
scope.$apply();
|
||||||
|
//expect(ctrl.buildSubnetOptions).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build subnetOptions', function() {
|
||||||
|
ctrl.buildSubnetOptions();
|
||||||
|
|
||||||
|
expect(ctrl.subnetOptions).not.toBe(scope.model.subnets);
|
||||||
|
expect(ctrl.subnetOptions).toEqual(scope.model.subnets);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should produce column data', function() {
|
||||||
|
expect(ctrl.subnetColumns).toBeDefined();
|
||||||
|
|
||||||
|
var networkLabel1 = ctrl.subnetColumns[0].value(mockSubnets[0]);
|
||||||
|
expect(networkLabel1).toBe('network_1');
|
||||||
|
|
||||||
|
var networkLabel2 = ctrl.subnetColumns[0].value(mockSubnets[1]);
|
||||||
|
expect(networkLabel2).toBe('');
|
||||||
|
|
||||||
|
expect(ctrl.subnetColumns[1].label).toBe('Network ID');
|
||||||
|
expect(ctrl.subnetColumns[1].value).toBe('network_id');
|
||||||
|
|
||||||
|
expect(ctrl.subnetColumns[2].label).toBe('Subnet');
|
||||||
|
expect(ctrl.subnetColumns[2].value).toBe('name');
|
||||||
|
|
||||||
|
expect(ctrl.subnetColumns[3].label).toBe('Subnet ID');
|
||||||
|
expect(ctrl.subnetColumns[3].value).toBe('id');
|
||||||
|
|
||||||
|
expect(ctrl.subnetColumns[4].label).toBe('CIDR');
|
||||||
|
expect(ctrl.subnetColumns[4].value).toBe('cidr');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should react to data being loaded', function() {
|
||||||
|
ctrl._checkLoaded();
|
||||||
|
expect(ctrl.buildSubnetOptions).not.toHaveBeenCalled();
|
||||||
|
expect(ctrl.dataLoaded).toBe(false);
|
||||||
|
|
||||||
|
scope.model.initialized = true;
|
||||||
|
ctrl._checkLoaded();
|
||||||
|
expect(ctrl.buildSubnetOptions).toHaveBeenCalled();
|
||||||
|
expect(ctrl.dataLoaded).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
@ -11,18 +11,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xs-12 col-sm-8 col-md-6">
|
|
||||||
<div class="form-group">
|
|
||||||
<label translate class="control-label" for="description">Description</label>
|
|
||||||
<input name="description" id="description" type="text" class="form-control"
|
|
||||||
ng-model="model.spec.loadbalancer.description">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
|
|
||||||
<div class="col-xs-12 col-sm-8 col-md-6">
|
<div class="col-xs-12 col-sm-8 col-md-6">
|
||||||
<div class="form-group"
|
<div class="form-group"
|
||||||
ng-class="{ 'has-error': loadBalancerDetailsForm.ip.$invalid && loadBalancerDetailsForm.ip.$dirty }">
|
ng-class="{ 'has-error': loadBalancerDetailsForm.ip.$invalid && loadBalancerDetailsForm.ip.$dirty }">
|
||||||
@ -35,25 +23,43 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-xs-12 col-sm-8 col-md-6">
|
|
||||||
<div class="form-group required">
|
|
||||||
<label class="control-label" for="subnet">
|
|
||||||
<translate>Subnet</translate>
|
|
||||||
<span class="hz-icon-required fa fa-asterisk"></span>
|
|
||||||
</label>
|
|
||||||
<select class="form-control" name="subnet" id="subnet"
|
|
||||||
ng-options="subnet.name || subnet.id for subnet in model.subnets"
|
|
||||||
ng-model="model.spec.loadbalancer.vip_subnet_id" ng-required="true"
|
|
||||||
ng-disabled="model.context.id">
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-sm-12 col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label translate class="control-label" for="description">Description</label>
|
||||||
|
<input name="description" id="description" type="text" class="form-control"
|
||||||
|
ng-model="model.spec.loadbalancer.description">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-sm-12 col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">
|
||||||
|
<translate>Subnet</translate>
|
||||||
|
<span class="hz-icon-required fa fa-asterisk"></span>
|
||||||
|
</label>
|
||||||
|
<!-- value="model.spec.loadbalancer.vip_subnet_id" -->
|
||||||
|
<filter-select
|
||||||
|
shorthand="ctrl.shorthand"
|
||||||
|
|
||||||
|
on-select="ctrl.setSubnet(option)"
|
||||||
|
disabled="model.context.id"
|
||||||
|
columns="ctrl.subnetColumns"
|
||||||
|
options="ctrl.subnetOptions"
|
||||||
|
loaded="ctrl.dataLoaded"
|
||||||
|
|
||||||
|
ng-required="true"
|
||||||
|
ng-model="model.spec.loadbalancer.vip_subnet_id"
|
||||||
|
></filter-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<div class="col-xs-12 col-sm-8 col-md-6">
|
<div class="col-xs-12 col-sm-8 col-md-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label required" translate>Admin State Up</label>
|
<label class="control-label required" translate>Admin State Up</label>
|
||||||
@ -67,7 +73,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,6 +84,7 @@
|
|||||||
|
|
||||||
subnets: [],
|
subnets: [],
|
||||||
members: [],
|
members: [],
|
||||||
|
networks: {},
|
||||||
listenerProtocols: ['HTTP', 'TCP', 'TERMINATED_HTTPS', 'HTTPS'],
|
listenerProtocols: ['HTTP', 'TCP', 'TERMINATED_HTTPS', 'HTTPS'],
|
||||||
l7policyActions: ['REJECT', 'REDIRECT_TO_URL', 'REDIRECT_TO_POOL'],
|
l7policyActions: ['REJECT', 'REDIRECT_TO_URL', 'REDIRECT_TO_POOL'],
|
||||||
l7ruleTypes: ['HOST_NAME', 'PATH', 'FILE_TYPE', 'HEADER', 'COOKIE'],
|
l7ruleTypes: ['HOST_NAME', 'PATH', 'FILE_TYPE', 'HEADER', 'COOKIE'],
|
||||||
@ -264,11 +265,18 @@
|
|||||||
return $q.all([
|
return $q.all([
|
||||||
neutronAPI.getSubnets().then(onGetSubnets),
|
neutronAPI.getSubnets().then(onGetSubnets),
|
||||||
neutronAPI.getPorts().then(onGetPorts),
|
neutronAPI.getPorts().then(onGetPorts),
|
||||||
|
neutronAPI.getNetworks().then(onGetNetworks),
|
||||||
novaAPI.getServers().then(onGetServers),
|
novaAPI.getServers().then(onGetServers),
|
||||||
keymanagerPromise.then(prepareCertificates, angular.noop)
|
keymanagerPromise.then(prepareCertificates, angular.noop)
|
||||||
]).then(initMemberAddresses);
|
]).then(initMemberAddresses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onGetNetworks(response) {
|
||||||
|
angular.forEach(response.data.items, function(value) {
|
||||||
|
model.networks[value.id] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function initCreateListener(keymanagerPromise) {
|
function initCreateListener(keymanagerPromise) {
|
||||||
model.context.submit = createListener;
|
model.context.submit = createListener;
|
||||||
return $q.all([
|
return $q.all([
|
||||||
@ -330,7 +338,8 @@
|
|||||||
model.context.submit = editLoadBalancer;
|
model.context.submit = editLoadBalancer;
|
||||||
return $q.all([
|
return $q.all([
|
||||||
lbaasv2API.getLoadBalancer(model.context.id).then(onGetLoadBalancer),
|
lbaasv2API.getLoadBalancer(model.context.id).then(onGetLoadBalancer),
|
||||||
neutronAPI.getSubnets().then(onGetSubnets)
|
neutronAPI.getSubnets().then(onGetSubnets),
|
||||||
|
neutronAPI.getNetworks().then(onGetNetworks)
|
||||||
]).then(initSubnet);
|
]).then(initSubnet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
describe('LBaaS v2 Workflow Model Service', function() {
|
describe('LBaaS v2 Workflow Model Service', function() {
|
||||||
var model, $q, scope, listenerResources, barbicanEnabled, certificatesError;
|
var model, $q, scope, listenerResources, barbicanEnabled,
|
||||||
|
certificatesError, mockNetworks;
|
||||||
var includeChildResources = true;
|
var includeChildResources = true;
|
||||||
|
|
||||||
beforeEach(module('horizon.framework.util.i18n'));
|
beforeEach(module('horizon.framework.util.i18n'));
|
||||||
@ -33,7 +34,7 @@
|
|||||||
protocol: 'HTTP',
|
protocol: 'HTTP',
|
||||||
protocol_port: 80,
|
protocol_port: 80,
|
||||||
connection_limit: 999,
|
connection_limit: 999,
|
||||||
load_balancers: [ { id: '1234' } ],
|
load_balancers: [{id: '1234'}],
|
||||||
sni_container_refs: ['container2'],
|
sni_container_refs: ['container2'],
|
||||||
insert_headers: {
|
insert_headers: {
|
||||||
'X-Forwarded-For': 'True',
|
'X-Forwarded-For': 'True',
|
||||||
@ -84,19 +85,29 @@
|
|||||||
};
|
};
|
||||||
barbicanEnabled = true;
|
barbicanEnabled = true;
|
||||||
certificatesError = false;
|
certificatesError = false;
|
||||||
|
mockNetworks = {
|
||||||
|
a1: {
|
||||||
|
name: 'network_1',
|
||||||
|
id: 'a1'
|
||||||
|
},
|
||||||
|
b2: {
|
||||||
|
name: 'network_2',
|
||||||
|
id: 'b2'
|
||||||
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(module(function($provide) {
|
beforeEach(module(function($provide) {
|
||||||
$provide.value('horizon.app.core.openstack-service-api.lbaasv2', {
|
$provide.value('horizon.app.core.openstack-service-api.lbaasv2', {
|
||||||
getLoadBalancers: function() {
|
getLoadBalancers: function() {
|
||||||
var loadbalancers = [
|
var loadbalancers = [
|
||||||
{ id: '1234', name: 'Load Balancer 1' },
|
{id: '1234', name: 'Load Balancer 1'},
|
||||||
{ id: '5678', name: 'Load Balancer 2' },
|
{id: '5678', name: 'Load Balancer 2'},
|
||||||
{ id: '9012', name: 'myLoadBalancer3' }
|
{id: '9012', name: 'myLoadBalancer3'}
|
||||||
];
|
];
|
||||||
|
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
deferred.resolve({ data: { items: loadbalancers } });
|
deferred.resolve({data: {items: loadbalancers}});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
@ -111,31 +122,36 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
deferred.resolve({ data: loadbalancer });
|
deferred.resolve({data: loadbalancer});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
getListeners: function() {
|
getListeners: function() {
|
||||||
var listeners = [
|
var listeners = [
|
||||||
{ id: '1234', name: 'Listener 1', protocol_port: 80 },
|
{id: '1234', name: 'Listener 1', protocol_port: 80},
|
||||||
{ id: '5678', name: 'Listener 2', protocol_port: 81 },
|
{id: '5678', name: 'Listener 2', protocol_port: 81},
|
||||||
{ id: '9012', name: 'myListener3', protocol_port: 82 }
|
{id: '9012', name: 'myListener3', protocol_port: 82}
|
||||||
];
|
];
|
||||||
|
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
deferred.resolve({ data: { items: listeners } });
|
deferred.resolve({data: {items: listeners}});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
getPools: function() {
|
getPools: function() {
|
||||||
var pools = [
|
var pools = [
|
||||||
{ id: '1234', name: 'Pool 1', listeners: [ '1234' ], protocol: 'HTTP' },
|
{
|
||||||
{ id: '5678', name: 'Pool 2', listeners: [], protocol: 'HTTP' },
|
id: '1234',
|
||||||
{ id: '9012', name: 'Pool 3', listeners: [], protocol: 'HTTPS' }
|
name: 'Pool 1',
|
||||||
|
listeners: ['1234'],
|
||||||
|
protocol: 'HTTP'
|
||||||
|
},
|
||||||
|
{id: '5678', name: 'Pool 2', listeners: [], protocol: 'HTTP'},
|
||||||
|
{id: '9012', name: 'Pool 3', listeners: [], protocol: 'HTTPS'}
|
||||||
];
|
];
|
||||||
|
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
deferred.resolve({ data: { items: pools } });
|
deferred.resolve({data: {items: pools}});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
@ -143,7 +159,7 @@
|
|||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
var listenerData;
|
var listenerData;
|
||||||
listenerData = includeChildResources ? listenerResources : listenerResources.listener;
|
listenerData = includeChildResources ? listenerResources : listenerResources.listener;
|
||||||
deferred.resolve({ data: listenerData });
|
deferred.resolve({data: listenerData});
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
getL7Policy: function() {
|
getL7Policy: function() {
|
||||||
@ -158,7 +174,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
deferred.resolve({ data: l7policy });
|
deferred.resolve({data: l7policy});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
@ -173,7 +189,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
deferred.resolve({ data: l7rule });
|
deferred.resolve({data: l7rule});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
@ -183,7 +199,7 @@
|
|||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
var poolData;
|
var poolData;
|
||||||
poolData = includeChildResources ? poolResources : poolResources.pool;
|
poolData = includeChildResources ? poolResources : poolResources.pool;
|
||||||
deferred.resolve({ data: poolData });
|
deferred.resolve({data: poolData});
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
getMembers: function() {
|
getMembers: function() {
|
||||||
@ -204,7 +220,7 @@
|
|||||||
}];
|
}];
|
||||||
|
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
deferred.resolve({ data: { items: members } });
|
deferred.resolve({data: {items: members}});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
@ -222,7 +238,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
deferred.resolve({ data: monitor });
|
deferred.resolve({data: monitor});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
@ -276,12 +292,12 @@
|
|||||||
}, {
|
}, {
|
||||||
container_ref: 'container2',
|
container_ref: 'container2',
|
||||||
secret_refs: [{name: 'certificate', secret_ref: 'certificate1'},
|
secret_refs: [{name: 'certificate', secret_ref: 'certificate1'},
|
||||||
{name: 'private_key', secret_ref: 'privatekey1'}]
|
{name: 'private_key', secret_ref: 'privatekey1'}]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
container_ref: 'container3',
|
container_ref: 'container3',
|
||||||
secret_refs: [{name: 'certificate', secret_ref: 'certificate2'},
|
secret_refs: [{name: 'certificate', secret_ref: 'certificate2'},
|
||||||
{name: 'private_key', secret_ref: 'privatekey2'}]
|
{name: 'private_key', secret_ref: 'privatekey2'}]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -289,7 +305,7 @@
|
|||||||
if (certificatesError) {
|
if (certificatesError) {
|
||||||
deferred.reject();
|
deferred.reject();
|
||||||
} else {
|
} else {
|
||||||
deferred.resolve({ data: { items: containers } });
|
deferred.resolve({data: {items: containers}});
|
||||||
}
|
}
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
@ -300,18 +316,18 @@
|
|||||||
name: 'foo',
|
name: 'foo',
|
||||||
expiration: '2016-03-26T21:10:45.417835',
|
expiration: '2016-03-26T21:10:45.417835',
|
||||||
secret_ref: 'certificate1'
|
secret_ref: 'certificate1'
|
||||||
},{
|
}, {
|
||||||
expiration: '2016-03-28T21:10:45.417835',
|
expiration: '2016-03-28T21:10:45.417835',
|
||||||
secret_ref: 'certificate2'
|
secret_ref: 'certificate2'
|
||||||
},{
|
}, {
|
||||||
secret_ref: 'privatekey1'
|
secret_ref: 'privatekey1'
|
||||||
},{
|
}, {
|
||||||
secret_ref: 'privatekey2'
|
secret_ref: 'privatekey2'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
deferred.resolve({ data: { items: secrets } });
|
deferred.resolve({data: {items: secrets}});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
@ -319,37 +335,54 @@
|
|||||||
|
|
||||||
$provide.value('horizon.app.core.openstack-service-api.neutron', {
|
$provide.value('horizon.app.core.openstack-service-api.neutron', {
|
||||||
getSubnets: function() {
|
getSubnets: function() {
|
||||||
var subnets = [ { id: 'subnet-1', name: 'subnet-1' },
|
var subnets = [{id: 'subnet-1', name: 'subnet-1'},
|
||||||
{ id: 'subnet-2', name: 'subnet-2' } ];
|
{id: 'subnet-2', name: 'subnet-2'}];
|
||||||
|
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
deferred.resolve({ data: { items: subnets } });
|
deferred.resolve({data: {items: subnets}});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
getPorts: function() {
|
getPorts: function() {
|
||||||
var ports = [ { device_id: '1',
|
var ports = [{
|
||||||
fixed_ips: [{ ip_address: '1.2.3.4', subnet_id: '1' },
|
device_id: '1',
|
||||||
{ ip_address: '2.3.4.5', subnet_id: '2' }] },
|
fixed_ips: [{ip_address: '1.2.3.4', subnet_id: '1'},
|
||||||
{ device_id: '2',
|
{ip_address: '2.3.4.5', subnet_id: '2'}]
|
||||||
fixed_ips: [{ ip_address: '3.4.5.6', subnet_id: '1' },
|
},
|
||||||
{ ip_address: '4.5.6.7', 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();
|
var deferred = $q.defer();
|
||||||
deferred.resolve({ data: { items: ports } });
|
deferred.resolve({data: {items: ports}});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
},
|
||||||
|
getNetworks: function() {
|
||||||
|
var networks = [{
|
||||||
|
name: 'network_1',
|
||||||
|
id: 'a1'
|
||||||
|
}, {
|
||||||
|
name: 'network_2',
|
||||||
|
id: 'b2'
|
||||||
|
}];
|
||||||
|
|
||||||
|
var deferred = $q.defer();
|
||||||
|
deferred.resolve({data: {items: networks}});
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$provide.value('horizon.app.core.openstack-service-api.nova', {
|
$provide.value('horizon.app.core.openstack-service-api.nova', {
|
||||||
getServers: function() {
|
getServers: function() {
|
||||||
var servers = [ { id: '1', name: 'server-1', addresses: { foo: 'bar' } },
|
var servers = [{id: '1', name: 'server-1', addresses: {foo: 'bar'}},
|
||||||
{ id: '2', name: 'server-2', addresses: { foo: 'bar' } },
|
{id: '2', name: 'server-2', addresses: {foo: 'bar'}},
|
||||||
{ id: '3', name: 'server-3' }];
|
{id: '3', name: 'server-3'}];
|
||||||
|
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
deferred.resolve({ data: { items: servers } });
|
deferred.resolve({data: {items: servers}});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
@ -364,7 +397,7 @@
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(inject(function ($injector) {
|
beforeEach(inject(function($injector) {
|
||||||
model = $injector.get(
|
model = $injector.get(
|
||||||
'horizon.dashboard.project.lbaasv2.workflow.model'
|
'horizon.dashboard.project.lbaasv2.workflow.model'
|
||||||
);
|
);
|
||||||
@ -391,6 +424,10 @@
|
|||||||
expect(model.subnets).toEqual([]);
|
expect(model.subnets).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has empty networks', function() {
|
||||||
|
expect(model.networks).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
it('has empty members array', function() {
|
it('has empty members array', function() {
|
||||||
expect(model.members).toEqual([]);
|
expect(model.members).toEqual([]);
|
||||||
});
|
});
|
||||||
@ -421,7 +458,7 @@
|
|||||||
|
|
||||||
it('has array of monitor http_methods', function() {
|
it('has array of monitor http_methods', function() {
|
||||||
expect(model.monitorMethods).toEqual(['GET', 'HEAD', 'POST', 'PUT', 'DELETE',
|
expect(model.monitorMethods).toEqual(['GET', 'HEAD', 'POST', 'PUT', 'DELETE',
|
||||||
'TRACE', 'OPTIONS', 'PATCH', 'CONNECT']);
|
'TRACE', 'OPTIONS', 'PATCH', 'CONNECT']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has an "initialize" function', function() {
|
it('has an "initialize" function', function() {
|
||||||
@ -448,6 +485,7 @@
|
|||||||
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.networks).toEqual(mockNetworks);
|
||||||
expect(model.members.length).toBe(2);
|
expect(model.members.length).toBe(2);
|
||||||
expect(model.certificates.length).toBe(2);
|
expect(model.certificates.length).toBe(2);
|
||||||
expect(model.listenerPorts.length).toBe(0);
|
expect(model.listenerPorts.length).toBe(0);
|
||||||
@ -703,6 +741,7 @@
|
|||||||
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.networks).toEqual(mockNetworks);
|
||||||
expect(model.members.length).toBe(0);
|
expect(model.members.length).toBe(0);
|
||||||
expect(model.certificates.length).toBe(0);
|
expect(model.certificates.length).toBe(0);
|
||||||
expect(model.listenerPorts.length).toBe(0);
|
expect(model.listenerPorts.length).toBe(0);
|
||||||
@ -721,7 +760,10 @@
|
|||||||
expect(model.spec.loadbalancer.name).toEqual('Load Balancer 1');
|
expect(model.spec.loadbalancer.name).toEqual('Load Balancer 1');
|
||||||
expect(model.spec.loadbalancer.description).toEqual('');
|
expect(model.spec.loadbalancer.description).toEqual('');
|
||||||
expect(model.spec.loadbalancer.vip_address).toEqual('1.2.3.4');
|
expect(model.spec.loadbalancer.vip_address).toEqual('1.2.3.4');
|
||||||
expect(model.spec.loadbalancer.vip_subnet_id).toEqual({ id: 'subnet-1', name: 'subnet-1' });
|
expect(model.spec.loadbalancer.vip_subnet_id).toEqual({
|
||||||
|
id: 'subnet-1',
|
||||||
|
name: 'subnet-1'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not initialize listener model spec properties', function() {
|
it('should not initialize listener model spec properties', function() {
|
||||||
@ -902,12 +944,18 @@
|
|||||||
it('should initialize members and properties', function() {
|
it('should initialize members and properties', function() {
|
||||||
expect(model.spec.members[0].id).toBe('1234');
|
expect(model.spec.members[0].id).toBe('1234');
|
||||||
expect(model.spec.members[0].address).toBe('1.2.3.4');
|
expect(model.spec.members[0].address).toBe('1.2.3.4');
|
||||||
expect(model.spec.members[0].subnet_id).toEqual({ id: 'subnet-1', name: 'subnet-1' });
|
expect(model.spec.members[0].subnet_id).toEqual({
|
||||||
|
id: 'subnet-1',
|
||||||
|
name: 'subnet-1'
|
||||||
|
});
|
||||||
expect(model.spec.members[0].protocol_port).toBe(80);
|
expect(model.spec.members[0].protocol_port).toBe(80);
|
||||||
expect(model.spec.members[0].weight).toBe(1);
|
expect(model.spec.members[0].weight).toBe(1);
|
||||||
expect(model.spec.members[1].id).toBe('5678');
|
expect(model.spec.members[1].id).toBe('5678');
|
||||||
expect(model.spec.members[1].address).toBe('5.6.7.8');
|
expect(model.spec.members[1].address).toBe('5.6.7.8');
|
||||||
expect(model.spec.members[1].subnet_id).toEqual({ id: 'subnet-1', name: 'subnet-1' });
|
expect(model.spec.members[1].subnet_id).toEqual({
|
||||||
|
id: 'subnet-1',
|
||||||
|
name: 'subnet-1'
|
||||||
|
});
|
||||||
expect(model.spec.members[1].protocol_port).toBe(80);
|
expect(model.spec.members[1].protocol_port).toBe(80);
|
||||||
expect(model.spec.members[1].weight).toBe(1);
|
expect(model.spec.members[1].weight).toBe(1);
|
||||||
});
|
});
|
||||||
@ -1033,12 +1081,18 @@
|
|||||||
it('should initialize members and properties', function() {
|
it('should initialize members and properties', function() {
|
||||||
expect(model.spec.members[0].id).toBe('1234');
|
expect(model.spec.members[0].id).toBe('1234');
|
||||||
expect(model.spec.members[0].address).toBe('1.2.3.4');
|
expect(model.spec.members[0].address).toBe('1.2.3.4');
|
||||||
expect(model.spec.members[0].subnet_id).toEqual({ id: 'subnet-1', name: 'subnet-1' });
|
expect(model.spec.members[0].subnet_id).toEqual({
|
||||||
|
id: 'subnet-1',
|
||||||
|
name: 'subnet-1'
|
||||||
|
});
|
||||||
expect(model.spec.members[0].protocol_port).toBe(80);
|
expect(model.spec.members[0].protocol_port).toBe(80);
|
||||||
expect(model.spec.members[0].weight).toBe(1);
|
expect(model.spec.members[0].weight).toBe(1);
|
||||||
expect(model.spec.members[1].id).toBe('5678');
|
expect(model.spec.members[1].id).toBe('5678');
|
||||||
expect(model.spec.members[1].address).toBe('5.6.7.8');
|
expect(model.spec.members[1].address).toBe('5.6.7.8');
|
||||||
expect(model.spec.members[1].subnet_id).toEqual({ id: 'subnet-1', name: 'subnet-1' });
|
expect(model.spec.members[1].subnet_id).toEqual({
|
||||||
|
id: 'subnet-1',
|
||||||
|
name: 'subnet-1'
|
||||||
|
});
|
||||||
expect(model.spec.members[1].protocol_port).toBe(80);
|
expect(model.spec.members[1].protocol_port).toBe(80);
|
||||||
expect(model.spec.members[1].weight).toBe(1);
|
expect(model.spec.members[1].weight).toBe(1);
|
||||||
});
|
});
|
||||||
@ -1117,12 +1171,18 @@
|
|||||||
it('should initialize members and properties', function() {
|
it('should initialize members and properties', function() {
|
||||||
expect(model.spec.members[0].id).toBe('1234');
|
expect(model.spec.members[0].id).toBe('1234');
|
||||||
expect(model.spec.members[0].address).toBe('1.2.3.4');
|
expect(model.spec.members[0].address).toBe('1.2.3.4');
|
||||||
expect(model.spec.members[0].subnet_id).toEqual({ id: 'subnet-1', name: 'subnet-1' });
|
expect(model.spec.members[0].subnet_id).toEqual({
|
||||||
|
id: 'subnet-1',
|
||||||
|
name: 'subnet-1'
|
||||||
|
});
|
||||||
expect(model.spec.members[0].protocol_port).toBe(80);
|
expect(model.spec.members[0].protocol_port).toBe(80);
|
||||||
expect(model.spec.members[0].weight).toBe(1);
|
expect(model.spec.members[0].weight).toBe(1);
|
||||||
expect(model.spec.members[1].id).toBe('5678');
|
expect(model.spec.members[1].id).toBe('5678');
|
||||||
expect(model.spec.members[1].address).toBe('5.6.7.8');
|
expect(model.spec.members[1].address).toBe('5.6.7.8');
|
||||||
expect(model.spec.members[1].subnet_id).toEqual({ id: 'subnet-1', name: 'subnet-1' });
|
expect(model.spec.members[1].subnet_id).toEqual({
|
||||||
|
id: 'subnet-1',
|
||||||
|
name: 'subnet-1'
|
||||||
|
});
|
||||||
expect(model.spec.members[1].protocol_port).toBe(80);
|
expect(model.spec.members[1].protocol_port).toBe(80);
|
||||||
expect(model.spec.members[1].weight).toBe(1);
|
expect(model.spec.members[1].weight).toBe(1);
|
||||||
});
|
});
|
||||||
@ -1294,7 +1354,7 @@
|
|||||||
|
|
||||||
describe('Initialization failure', function() {
|
describe('Initialization failure', function() {
|
||||||
|
|
||||||
beforeEach(inject(function ($injector) {
|
beforeEach(inject(function($injector) {
|
||||||
var neutronAPI = $injector.get('horizon.app.core.openstack-service-api.neutron');
|
var neutronAPI = $injector.get('horizon.app.core.openstack-service-api.neutron');
|
||||||
neutronAPI.getSubnets = function() {
|
neutronAPI.getSubnets = function() {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
@ -1434,9 +1494,9 @@
|
|||||||
model.spec.pool.lb_algorithm = 'LEAST_CONNECTIONS';
|
model.spec.pool.lb_algorithm = 'LEAST_CONNECTIONS';
|
||||||
model.spec.pool.session_persistence.type = 'SOURCE_IP';
|
model.spec.pool.session_persistence.type = 'SOURCE_IP';
|
||||||
model.spec.members = [{
|
model.spec.members = [{
|
||||||
address: { ip: '1.2.3.4', subnet: '1' },
|
address: {ip: '1.2.3.4', subnet: '1'},
|
||||||
addresses: [{ ip: '1.2.3.4', subnet: '1' },
|
addresses: [{ip: '1.2.3.4', subnet: '1'},
|
||||||
{ ip: '2.3.4.5', subnet: '2' }],
|
{ip: '2.3.4.5', subnet: '2'}],
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
protocol_port: 80,
|
protocol_port: 80,
|
||||||
@ -1456,7 +1516,7 @@
|
|||||||
}, {
|
}, {
|
||||||
id: 'external-member-2',
|
id: 'external-member-2',
|
||||||
address: '3.4.5.6',
|
address: '3.4.5.6',
|
||||||
subnet_id: { id: '1' },
|
subnet_id: {id: '1'},
|
||||||
protocol_port: 80,
|
protocol_port: 80,
|
||||||
weight: 1
|
weight: 1
|
||||||
}];
|
}];
|
||||||
@ -1690,9 +1750,9 @@
|
|||||||
model.spec.pool.description = 'pool description';
|
model.spec.pool.description = 'pool description';
|
||||||
model.spec.pool.lb_algorithm = 'LEAST_CONNECTIONS';
|
model.spec.pool.lb_algorithm = 'LEAST_CONNECTIONS';
|
||||||
model.spec.members = [{
|
model.spec.members = [{
|
||||||
address: { ip: '1.2.3.4', subnet: '1' },
|
address: {ip: '1.2.3.4', subnet: '1'},
|
||||||
addresses: [{ ip: '1.2.3.4', subnet: '1' },
|
addresses: [{ip: '1.2.3.4', subnet: '1'},
|
||||||
{ ip: '2.3.4.5', subnet: '2' }],
|
{ip: '2.3.4.5', subnet: '2'}],
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
protocol_port: 80,
|
protocol_port: 80,
|
||||||
@ -1712,7 +1772,7 @@
|
|||||||
}, {
|
}, {
|
||||||
id: 'external-member-2',
|
id: 'external-member-2',
|
||||||
address: '3.4.5.6',
|
address: '3.4.5.6',
|
||||||
subnet_id: { id: '1' },
|
subnet_id: {id: '1'},
|
||||||
protocol_port: 80,
|
protocol_port: 80,
|
||||||
weight: 1
|
weight: 1
|
||||||
}];
|
}];
|
||||||
@ -1945,9 +2005,9 @@
|
|||||||
model.spec.pool.description = 'pool description';
|
model.spec.pool.description = 'pool description';
|
||||||
model.spec.pool.lb_algorithm = 'LEAST_CONNECTIONS';
|
model.spec.pool.lb_algorithm = 'LEAST_CONNECTIONS';
|
||||||
model.spec.members = [{
|
model.spec.members = [{
|
||||||
address: { ip: '1.2.3.4', subnet: '1' },
|
address: {ip: '1.2.3.4', subnet: '1'},
|
||||||
addresses: [{ ip: '1.2.3.4', subnet: '1' },
|
addresses: [{ip: '1.2.3.4', subnet: '1'},
|
||||||
{ ip: '2.3.4.5', subnet: '2' }],
|
{ip: '2.3.4.5', subnet: '2'}],
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
protocol_port: 80,
|
protocol_port: 80,
|
||||||
@ -1967,7 +2027,7 @@
|
|||||||
}, {
|
}, {
|
||||||
id: 'external-member-2',
|
id: 'external-member-2',
|
||||||
address: '3.4.5.6',
|
address: '3.4.5.6',
|
||||||
subnet_id: { id: '1' },
|
subnet_id: {id: '1'},
|
||||||
protocol_port: 80,
|
protocol_port: 80,
|
||||||
weight: 1
|
weight: 1
|
||||||
}];
|
}];
|
||||||
|
11
releasenotes/notes/filter-select-65160dcbe699a96d.yaml
Normal file
11
releasenotes/notes/filter-select-65160dcbe699a96d.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Adds a new UI component which works as a standard select control
|
||||||
|
alternative. Options in the component are presented in a table which may be
|
||||||
|
filtered using the select input field. Filtering is done across all table
|
||||||
|
fields.
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
The new component replaces the standard select for subnet selection in
|
||||||
|
the Load Balancer creation modal wizard.
|
Loading…
Reference in New Issue
Block a user