Add the angular LBaaS V2 members table and detail pages

This change implements the members table under the 'Members' tab on the pools
detail page that shows all of the members that belong to that particular pool.
It also adds the member detail page which includes the details for a specific
member in the pool.

Partially-Implements: blueprint horizon-lbaas-v2-ui
Change-Id: I879c840d174630c697375c5ce7649b01303aeb00
This commit is contained in:
Lucas Palm 2016-01-05 11:42:40 -06:00
parent 964d845e21
commit 7b90156da1
15 changed files with 628 additions and 7 deletions

View File

@ -310,3 +310,39 @@ class Pool(generic.View):
"""
lb = neutronclient(request).show_lbaas_pool(pool_id)
return lb.get('pool')
@urls.register
class Members(generic.View):
"""API for load balancer members.
"""
url_regex = r'lbaas/pools/(?P<pool_id>[^/]+)/members/$'
@rest_utils.ajax()
def get(self, request, pool_id):
"""List of members for the current project.
The listing result is an object with property "items".
"""
tenant_id = request.user.project_id
result = neutronclient(request).list_lbaas_members(pool_id,
tenant_id=tenant_id)
return {'items': result.get('members')}
@urls.register
class Member(generic.View):
"""API for retrieving a single member.
"""
url_regex = r'lbaas/pools/(?P<pool_id>[^/]+)' + \
'/members/(?P<member_id>[^/]+)/$'
@rest_utils.ajax()
def get(self, request, member_id, pool_id):
"""Get a specific member belonging to a specific pool.
"""
lb = neutronclient(request).show_lbaas_member(member_id, pool_id)
return lb.get('member')

View File

@ -57,7 +57,10 @@ ADD_JS_FILES = [
'dashboard/project/lbaasv2/listeners/detail.controller.js',
'dashboard/project/lbaasv2/listeners/filters.js',
'dashboard/project/lbaasv2/pools/pools.module.js',
'dashboard/project/lbaasv2/pools/detail.controller.js'
'dashboard/project/lbaasv2/pools/detail.controller.js',
'dashboard/project/lbaasv2/members/members.module.js',
'dashboard/project/lbaasv2/members/detail.controller.js',
'dashboard/project/lbaasv2/members/table.controller.js'
]
ADD_JS_SPEC_FILES = [
@ -89,7 +92,10 @@ ADD_JS_SPEC_FILES = [
'dashboard/project/lbaasv2/listeners/detail.controller.spec.js',
'dashboard/project/lbassv2/listeners/filters.spec.js',
'dashboard/project/lbaasv2/pools/pools.module.spec.js',
'dashboard/project/lbaasv2/pools/detail.controller.spec.js'
'dashboard/project/lbaasv2/pools/detail.controller.spec.js',
'dashboard/project/lbaasv2/members/members.module.spec.js',
'dashboard/project/lbaasv2/members/detail.controller.spec.js',
'dashboard/project/lbaasv2/members/table.controller.spec.js'
]
ADD_SCSS_FILES = [

View File

@ -42,7 +42,9 @@
editLoadBalancer: editLoadBalancer,
getListeners: getListeners,
getListener: getListener,
getPool: getPool
getPool: getPool,
getMembers: getMembers,
getMember: getMember
};
return service;
@ -152,6 +154,8 @@
});
}
// Pools
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.getPool
* @description
@ -167,5 +171,42 @@
});
}
// Members
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.getMembers
* @description
* Get a list of members.
* @param {string} poolId
* Specifies the id of the pool the members belong to.
*
* The listing result is an object with property "items". Each item is
* a member.
*/
function getMembers(poolId) {
return apiService.get('/api/lbaas/pools/' + poolId + '/members/')
.error(function () {
toastService.add('error', gettext('Unable to retrieve members.'));
});
}
/**
* @name horizon.app.core.openstack-service-api.lbaasv2.getMember
* @description
* Get a single pool Member by ID.
* @param {string} poolId
* Specifies the id of the pool the member belongs to.
* @param {string} memberId
* Specifies the id of the member to request.
*/
function getMember(poolId, memberId) {
return apiService.get('/api/lbaas/pools/' + poolId + '/members/' + memberId)
.error(function () {
toastService.add('error', gettext('Unable to retrieve member.'));
});
}
}
}());

View File

@ -90,6 +90,25 @@
'1234'
]
},
{
"func": "getMembers",
"method": "get",
"path": "/api/lbaas/pools/1234/members/",
"error": "Unable to retrieve members.",
"testInput": [
'1234'
]
},
{
"func": "getMember",
"method": "get",
"path": "/api/lbaas/pools/1234/members/5678",
"error": "Unable to retrieve member.",
"testInput": [
'1234',
'5678'
]
},
{
"func": "createLoadBalancer",
"method": "post",

View File

@ -28,7 +28,8 @@
'ngRoute',
'horizon.dashboard.project.lbaasv2.loadbalancers',
'horizon.dashboard.project.lbaasv2.listeners',
'horizon.dashboard.project.lbaasv2.pools'
'horizon.dashboard.project.lbaasv2.pools',
'horizon.dashboard.project.lbaasv2.members'
])
.config(config)
.constant('horizon.dashboard.project.lbaasv2.patterns', {
@ -68,6 +69,9 @@
})
.when(href + 'pools/detail/:poolId', {
templateUrl: basePath + 'pools/detail.html'
})
.when(href + 'pools/:poolId/members/detail/:memberId', {
templateUrl: basePath + 'members/detail.html'
});
}

View File

@ -120,10 +120,16 @@
{
templateUrl: basePath + 'pools/detail.html'
}
],
[
href + 'pools/:poolId/members/detail/:memberId',
{
templateUrl: basePath + 'members/detail.html'
}
]
];
expect($routeProvider.when.calls.count()).toBe(4);
expect($routeProvider.when.calls.count()).toBe(5);
angular.forEach($routeProvider.when.calls.all(), function(call, i) {
expect(call.args).toEqual(routes[i]);
});

View File

@ -0,0 +1,98 @@
/*
* 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.members')
.controller('MemberDetailController', MemberDetailController);
MemberDetailController.$inject = [
'horizon.app.core.openstack-service-api.lbaasv2',
'$routeParams'
];
/**
* @ngdoc controller
* @name MemberDetailController
*
* @description
* Controller for the LBaaS v2 member detail page.
*
* @param api The LBaaS v2 API service.
* @param $routeParams The angular $routeParams service.
* @returns undefined
*/
function MemberDetailController(api, $routeParams) {
var ctrl = this;
ctrl.member = {};
ctrl.pool = {};
ctrl.listener = {};
ctrl.loadbalancer = {};
var poolID = $routeParams.poolId;
var memberID = $routeParams.memberId;
init();
////////////////////////////////
function init() {
api.getMember(poolID, memberID).success(memberSuccess);
}
function memberSuccess(response) {
ctrl.member = response;
getPoolDetails(poolID);
}
function getPoolDetails(poolId) {
api.getPool(poolId).success(poolSuccess);
}
function poolSuccess(response) {
ctrl.pool = response;
if (ctrl.pool.hasOwnProperty('listeners') &&
ctrl.pool.listeners.length > 0) {
getListenerDetails(ctrl.pool.listeners[0].id);
}
}
function getListenerDetails(listenerId) {
api.getListener(listenerId).success(listenerSuccess);
}
function listenerSuccess(response) {
ctrl.listener = response;
if (ctrl.listener.hasOwnProperty('loadbalancers') &&
ctrl.listener.loadbalancers.length > 0) {
getLoadBalancerDetails(ctrl.listener.loadbalancers[0].id);
}
}
function getLoadBalancerDetails(loadbalancerId) {
api.getLoadBalancer(loadbalancerId).success(loadbalancerSuccess);
}
function loadbalancerSuccess(response) {
ctrl.loadbalancer = response;
}
}
})();

View File

@ -0,0 +1,111 @@
/*
* Copyright 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
describe('LBaaS v2 Member Detail Controller', function() {
var controller, lbaasv2API, member, pool, listener, loadbalancer;
function fakeMemberAPI() {
return {
success: function(callback) {
callback(member);
}
};
}
function fakePoolAPI() {
return {
success: function(callback) {
callback(pool);
}
};
}
function fakeListenerAPI() {
return {
success: function(callback) {
callback(listener);
}
};
}
function fakeLoadBalancerAPI() {
return {
success: function(callback) {
callback(loadbalancer);
}
};
}
///////////////////////
beforeEach(module('horizon.framework.util.http'));
beforeEach(module('horizon.framework.widgets.toast'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(inject(function($injector) {
member = { id: '5678' };
lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2');
controller = $injector.get('$controller');
spyOn(lbaasv2API, 'getMember').and.callFake(fakeMemberAPI);
spyOn(lbaasv2API, 'getPool').and.callFake(fakePoolAPI);
spyOn(lbaasv2API, 'getListener').and.callFake(fakeListenerAPI);
spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(fakeLoadBalancerAPI);
}));
function createController() {
return controller('MemberDetailController', {
api: lbaasv2API,
$routeParams: { poolId: 'poolId', memberId: 'memberId' }
});
}
it('should invoke lbaasv2 apis', function() {
pool = { id: 'poolId', listeners: [{id: 'listenerId'}] };
listener = { id: 'listenerId', loadbalancers: [{id: 'loadbalancerId'}] };
loadbalancer = { id: 'loadbalancerId' };
createController();
expect(lbaasv2API.getMember).toHaveBeenCalledWith('poolId', 'memberId');
expect(lbaasv2API.getPool).toHaveBeenCalledWith('poolId');
expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId');
expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('loadbalancerId');
});
it('should not invoke the getListener or getLoadBalancer lbaasv2 api', function() {
pool = { id: 'poolId', listeners: [] };
createController();
expect(lbaasv2API.getMember).toHaveBeenCalledWith('poolId', 'memberId');
expect(lbaasv2API.getPool).toHaveBeenCalledWith('poolId');
expect(lbaasv2API.getListener).not.toHaveBeenCalled();
expect(lbaasv2API.getLoadBalancer).not.toHaveBeenCalled();
});
it('should not invoke getLoadBalancer lbaasv2 api', function() {
pool = { id: 'poolId', listeners: [{id: 'listenerId'}] };
listener = { id: 'listenerId', loadbalancers: [] };
createController();
expect(lbaasv2API.getMember).toHaveBeenCalledWith('poolId', 'memberId');
expect(lbaasv2API.getPool).toHaveBeenCalledWith('poolId');
expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId');
expect(lbaasv2API.getLoadBalancer).not.toHaveBeenCalled();
});
});
})();

View File

@ -0,0 +1,39 @@
<div class="content" ng-controller="MemberDetailController as ctrl">
<div class='page-header'>
<ol class="breadcrumb">
<li><a href="project/ngloadbalancersv2/"><translate>Load Balancers</translate></a></li>
<li><a href="project/ngloadbalancersv2/detail/{$ ::ctrl.loadbalancer.id $}">{$ ::(ctrl.loadbalancer.name || ctrl.loadbalancer.id) $}</a></li>
<li><a href="project/ngloadbalancersv2/listeners/detail/{$ ::ctrl.listener.id $}">{$ ::(ctrl.listener.name || ctrl.listener.id) $}</a></li>
<li><a href="project/ngloadbalancersv2/pools/detail/{$ ::ctrl.pool.id $}">{$ ::(ctrl.pool.name || ctrl.pool.id) $}</a></li>
<li class="active">{$ ::(ctrl.member.name || ctrl.member.id) $}</li>
</ol>
</div>
<div class="detail-page">
<dl class="dl-horizontal">
<div>
<dt translate>Member ID</dt>
<dd>{$ ::ctrl.member.id $}</dd>
</div>
<div>
<dt translate>Tenant ID</dt>
<dd>{$ ::ctrl.member.tenant_id $}</dd>
</div>
<div>
<dt translate>Protocol Port</dt>
<dd>{$ ::ctrl.member.protocol_port $}</dd>
</div>
<div>
<dt translate>Admin State Up</dt>
<dd>{$ ::ctrl.member.admin_state_up | yesno $}</dd>
</div>
<div>
<dt translate>Address</dt>
<dd>{$ ::ctrl.member.address $}</dd>
</div>
<div>
<dt translate>Weight</dt>
<dd>{$ ::ctrl.member.weight $}</dd>
</div>
</dl>
</div>
</div>

View File

@ -0,0 +1,31 @@
/*
* 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';
/**
* @ngdoc overview
* @ngname horizon.dashboard.project.lbaasv2.members
*
* @description
* Provides the services and widgets required to support and display the project members
* for the load balancers v2 panel.
*/
angular
.module('horizon.dashboard.project.lbaasv2.members', []);
})();

View File

@ -0,0 +1,25 @@
/*
* Copyright 2016 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
describe('LBaaS v2 Members Module', function() {
it('should exist', function() {
expect(angular.module('horizon.dashboard.project.lbaasv2.members')).toBeDefined();
});
});
})();

View File

@ -0,0 +1,64 @@
/*
* 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.members')
.controller('MembersTableController', MembersTableController);
MembersTableController.$inject = [
'horizon.app.core.openstack-service-api.lbaasv2',
'$routeParams'
];
/**
* @ngdoc controller
* @name MembersTableController
*
* @description
* Controller for the LBaaS v2 members table. Serves as the focal point for table actions.
*
* @param api The LBaaS V2 service API.
* @param $routeParams The angular $routeParams service.
* @returns undefined
*/
function MembersTableController(api, $routeParams) {
var ctrl = this;
ctrl.items = [];
ctrl.src = [];
ctrl.checked = {};
var poolID = $routeParams.poolId;
ctrl.pool_id = poolID;
init();
////////////////////////////////
function init() {
api.getMembers(poolID).success(success);
}
function success(response) {
ctrl.src = response.items;
}
}
})();

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';
describe('LBaaS v2 Members Table Controller', function() {
var controller, lbaasv2API, scope;
var items = [];
function fakeAPI() {
return {
success: function(callback) {
callback({ items: items });
}
};
}
///////////////////////
beforeEach(module('horizon.framework.widgets.toast'));
beforeEach(module('horizon.framework.conf'));
beforeEach(module('horizon.framework.util'));
beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function($provide) {
$provide.value('$modal', {});
}));
beforeEach(inject(function($injector) {
lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2');
controller = $injector.get('$controller');
spyOn(lbaasv2API, 'getMembers').and.callFake(fakeAPI);
}));
function createController() {
return controller('MembersTableController', { $scope: scope });
}
it('should initialize correctly', function() {
var ctrl = createController();
expect(ctrl.items).toEqual([]);
expect(ctrl.src).toEqual(items);
expect(ctrl.checked).toEqual({});
});
it('should invoke lbaasv2 apis', function() {
createController();
expect(lbaasv2API.getMembers).toHaveBeenCalled();
});
});
})();

View File

@ -0,0 +1,76 @@
<hz-page-header header="{$ 'Members' | translate $}"></hz-page-header>
<table ng-controller="MembersTableController as table"
hz-table ng-cloak
st-table="table.items"
st-safe-src="table.src"
default-sort="id"
default-sort-reverse="false"
class="table-striped table-rsp table-detail modern">
<!--
TODO(jpomero): This table pattern does not allow for extensibility and should be revisited
once horizon implements a better one.
-->
<thead>
<tr>
<!--
Table-batch-actions:
This is where batch actions like searching, creating, and deleting.
-->
<th colspan="100" class="search-header">
<hz-search-bar group-classes="input-group-sm" icon-classes="fa-search">
</hz-search-bar>
</th>
</tr>
<tr>
<!--
Table-column-headers:
This is where we declaratively define the table column headers.
Include select-col if you want to select all.
Include expander if you want to inline details.
Include action-col if you want to perform actions.
-->
<th class="select-col">
<input type="checkbox" hz-select-all="table.items">
</th>
<th class="rsp-p1" st-sort="id" st-sort-default="id" translate>ID</th>
<th class="rsp-p1" st-sort="address" translate>IP Address</th>
<th class="rsp-p1" st-sort="protocol_port" translate>Protocol Port</th>
<th class="rsp-p1" st-sort="weight" translate>Weight</th>
</tr>
</thead>
<tbody>
<!--
Table-rows:
This is where we declaratively define the table columns.
Include select-col if you want to select all.
Include expander if you want to inline details.
Include action-col if you want to perform actions.
rsp-p1 rsp-p2 are responsive priority as user resizes window.
-->
<tr ng-repeat="item in table.items track by item.id"
ng-class="{'st-selected': checked[item.id]}">
<td class="select-col">
<input type="checkbox"
ng-model="selected[item.id].checked"
hz-select="item">
</td>
<td class="rsp-p1"><a ng-href="project/ngloadbalancersv2/pools/{$ ::table.pool_id $}/members/detail/{$ ::item.id $}">{$ ::item.id $}</a></td>
<td class="rsp-p1">{$ ::item.address $}</td>
<td class="rsp-p1">{$ ::item.protocol_port $}</td>
<td class="rsp-p1">{$ ::item.weight $}</td>
</tr>
</tbody>
<!--
Table-footer:
This is where we display number of items and pagination controls.
-->
<tfoot hz-table-footer items="table.items"></tfoot>
</table>

View File

@ -57,8 +57,7 @@
</dl>
</tab>
<tab heading="{$ 'Members' | translate $}">
<!--This should be uncommented in the members table patch work.-->
<!--<ng-include src="'static/dashboard/project/lbaasv2/members/table.html'"></ng-include>-->
<ng-include src="'static/dashboard/project/lbaasv2/members/table.html'"></ng-include>
</tab>
</tabset>
</div>