Add loading and error status to detail pages

This adds a loading indicator to detail pages and makes sure that the
page content is not displayed until fully loaded. This prevents the
pages from displaying with partial content and having the updated
values pop in. It also adds an error indicator with a message when
there is an error loading the page. The page content will not display
if there is an error. This prevents the user from seeing the page in
an error state with missing information and broken links.

Change-Id: If26553b0650f38b92e9624baa73e531714baeef3
Closes-Bug: #1561102
This commit is contained in:
Justin Pomeroy 2017-07-10 07:23:57 +08:00 committed by Jacky Hu
parent d57d7a4ebd
commit cd74d86dac
19 changed files with 781 additions and 335 deletions

View File

@ -23,7 +23,8 @@
HealthMonitorDetailController.$inject = [ HealthMonitorDetailController.$inject = [
'horizon.app.core.openstack-service-api.lbaasv2', 'horizon.app.core.openstack-service-api.lbaasv2',
'horizon.dashboard.project.lbaasv2.healthmonitors.actions.rowActions', 'horizon.dashboard.project.lbaasv2.healthmonitors.actions.rowActions',
'$routeParams' '$routeParams',
'$q'
]; ];
/** /**
@ -36,12 +37,15 @@
* @param api The LBaaS v2 API service. * @param api The LBaaS v2 API service.
* @param rowActions The LBaaS v2 health monitor row actions service. * @param rowActions The LBaaS v2 health monitor row actions service.
* @param $routeParams The angular $routeParams service. * @param $routeParams The angular $routeParams service.
* @param $q The angular service for promises.
* @returns undefined * @returns undefined
*/ */
function HealthMonitorDetailController(api, rowActions, $routeParams) { function HealthMonitorDetailController(api, rowActions, $routeParams, $q) {
var ctrl = this; var ctrl = this;
ctrl.loading = true;
ctrl.error = false;
ctrl.actions = rowActions.init($routeParams.loadbalancerId, ctrl.actions = rowActions.init($routeParams.loadbalancerId,
$routeParams.listenerId, $routeParams.listenerId,
$routeParams.poolId).actions; $routeParams.poolId).actions;
@ -51,18 +55,46 @@
//////////////////////////////// ////////////////////////////////
function init() { function init() {
api.getHealthMonitor($routeParams.healthmonitorId).success(set('healthmonitor')); ctrl.healthmonitor = null;
api.getPool($routeParams.poolId).success(set('pool')); ctrl.pool = null;
api.getListener($routeParams.listenerId).success(set('listener')); ctrl.listener = null;
api.getLoadBalancer($routeParams.loadbalancerId).success(set('loadbalancer')); ctrl.loadbalancer = null;
ctrl.loading = true;
ctrl.error = false;
$q.all([
api.getHealthMonitor($routeParams.healthmonitorId)
.then(success('healthmonitor'), fail('healthmonitor')),
api.getPool($routeParams.poolId)
.then(success('pool'), fail('pool')),
api.getListener($routeParams.listenerId)
.then(success('listener'), fail('listener')),
api.getLoadBalancer($routeParams.loadbalancerId)
.then(success('loadbalancer'), fail('loadbalancer'))
]).then(postInit, initError);
} }
function set(property) { function success(property) {
return angular.bind(null, function setProp(property, value) { return angular.bind(null, function setProp(property, response) {
ctrl[property] = value; ctrl[property] = response.data;
}, property); }, property);
} }
function fail(property) {
return angular.bind(null, function setProp(property, error) {
ctrl[property] = null;
throw error;
}, property);
}
function postInit() {
ctrl.loading = false;
}
function initError() {
ctrl.loading = false;
ctrl.error = true;
}
} }
})(); })();

View File

@ -17,26 +17,42 @@
'use strict'; 'use strict';
describe('LBaaS v2 Healthmonitor Detail Controller', function() { describe('LBaaS v2 Healthmonitor Detail Controller', function() {
var lbaasv2API, ctrl; var lbaasv2API, $controller, apiFail, qAllFail;
function fakeAPI() { function fakePromise(data, reject) {
return { return {
success: function(callback) { then: function(success, fail) {
callback('foo'); if (reject) {
fail();
} else {
success({ data: data });
}
return fakePromise();
} }
}; };
} }
function fakeAPI() {
return fakePromise('foo', apiFail);
}
function loadbalancerAPI() { function loadbalancerAPI() {
var loadbalancer = { provisioning_status: 'ACTIVE' }; return fakePromise({ provisioning_status: 'ACTIVE' });
return { }
success: function(callback) {
callback(loadbalancer); function qAll() {
}, return fakePromise(null, qAllFail);
then: function(callback) { }
callback({ data: loadbalancer });
function createController() {
return $controller('HealthMonitorDetailController', {
$routeParams: {
loadbalancerId: 'loadbalancerId',
listenerId: 'listenerId',
poolId: 'poolId',
healthmonitorId: 'healthmonitorId'
} }
}; });
} }
/////////////////////// ///////////////////////
@ -47,24 +63,24 @@
beforeEach(module('horizon.app.core.openstack-service-api')); beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2')); beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function($provide) {
apiFail = false;
qAllFail = false;
$provide.value('$q', { all: qAll });
}));
beforeEach(inject(function($injector) { beforeEach(inject(function($injector) {
lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2'); lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2');
spyOn(lbaasv2API, 'getHealthMonitor').and.callFake(fakeAPI); spyOn(lbaasv2API, 'getHealthMonitor').and.callFake(fakeAPI);
spyOn(lbaasv2API, 'getPool').and.callFake(fakeAPI); spyOn(lbaasv2API, 'getPool').and.callFake(fakeAPI);
spyOn(lbaasv2API, 'getListener').and.callFake(fakeAPI); spyOn(lbaasv2API, 'getListener').and.callFake(fakeAPI);
spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(loadbalancerAPI); spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(loadbalancerAPI);
var controller = $injector.get('$controller'); $controller = $injector.get('$controller');
ctrl = controller('HealthMonitorDetailController', {
$routeParams: {
loadbalancerId: 'loadbalancerId',
listenerId: 'listenerId',
poolId: 'poolId',
healthmonitorId: 'healthmonitorId'
}
});
})); }));
it('should invoke lbaasv2 apis', function() { it('should invoke lbaasv2 apis', function() {
var ctrl = createController();
expect(lbaasv2API.getHealthMonitor).toHaveBeenCalledWith('healthmonitorId'); expect(lbaasv2API.getHealthMonitor).toHaveBeenCalledWith('healthmonitorId');
expect(lbaasv2API.getPool).toHaveBeenCalledWith('poolId'); expect(lbaasv2API.getPool).toHaveBeenCalledWith('poolId');
expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId'); expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId');
@ -75,6 +91,21 @@
expect(ctrl.healthmonitor).toBe('foo'); expect(ctrl.healthmonitor).toBe('foo');
}); });
it('should throw error on API fail', function() {
apiFail = true;
var init = function() {
createController();
};
expect(init).toThrow();
});
it('should set error state if any APIs fail', function() {
qAllFail = true;
var ctrl = createController();
expect(ctrl.loading).toBe(false);
expect(ctrl.error).toBe(true);
});
}); });
})(); })();

View File

@ -1,39 +1,42 @@
<div ng-controller="HealthMonitorDetailController as ctrl"> <div ng-controller="HealthMonitorDetailController as ctrl">
<div class="page-header"> <detail-status loading="ctrl.loading" error="ctrl.error"></detail-status>
<ol class="breadcrumb"> <div ng-if="!ctrl.loading && !ctrl.error">
<li><a href="project/ngloadbalancersv2/"><translate>Load Balancers</translate></a></li> <div class="page-header">
<li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}">{$ ::(ctrl.loadbalancer.name || ctrl.loadbalancer.id) $}</a></li> <ol class="breadcrumb">
<li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}">{$ ::(ctrl.listener.name || ctrl.listener.id) $}</a></li> <li><a href="project/ngloadbalancersv2/"><translate>Load Balancers</translate></a></li>
<li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}/pools/{$ ::ctrl.pool.id $}">{$ ::(ctrl.pool.name || ctrl.pool.id) $}</a></li> <li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}">{$ ::(ctrl.loadbalancer.name || ctrl.loadbalancer.id) $}</a></li>
<li class="active">{$ ::(ctrl.healthmonitor.name || ctrl.healthmonitor.id) $}</li> <li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}">{$ ::(ctrl.listener.name || ctrl.listener.id) $}</a></li>
<actions allowed="ctrl.actions" type="row" item="ctrl.healthmonitor" <li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}/pools/{$ ::ctrl.pool.id $}">{$ ::(ctrl.pool.name || ctrl.pool.id) $}</a></li>
ng-if="ctrl.healthmonitor" class="actions_column pull-right"></actions> <li class="active">{$ ::(ctrl.healthmonitor.name || ctrl.healthmonitor.id) $}</li>
</ol> <actions allowed="ctrl.actions" type="row" item="ctrl.healthmonitor"
</div> ng-if="ctrl.healthmonitor" class="actions_column pull-right"></actions>
<div class="row"> </ol>
<div class="col-md-6 detail"> </div>
<dl class="dl-horizontal"> <div class="row">
<dt translate>Type</dt> <div class="col-md-6 detail">
<dd>{$ ::ctrl.healthmonitor.type $}</dd> <dl class="dl-horizontal">
<dt translate>Delay</dt> <dt translate>Type</dt>
<dd>{$ ::ctrl.healthmonitor.delay $}</dd> <dd>{$ ::ctrl.healthmonitor.type $}</dd>
<dt translate>Max Retries</dt> <dt translate>Delay</dt>
<dd>{$ ::ctrl.healthmonitor.max_retries $}</dd> <dd>{$ ::ctrl.healthmonitor.delay $}</dd>
<dt translate>Timeout</dt> <dt translate>Max Retries</dt>
<dd>{$ ::ctrl.healthmonitor.timeout $}</dd> <dd>{$ ::ctrl.healthmonitor.max_retries $}</dd>
<dt translate ng-if="::ctrl.healthmonitor.http_method">HTTP Method</dt> <dt translate>Timeout</dt>
<dd ng-if="::ctrl.healthmonitor.http_method">{$ ::ctrl.healthmonitor.http_method $}</dd> <dd>{$ ::ctrl.healthmonitor.timeout $}</dd>
<dt translate ng-if="::ctrl.healthmonitor.expected_codes">Expected Codes</dt> <dt translate ng-if="::ctrl.healthmonitor.http_method">HTTP Method</dt>
<dd ng-if="::ctrl.healthmonitor.expected_codes">{$ ::ctrl.healthmonitor.expected_codes $}</dd> <dd ng-if="::ctrl.healthmonitor.http_method">{$ ::ctrl.healthmonitor.http_method $}</dd>
<dt translate ng-if="::ctrl.healthmonitor.url_path">URL Path</dt> <dt translate ng-if="::ctrl.healthmonitor.expected_codes">Expected Codes</dt>
<dd ng-if="::ctrl.healthmonitor.url_path">{$ ::ctrl.healthmonitor.url_path $}</dd> <dd ng-if="::ctrl.healthmonitor.expected_codes">{$ ::ctrl.healthmonitor.expected_codes $}</dd>
<dt translate>Admin State Up</dt> <dt translate ng-if="::ctrl.healthmonitor.url_path">URL Path</dt>
<dd>{$ ctrl.healthmonitor.admin_state_up | yesno $}</dd> <dd ng-if="::ctrl.healthmonitor.url_path">{$ ::ctrl.healthmonitor.url_path $}</dd>
<dt translate>Monitor ID</dt> <dt translate>Admin State Up</dt>
<dd>{$ ::ctrl.healthmonitor.id $}</dd> <dd>{$ ctrl.healthmonitor.admin_state_up | yesno $}</dd>
<dt translate>Tenant ID</dt> <dt translate>Monitor ID</dt>
<dd>{$ ::ctrl.healthmonitor.tenant_id $}</dd> <dd>{$ ::ctrl.healthmonitor.id $}</dd>
</dl> <dt translate>Tenant ID</dt>
<dd>{$ ::ctrl.healthmonitor.tenant_id $}</dd>
</dl>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -68,8 +68,9 @@
} }
} }
/* Progress indicator in the table while items are loading */ /* Progress indicator while data is loading */
[table-status] { [table-status],
detail-status {
.progress { .progress {
margin: 0px auto; margin: 0px auto;
width: 25%; width: 25%;
@ -78,4 +79,13 @@
width: 100%; width: 100%;
} }
} }
}
detail-status {
.progress {
margin-top: 25vh;
}
.error-actions {
text-align: center;
margin-top: 10px;
}
} }

View File

@ -23,7 +23,8 @@
ListenerDetailController.$inject = [ ListenerDetailController.$inject = [
'horizon.app.core.openstack-service-api.lbaasv2', 'horizon.app.core.openstack-service-api.lbaasv2',
'horizon.dashboard.project.lbaasv2.listeners.actions.rowActions', 'horizon.dashboard.project.lbaasv2.listeners.actions.rowActions',
'$routeParams' '$routeParams',
'$q'
]; ];
/** /**
@ -36,12 +37,15 @@
* @param api The LBaaS v2 API service. * @param api The LBaaS v2 API service.
* @param rowActions The listener row actions service. * @param rowActions The listener row actions service.
* @param $routeParams The angular $routeParams service. * @param $routeParams The angular $routeParams service.
* @param $q The angular service for promises.
* @returns undefined * @returns undefined
*/ */
function ListenerDetailController(api, rowActions, $routeParams) { function ListenerDetailController(api, rowActions, $routeParams, $q) {
var ctrl = this; var ctrl = this;
ctrl.loading = true;
ctrl.error = false;
ctrl.actions = rowActions.init($routeParams.loadbalancerId).actions; ctrl.actions = rowActions.init($routeParams.loadbalancerId).actions;
init(); init();
@ -49,16 +53,40 @@
//////////////////////////////// ////////////////////////////////
function init() { function init() {
api.getListener($routeParams.listenerId).success(set('listener')); ctrl.listener = null;
api.getLoadBalancer($routeParams.loadbalancerId).success(set('loadbalancer')); ctrl.loadbalancer = null;
ctrl.loading = true;
ctrl.error = false;
$q.all([
api.getListener($routeParams.listenerId)
.then(success('listener'), fail('listener')),
api.getLoadBalancer($routeParams.loadbalancerId)
.then(success('loadbalancer'), fail('loadbalancer'))
]).then(postInit, initError);
} }
function set(property) { function success(property) {
return angular.bind(null, function setProp(property, value) { return angular.bind(null, function setProp(property, response) {
ctrl[property] = value; ctrl[property] = response.data;
}, property); }, property);
} }
function fail(property) {
return angular.bind(null, function setProp(property, error) {
ctrl[property] = null;
throw error;
}, property);
}
function postInit() {
ctrl.loading = false;
}
function initError() {
ctrl.loading = false;
ctrl.error = true;
}
} }
})(); })();

View File

@ -17,26 +17,40 @@
'use strict'; 'use strict';
describe('LBaaS v2 Listener Detail Controller', function() { describe('LBaaS v2 Listener Detail Controller', function() {
var lbaasv2API, ctrl; var lbaasv2API, $controller, apiFail, qAllFail;
function fakeAPI() { function fakePromise(data, reject) {
return { return {
success: function(callback) { then: function(success, fail) {
callback('foo'); if (reject) {
fail();
} else {
success({ data: data });
}
return fakePromise();
} }
}; };
} }
function fakeAPI() {
return fakePromise('foo', apiFail);
}
function loadbalancerAPI() { function loadbalancerAPI() {
var loadbalancer = { provisioning_status: 'ACTIVE' }; return fakePromise({ provisioning_status: 'ACTIVE' });
return { }
success: function(callback) {
callback(loadbalancer); function qAll() {
}, return fakePromise(null, qAllFail);
then: function(callback) { }
callback({ data: loadbalancer });
function createController() {
return $controller('ListenerDetailController', {
$routeParams: {
loadbalancerId: 'loadbalancerId',
listenerId: 'listenerId'
} }
}; });
} }
/////////////////////// ///////////////////////
@ -48,6 +62,10 @@
beforeEach(module('horizon.dashboard.project.lbaasv2')); beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function($provide) { beforeEach(module(function($provide) {
apiFail = false;
qAllFail = false;
$provide.value('$q', { all: qAll });
$provide.value('$uibModal', {}); $provide.value('$uibModal', {});
})); }));
@ -55,22 +73,32 @@
lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2'); lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2');
spyOn(lbaasv2API, 'getListener').and.callFake(fakeAPI); spyOn(lbaasv2API, 'getListener').and.callFake(fakeAPI);
spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(loadbalancerAPI); spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(loadbalancerAPI);
var controller = $injector.get('$controller'); $controller = $injector.get('$controller');
ctrl = controller('ListenerDetailController', {
$routeParams: {
loadbalancerId: 'loadbalancerId',
listenerId: 'listenerId'
}
});
})); }));
it('should invoke lbaasv2 apis', function() { it('should invoke lbaasv2 apis', function() {
var ctrl = createController();
expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId'); expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId');
expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('loadbalancerId'); expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('loadbalancerId');
expect(ctrl.loadbalancer).toEqual({ provisioning_status: 'ACTIVE' }); expect(ctrl.loadbalancer).toEqual({ provisioning_status: 'ACTIVE' });
expect(ctrl.listener).toBe('foo'); expect(ctrl.listener).toBe('foo');
}); });
it('should throw error on API fail', function() {
apiFail = true;
var init = function() {
createController();
};
expect(init).toThrow();
});
it('should set error state if any APIs fail', function() {
qAllFail = true;
var ctrl = createController();
expect(ctrl.loading).toBe(false);
expect(ctrl.error).toBe(true);
});
}); });
})(); })();

View File

@ -1,39 +1,42 @@
<div ng-controller="ListenerDetailController as ctrl"> <div ng-controller="ListenerDetailController as ctrl">
<div class="page-header"> <detail-status loading="ctrl.loading" error="ctrl.error"></detail-status>
<ol class="breadcrumb"> <div ng-if="!ctrl.loading && !ctrl.error">
<li><a href="project/ngloadbalancersv2/"><translate>Load Balancers</translate></a></li> <div class="page-header">
<li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}">{$ ::(ctrl.loadbalancer.name || ctrl.loadbalancer.id) $}</a></li> <ol class="breadcrumb">
<li class="active">{$ ::(ctrl.listener.name || ctrl.listener.id) $}</li> <li><a href="project/ngloadbalancersv2/"><translate>Load Balancers</translate></a></li>
<actions allowed="ctrl.actions" type="row" item="ctrl.listener" ng-if="ctrl.listener" <li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}">{$ ::(ctrl.loadbalancer.name || ctrl.loadbalancer.id) $}</a></li>
class="actions_column pull-right"></actions> <li class="active">{$ ::(ctrl.listener.name || ctrl.listener.id) $}</li>
</ol> <actions allowed="ctrl.actions" type="row" item="ctrl.listener" ng-if="ctrl.listener"
<p ng-if="::ctrl.listener.description">{$ ::ctrl.listener.description $}</p> class="actions_column pull-right"></actions>
</div> </ol>
<div class="row"> <p ng-if="::ctrl.listener.description">{$ ::ctrl.listener.description $}</p>
<div class="col-md-6 detail"> </div>
<dl class="dl-horizontal"> <div class="row">
<dt translate>Protocol</dt> <div class="col-md-6 detail">
<dd>{$ ::ctrl.listener.protocol $}</dd> <dl class="dl-horizontal">
<dt translate>Protocol Port</dt> <dt translate>Protocol</dt>
<dd>{$ ::ctrl.listener.protocol_port $}</dd> <dd>{$ ::ctrl.listener.protocol $}</dd>
<dt translate>Connection Limit</dt> <dt translate>Protocol Port</dt>
<dd>{$ ctrl.listener.connection_limit | limit $}</dd> <dd>{$ ::ctrl.listener.protocol_port $}</dd>
<dt translate>Admin State Up</dt> <dt translate>Connection Limit</dt>
<dd>{$ ctrl.listener.admin_state_up | yesno $}</dd> <dd>{$ ctrl.listener.connection_limit | limit $}</dd>
<dt translate>Default Pool ID</dt> <dt translate>Admin State Up</dt>
<dd> <dd>{$ ctrl.listener.admin_state_up | yesno $}</dd>
<a ng-href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}/pools/{$ ::ctrl.listener.default_pool_id $}" ng-if="ctrl.listener.default_pool_id"> <dt translate>Default Pool ID</dt>
{$ ::ctrl.listener.default_pool_id $} <dd>
</a> <a ng-href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}/pools/{$ ::ctrl.listener.default_pool_id $}" ng-if="ctrl.listener.default_pool_id">
<span ng-if="!ctrl.listener.default_pool_id"> {$ ::ctrl.listener.default_pool_id $}
{$ 'None' | translate $} </a>
</span> <span ng-if="!ctrl.listener.default_pool_id">
</dd> {$ 'None' | translate $}
<dt translate>Listener ID</dt> </span>
<dd>{$ ::ctrl.listener.id $}</dd> </dd>
<dt translate>Tenant ID</dt> <dt translate>Listener ID</dt>
<dd>{$ ::ctrl.listener.tenant_id $}</dd> <dd>{$ ::ctrl.listener.id $}</dd>
</dl> <dt translate>Tenant ID</dt>
<dd>{$ ::ctrl.listener.tenant_id $}</dd>
</dl>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -50,6 +50,8 @@
) { ) {
var ctrl = this; var ctrl = this;
ctrl.loading = true;
ctrl.error = false;
ctrl.actions = rowActions.actions; ctrl.actions = rowActions.actions;
ctrl.operatingStatus = loadBalancersService.operatingStatus; ctrl.operatingStatus = loadBalancersService.operatingStatus;
ctrl.provisioningStatus = loadBalancersService.provisioningStatus; ctrl.provisioningStatus = loadBalancersService.provisioningStatus;
@ -60,11 +62,21 @@
//////////////////////////////// ////////////////////////////////
function init() { function init() {
api.getLoadBalancer($routeParams.loadbalancerId, true).success(success); ctrl.loadbalancer = null;
ctrl.loading = true;
ctrl.error = false;
api.getLoadBalancer($routeParams.loadbalancerId, true).then(success, fail);
} }
function success(response) { function success(response) {
ctrl.loadbalancer = response; ctrl.loadbalancer = response.data;
ctrl.loading = false;
}
function fail(/*response*/) {
ctrl.loadbalancer = null;
ctrl.loading = false;
ctrl.error = true;
} }
// Save the active state of the listeners tab in the global window object so it can stay // Save the active state of the listeners tab in the global window object so it can stay

View File

@ -17,16 +17,28 @@
'use strict'; 'use strict';
describe('LBaaS v2 Load Balancer Detail Controller', function() { describe('LBaaS v2 Load Balancer Detail Controller', function() {
var lbaasv2API, ctrl, $scope, $window; var lbaasv2API, $scope, $window, $controller, apiFail;
function fakeAPI() { function fakeAPI() {
return { return {
success: function(callback) { then: function(success, fail) {
callback({ id: '1234' }); if (apiFail && fail) {
fail();
} else {
success({ id: '1234' });
}
} }
}; };
} }
function createController() {
return $controller('LoadBalancerDetailController', {
$scope: $scope,
$window: $window,
$routeParams: { loadbalancerId: '1234' }
});
}
/////////////////////// ///////////////////////
beforeEach(module('horizon.framework.widgets')); beforeEach(module('horizon.framework.widgets'));
@ -36,6 +48,7 @@
beforeEach(module('horizon.dashboard.project.lbaasv2')); beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function($provide) { beforeEach(module(function($provide) {
apiFail = false;
$provide.value('$uibModal', {}); $provide.value('$uibModal', {});
})); }));
@ -44,19 +57,16 @@
spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(fakeAPI); spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(fakeAPI);
$scope = $injector.get('$rootScope').$new(); $scope = $injector.get('$rootScope').$new();
$window = {}; $window = {};
var controller = $injector.get('$controller'); $controller = $injector.get('$controller');
ctrl = controller('LoadBalancerDetailController', {
$scope: $scope,
$window: $window,
$routeParams: { loadbalancerId: '1234' }
});
})); }));
it('should invoke lbaasv2 apis', function() { it('should invoke lbaasv2 apis', function() {
createController();
expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('1234', true); expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('1234', true);
}); });
it('should save changes to listeners tab active state', function() { it('should save changes to listeners tab active state', function() {
var ctrl = createController();
expect($window.listenersTabActive).toBeUndefined(); expect($window.listenersTabActive).toBeUndefined();
expect(ctrl.listenersTabActive).toBeUndefined(); expect(ctrl.listenersTabActive).toBeUndefined();
ctrl.listenersTabActive = true; ctrl.listenersTabActive = true;
@ -67,6 +77,13 @@
expect($window.listenersTabActive).toBe(false); expect($window.listenersTabActive).toBe(false);
}); });
it('should set error state', function() {
apiFail = true;
var ctrl = createController();
expect(ctrl.loading).toBe(false);
expect(ctrl.error).toBe(true);
});
}); });
})(); })();

View File

@ -1,54 +1,57 @@
<div ng-controller="LoadBalancerDetailController as ctrl"> <div ng-controller="LoadBalancerDetailController as ctrl">
<div class="page-header"> <detail-status loading="ctrl.loading" error="ctrl.error"></detail-status>
<ol class="breadcrumb"> <div ng-if="!ctrl.loading && !ctrl.error">
<li><a href="project/ngloadbalancersv2/"><translate>Load Balancers</translate></a></li> <div class="page-header">
<li class="active">{$ ctrl.loadbalancer.name || ctrl.loadbalancer.id $}</li> <ol class="breadcrumb">
<actions allowed="ctrl.actions" type="row" item="ctrl.loadbalancer" <li><a href="project/ngloadbalancersv2/"><translate>Load Balancers</translate></a></li>
ng-if="ctrl.loadbalancer" class="actions_column pull-right"></actions> <li class="active">{$ ctrl.loadbalancer.name || ctrl.loadbalancer.id $}</li>
</ol> <actions allowed="ctrl.actions" type="row" item="ctrl.loadbalancer"
<p ng-if="::ctrl.loadbalancer.description">{$ ::ctrl.loadbalancer.description $}</p> ng-if="ctrl.loadbalancer" class="actions_column pull-right"></actions>
<ul class="list-inline"> </ol>
<li> <p ng-if="::ctrl.loadbalancer.description">{$ ::ctrl.loadbalancer.description $}</p>
<strong translate>IP Address</strong> <ul class="list-inline">
{$ ::ctrl.loadbalancer.vip_address $} <li>
</li> <strong translate>IP Address</strong>
<li> {$ ::ctrl.loadbalancer.vip_address $}
<strong translate>Operating Status</strong> </li>
{$ ctrl.loadbalancer.operating_status | decode:ctrl.operatingStatus $} <li>
</li> <strong translate>Operating Status</strong>
<li> {$ ctrl.loadbalancer.operating_status | decode:ctrl.operatingStatus $}
<strong translate>Provisioning Status</strong> </li>
{$ ctrl.loadbalancer.provisioning_status | decode:ctrl.provisioningStatus $} <li>
</li> <strong translate>Provisioning Status</strong>
</ul> {$ ctrl.loadbalancer.provisioning_status | decode:ctrl.provisioningStatus $}
</div> </li>
<tabset> </ul>
<tab heading="{$ 'Overview' | translate $}"> </div>
<div class="row"> <tabset>
<div class="col-md-6 detail"> <tab heading="{$ 'Overview' | translate $}">
<dl class="dl-horizontal"> <div class="row">
<dt translate>Provider</dt> <div class="col-md-6 detail">
<dd>{$ ::ctrl.loadbalancer.provider $}</dd> <dl class="dl-horizontal">
<dt translate>Admin State Up</dt> <dt translate>Provider</dt>
<dd>{$ ctrl.loadbalancer.admin_state_up | yesno $}</dd> <dd>{$ ::ctrl.loadbalancer.provider $}</dd>
<dt translate ng-if="ctrl.loadbalancer.floating_ip !== undefined">Floating IP Address</dt> <dt translate>Admin State Up</dt>
<dd ng-if="ctrl.loadbalancer.floating_ip !== undefined">{$ ctrl.loadbalancer.floating_ip.ip | noValue:('None' | translate) $}</dd> <dd>{$ ctrl.loadbalancer.admin_state_up | yesno $}</dd>
<dt translate>Load Balancer ID</dt> <dt translate ng-if="ctrl.loadbalancer.floating_ip !== undefined">Floating IP Address</dt>
<dd>{$ ::ctrl.loadbalancer.id $}</dd> <dd ng-if="ctrl.loadbalancer.floating_ip !== undefined">{$ ctrl.loadbalancer.floating_ip.ip | noValue:('None' | translate) $}</dd>
<dt translate>Subnet ID</dt> <dt translate>Load Balancer ID</dt>
<dd> <dd>{$ ::ctrl.loadbalancer.id $}</dd>
<a target="_self" ng-href="project/networks/subnets/{$ ::ctrl.loadbalancer.vip_subnet_id $}/detail">{$ ::ctrl.loadbalancer.vip_subnet_id $}</a> <dt translate>Subnet ID</dt>
</dd> <dd>
<dt translate>Port ID</dt> <a target="_self" ng-href="project/networks/subnets/{$ ::ctrl.loadbalancer.vip_subnet_id $}/detail">{$ ::ctrl.loadbalancer.vip_subnet_id $}</a>
<dd> </dd>
<a target="_self" ng-href="project/networks/ports/{$ ::ctrl.loadbalancer.vip_port_id $}/detail">{$ ::ctrl.loadbalancer.vip_port_id $}</a> <dt translate>Port ID</dt>
</dd> <dd>
</dl> <a target="_self" ng-href="project/networks/ports/{$ ::ctrl.loadbalancer.vip_port_id $}/detail">{$ ::ctrl.loadbalancer.vip_port_id $}</a>
</dd>
</dl>
</div>
</div> </div>
</div> </tab>
</tab> <tab heading="{$ 'Listeners' | translate $}" active="ctrl.listenersTabActive">
<tab heading="{$ 'Listeners' | translate $}" active="ctrl.listenersTabActive"> <ng-include src="'static/dashboard/project/lbaasv2/listeners/table.html'"></ng-include>
<ng-include src="'static/dashboard/project/lbaasv2/listeners/table.html'"></ng-include> </tab>
</tab> </tabset>
</tabset> </div>
</div> </div>

View File

@ -24,6 +24,7 @@
'horizon.app.core.openstack-service-api.lbaasv2', 'horizon.app.core.openstack-service-api.lbaasv2',
'horizon.dashboard.project.lbaasv2.members.actions.rowActions', 'horizon.dashboard.project.lbaasv2.members.actions.rowActions',
'$routeParams', '$routeParams',
'$q',
'horizon.dashboard.project.lbaasv2.loadbalancers.service', 'horizon.dashboard.project.lbaasv2.loadbalancers.service',
'horizon.dashboard.project.lbaasv2.members.service' 'horizon.dashboard.project.lbaasv2.members.service'
]; ];
@ -38,16 +39,19 @@
* @param api The LBaaS v2 API service. * @param api The LBaaS v2 API service.
* @param rowActions The pool members row actions service. * @param rowActions The pool members row actions service.
* @param $routeParams The angular $routeParams service. * @param $routeParams The angular $routeParams service.
* @param $q The angular service for promises.
* @param loadBalancersService The LBaaS v2 load balancers service. * @param loadBalancersService The LBaaS v2 load balancers service.
* @param membersService The LBaaS v2 members service. * @param membersService The LBaaS v2 members service.
* @returns undefined * @returns undefined
*/ */
function MemberDetailController( function MemberDetailController(
api, rowActions, $routeParams, loadBalancersService, membersService api, rowActions, $routeParams, $q, loadBalancersService, membersService
) { ) {
var ctrl = this; var ctrl = this;
ctrl.loading = true;
ctrl.error = false;
ctrl.actions = rowActions.init($routeParams.loadbalancerId, $routeParams.poolId).actions; ctrl.actions = rowActions.init($routeParams.loadbalancerId, $routeParams.poolId).actions;
ctrl.loadbalancerId = $routeParams.loadbalancerId; ctrl.loadbalancerId = $routeParams.loadbalancerId;
ctrl.listenerId = $routeParams.listenerId; ctrl.listenerId = $routeParams.listenerId;
@ -60,25 +64,53 @@
//////////////////////////////// ////////////////////////////////
function init() { function init() {
api.getMember($routeParams.poolId, $routeParams.memberId).success(memberSuccess); ctrl.member = null;
api.getPool($routeParams.poolId).success(set('pool')); ctrl.pool = null;
api.getListener($routeParams.listenerId).success(set('listener')); ctrl.listener = null;
api.getLoadBalancer($routeParams.loadbalancerId).success(set('loadbalancer')); ctrl.loadbalancer = null;
ctrl.loading = true;
ctrl.error = false;
$q.all([
api.getMember($routeParams.poolId, $routeParams.memberId)
.then(success('member'), fail('member')),
api.getPool($routeParams.poolId)
.then(success('pool'), fail('pool')),
api.getListener($routeParams.listenerId)
.then(success('listener'), fail('listener')),
api.getLoadBalancer($routeParams.loadbalancerId)
.then(success('loadbalancer'), fail('loadbalancer'))
]).then(postInit, initError);
} }
function set(property) { function success(property) {
return angular.bind(null, function setProp(property, value) { return angular.bind(null, function setProp(property, response) {
ctrl[property] = value; ctrl[property] = response.data;
if (property === 'member') {
membersService.associateMemberStatuses(
ctrl.loadbalancerId,
ctrl.listenerId,
ctrl.poolId,
[ctrl.member]);
}
}, property); }, property);
} }
function memberSuccess(response) { function fail(property) {
ctrl.member = response; return angular.bind(null, function setProp(property, error) {
membersService.associateMemberStatuses( ctrl[property] = null;
ctrl.loadbalancerId, throw error;
ctrl.listenerId, }, property);
ctrl.poolId, }
[ctrl.member]);
function postInit() {
ctrl.loading = false;
}
function initError() {
ctrl.loading = false;
ctrl.error = true;
} }
} }

View File

@ -17,26 +17,42 @@
'use strict'; 'use strict';
describe('LBaaS v2 Member Detail Controller', function() { describe('LBaaS v2 Member Detail Controller', function() {
var controller, lbaasv2API, membersService, ctrl, actions; var $controller, lbaasv2API, membersService, apiFail, qAllFail, actions;
function fakeAPI() { function fakePromise(data, reject) {
return { return {
success: function(callback) { then: function(success, fail) {
callback('foo'); if (reject) {
fail();
} else {
success({ data: data });
}
return fakePromise();
} }
}; };
} }
function fakeAPI() {
return fakePromise('foo', apiFail);
}
function loadbalancerAPI() { function loadbalancerAPI() {
var loadbalancer = { provisioning_status: 'ACTIVE' }; return fakePromise({ provisioning_status: 'ACTIVE' });
return { }
success: function(callback) {
callback(loadbalancer); function qAll() {
}, return fakePromise(null, qAllFail);
then: function(callback) { }
callback({ data: loadbalancer });
function createController() {
return $controller('MemberDetailController', {
$routeParams: {
loadbalancerId: 'loadbalancerId',
listenerId: 'listenerId',
poolId: 'poolId',
memberId: 'memberId'
} }
}; });
} }
/////////////////////// ///////////////////////
@ -48,6 +64,10 @@
beforeEach(module('horizon.dashboard.project.lbaasv2')); beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function($provide) { beforeEach(module(function($provide) {
apiFail = false;
qAllFail = false;
$provide.value('$q', { all: qAll });
$provide.value('$uibModal', {}); $provide.value('$uibModal', {});
$provide.value('horizon.dashboard.project.lbaasv2.members.actions.rowActions', { $provide.value('horizon.dashboard.project.lbaasv2.members.actions.rowActions', {
init: function() { init: function() {
@ -68,25 +88,15 @@
spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(loadbalancerAPI); spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(loadbalancerAPI);
spyOn(actions, 'init').and.callThrough(); spyOn(actions, 'init').and.callThrough();
spyOn(membersService, 'associateMemberStatuses'); spyOn(membersService, 'associateMemberStatuses');
controller = $injector.get('$controller'); $controller = $injector.get('$controller');
ctrl = controller('MemberDetailController', {
$routeParams: {
loadbalancerId: 'loadbalancerId',
listenerId: 'listenerId',
poolId: 'poolId',
memberId: 'memberId'
}
});
})); }));
it('should invoke lbaasv2 apis', function() { it('should invoke lbaasv2 apis', function() {
var ctrl = createController();
expect(lbaasv2API.getMember).toHaveBeenCalledWith('poolId','memberId'); expect(lbaasv2API.getMember).toHaveBeenCalledWith('poolId','memberId');
expect(lbaasv2API.getPool).toHaveBeenCalledWith('poolId'); expect(lbaasv2API.getPool).toHaveBeenCalledWith('poolId');
expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId'); expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId');
expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('loadbalancerId'); expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('loadbalancerId');
});
it('should initialize the controller properties correctly', function() {
expect(ctrl.loadbalancerId).toBeDefined(); expect(ctrl.loadbalancerId).toBeDefined();
expect(ctrl.listenerId).toBeDefined(); expect(ctrl.listenerId).toBeDefined();
expect(ctrl.poolId).toBeDefined(); expect(ctrl.poolId).toBeDefined();
@ -97,10 +107,26 @@
}); });
it('should invoke the "associateMemberStatuses" method', function() { it('should invoke the "associateMemberStatuses" method', function() {
var ctrl = createController();
expect(membersService.associateMemberStatuses).toHaveBeenCalledWith( expect(membersService.associateMemberStatuses).toHaveBeenCalledWith(
ctrl.loadbalancerId, ctrl.listenerId, ctrl.poolId, [ctrl.member]); ctrl.loadbalancerId, ctrl.listenerId, ctrl.poolId, [ctrl.member]);
}); });
it('should throw error on API fail', function() {
apiFail = true;
var init = function() {
createController();
};
expect(init).toThrow();
});
it('should set error state if any APIs fail', function() {
qAllFail = true;
var ctrl = createController();
expect(ctrl.loading).toBe(false);
expect(ctrl.error).toBe(true);
});
}); });
})(); })();

View File

@ -1,35 +1,38 @@
<div ng-controller="MemberDetailController as ctrl"> <div ng-controller="MemberDetailController as ctrl">
<div class="page-header"> <detail-status loading="ctrl.loading" error="ctrl.error"></detail-status>
<ol class="breadcrumb"> <div ng-if="!ctrl.loading && !ctrl.error">
<li><a href="project/ngloadbalancersv2/"><translate>Load Balancers</translate></a></li> <div class="page-header">
<li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}">{$ ::(ctrl.loadbalancer.name || ctrl.loadbalancer.id) $}</a></li> <ol class="breadcrumb">
<li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}">{$ ::(ctrl.listener.name || ctrl.listener.id) $}</a></li> <li><a href="project/ngloadbalancersv2/"><translate>Load Balancers</translate></a></li>
<li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}/pools/{$ ::ctrl.pool.id $}">{$ ::(ctrl.pool.name || ctrl.pool.id) $}</a></li> <li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}">{$ ::(ctrl.loadbalancer.name || ctrl.loadbalancer.id) $}</a></li>
<li class="active">{$ ::(ctrl.member.name || ctrl.member.id) $}</li> <li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}">{$ ::(ctrl.listener.name || ctrl.listener.id) $}</a></li>
<actions allowed="ctrl.actions" type="row" item="ctrl.member" <li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}/pools/{$ ::ctrl.pool.id $}">{$ ::(ctrl.pool.name || ctrl.pool.id) $}</a></li>
ng-if="ctrl.member" class="actions_column pull-right"></actions> <li class="active">{$ ::(ctrl.member.name || ctrl.member.id) $}</li>
</ol> <actions allowed="ctrl.actions" type="row" item="ctrl.member"
</div> ng-if="ctrl.member" class="actions_column pull-right"></actions>
<div class="row"> </ol>
<div class="col-md-6 detail"> </div>
<dl class="dl-horizontal"> <div class="row">
<dt translate>Address</dt> <div class="col-md-6 detail">
<dd>{$ ::ctrl.member.address $}</dd> <dl class="dl-horizontal">
<dt translate>Protocol Port</dt> <dt translate>Address</dt>
<dd>{$ ::ctrl.member.protocol_port $}</dd> <dd>{$ ::ctrl.member.address $}</dd>
<dt translate>Weight</dt> <dt translate>Protocol Port</dt>
<dd>{$ ctrl.member.weight $}</dd> <dd>{$ ::ctrl.member.protocol_port $}</dd>
<dt translate>Operating Status</dt> <dt translate>Weight</dt>
<dd>{$ ctrl.member.operating_status | decode:ctrl.operatingStatus $}</dd> <dd>{$ ctrl.member.weight $}</dd>
<dt translate>Provisioning Status</dt> <dt translate>Operating Status</dt>
<dd>{$ ctrl.member.provisioning_status | decode:ctrl.provisioningStatus $}</dd> <dd>{$ ctrl.member.operating_status | decode:ctrl.operatingStatus $}</dd>
<dt translate>Admin State Up</dt> <dt translate>Provisioning Status</dt>
<dd>{$ ctrl.member.admin_state_up | yesno $}</dd> <dd>{$ ctrl.member.provisioning_status | decode:ctrl.provisioningStatus $}</dd>
<dt translate>Member ID</dt> <dt translate>Admin State Up</dt>
<dd>{$ ::ctrl.member.id $}</dd> <dd>{$ ctrl.member.admin_state_up | yesno $}</dd>
<dt translate>Tenant ID</dt> <dt translate>Member ID</dt>
<dd>{$ ::ctrl.member.tenant_id $}</dd> <dd>{$ ::ctrl.member.id $}</dd>
</dl> <dt translate>Tenant ID</dt>
<dd>{$ ::ctrl.member.tenant_id $}</dd>
</dl>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -26,7 +26,8 @@
'$routeParams', '$routeParams',
'horizon.framework.util.i18n.gettext', 'horizon.framework.util.i18n.gettext',
'$window', '$window',
'$scope' '$scope',
'$q'
]; ];
/** /**
@ -42,12 +43,15 @@
* @param gettext The horizon gettext function for translation. * @param gettext The horizon gettext function for translation.
* @param $window Angular's reference to the browser window object. * @param $window Angular's reference to the browser window object.
* @param $scope The angular scope object. * @param $scope The angular scope object.
* @param $q The angular service for promises.
* @returns undefined * @returns undefined
*/ */
function PoolDetailController(api, rowActions, $routeParams, gettext, $window, $scope) { function PoolDetailController(api, rowActions, $routeParams, gettext, $window, $scope, $q) {
var ctrl = this; var ctrl = this;
ctrl.loading = true;
ctrl.error = false;
ctrl.loadBalancerAlgorithm = { ctrl.loadBalancerAlgorithm = {
ROUND_ROBIN: gettext('Round Robin'), ROUND_ROBIN: gettext('Round Robin'),
LEAST_CONNECTIONS: gettext('Least Connections'), LEAST_CONNECTIONS: gettext('Least Connections'),
@ -61,17 +65,43 @@
//////////////////////////////// ////////////////////////////////
function init() { function init() {
api.getPool($routeParams.poolId).success(set('pool')); ctrl.pool = null;
api.getListener($routeParams.listenerId).success(set('listener')); ctrl.listener = null;
api.getLoadBalancer($routeParams.loadbalancerId).success(set('loadbalancer')); ctrl.loadbalancer = null;
ctrl.loading = true;
ctrl.error = false;
$q.all([
api.getPool($routeParams.poolId)
.then(success('pool'), fail('pool')),
api.getListener($routeParams.listenerId)
.then(success('listener'), fail('listener')),
api.getLoadBalancer($routeParams.loadbalancerId)
.then(success('loadbalancer'), fail('loadbalancer'))
]).then(postInit, initError);
} }
function set(property) { function success(property) {
return angular.bind(null, function setProp(property, value) { return angular.bind(null, function setProp(property, response) {
ctrl[property] = value; ctrl[property] = response.data;
}, property); }, property);
} }
function fail(property) {
return angular.bind(null, function setProp(property, error) {
ctrl[property] = null;
throw error;
}, property);
}
function postInit() {
ctrl.loading = false;
}
function initError() {
ctrl.loading = false;
ctrl.error = true;
}
// Save the active state of the members tab in the global window object so it can stay // Save the active state of the members tab in the global window object so it can stay
// active after reloading the route following an action. // active after reloading the route following an action.
$scope.$watch(function() { $scope.$watch(function() {

View File

@ -17,26 +17,43 @@
'use strict'; 'use strict';
describe('LBaaS v2 Pool Detail Controller', function() { describe('LBaaS v2 Pool Detail Controller', function() {
var lbaasv2API, ctrl, $scope, $window; var lbaasv2API, $scope, $window, $controller, apiFail, qAllFail;
function fakeAPI() { function fakePromise(data, reject) {
return { return {
success: function(callback) { then: function(success, fail) {
callback('foo'); if (reject) {
fail();
} else {
success({ data: data });
}
return fakePromise();
} }
}; };
} }
function fakeAPI() {
return fakePromise('foo', apiFail);
}
function loadbalancerAPI() { function loadbalancerAPI() {
var loadbalancer = { provisioning_status: 'ACTIVE' }; return fakePromise({ provisioning_status: 'ACTIVE' });
return { }
success: function(callback) {
callback(loadbalancer); function qAll() {
}, return fakePromise(null, qAllFail);
then: function(callback) { }
callback({ data: loadbalancer });
function createController() {
return $controller('PoolDetailController', {
$scope: $scope,
$window: $window,
$routeParams: {
loadbalancerId: 'loadbalancerId',
listenerId: 'listenerId',
poolId: 'poolId'
} }
}; });
} }
/////////////////////// ///////////////////////
@ -47,6 +64,13 @@
beforeEach(module('horizon.app.core.openstack-service-api')); beforeEach(module('horizon.app.core.openstack-service-api'));
beforeEach(module('horizon.dashboard.project.lbaasv2')); beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(module(function($provide) {
apiFail = false;
qAllFail = false;
$provide.value('$q', { all: qAll });
}));
beforeEach(inject(function($injector) { beforeEach(inject(function($injector) {
lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2'); lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2');
spyOn(lbaasv2API, 'getPool').and.callFake(fakeAPI); spyOn(lbaasv2API, 'getPool').and.callFake(fakeAPI);
@ -54,19 +78,11 @@
spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(loadbalancerAPI); spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(loadbalancerAPI);
$scope = $injector.get('$rootScope').$new(); $scope = $injector.get('$rootScope').$new();
$window = {}; $window = {};
var controller = $injector.get('$controller'); $controller = $injector.get('$controller');
ctrl = controller('PoolDetailController', {
$scope: $scope,
$window: $window,
$routeParams: {
loadbalancerId: 'loadbalancerId',
listenerId: 'listenerId',
poolId: 'poolId'
}
});
})); }));
it('should invoke lbaasv2 apis', function() { it('should invoke lbaasv2 apis', function() {
var ctrl = createController();
expect(lbaasv2API.getPool).toHaveBeenCalledWith('poolId'); expect(lbaasv2API.getPool).toHaveBeenCalledWith('poolId');
expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId'); expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId');
expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('loadbalancerId'); expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('loadbalancerId');
@ -76,10 +92,12 @@
}); });
it('should define mapping for the load balancer algorithm', function() { it('should define mapping for the load balancer algorithm', function() {
var ctrl = createController();
expect(ctrl.loadBalancerAlgorithm).toBeDefined(); expect(ctrl.loadBalancerAlgorithm).toBeDefined();
}); });
it('should save changes to members tab active state', function() { it('should save changes to members tab active state', function() {
var ctrl = createController();
expect($window.membersTabActive).toBeUndefined(); expect($window.membersTabActive).toBeUndefined();
expect(ctrl.membersTabActive).toBeUndefined(); expect(ctrl.membersTabActive).toBeUndefined();
ctrl.membersTabActive = true; ctrl.membersTabActive = true;
@ -90,6 +108,21 @@
expect($window.membersTabActive).toBe(false); expect($window.membersTabActive).toBe(false);
}); });
it('should throw error on API fail', function() {
apiFail = true;
var init = function() {
createController();
};
expect(init).toThrow();
});
it('should set error state if any APIs fail', function() {
qAllFail = true;
var ctrl = createController();
expect(ctrl.loading).toBe(false);
expect(ctrl.error).toBe(true);
});
}); });
})(); })();

View File

@ -1,47 +1,50 @@
<div ng-controller="PoolDetailController as ctrl"> <div ng-controller="PoolDetailController as ctrl">
<div class="page-header"> <detail-status loading="ctrl.loading" error="ctrl.error"></detail-status>
<ol class="breadcrumb"> <div ng-if="!ctrl.loading && !ctrl.error">
<li><a href="project/ngloadbalancersv2/"><translate>Load Balancers</translate></a></li> <div class="page-header">
<li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}">{$ ::(ctrl.loadbalancer.name || ctrl.loadbalancer.id) $}</a></li> <ol class="breadcrumb">
<li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}">{$ ::(ctrl.listener.name || ctrl.listener.id) $}</a></li> <li><a href="project/ngloadbalancersv2/"><translate>Load Balancers</translate></a></li>
<li class="active">{$ ::(ctrl.pool.name || ctrl.pool.id) $}</li> <li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}">{$ ::(ctrl.loadbalancer.name || ctrl.loadbalancer.id) $}</a></li>
<actions allowed="ctrl.actions" type="row" item="ctrl.pool" <li><a href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}">{$ ::(ctrl.listener.name || ctrl.listener.id) $}</a></li>
ng-if="ctrl.pool" class="actions_column pull-right"></actions> <li class="active">{$ ::(ctrl.pool.name || ctrl.pool.id) $}</li>
</ol> <actions allowed="ctrl.actions" type="row" item="ctrl.pool"
<p ng-if="::ctrl.pool.description">{$ ::ctrl.pool.description $}</p> ng-if="ctrl.pool" class="actions_column pull-right"></actions>
</div> </ol>
<tabset> <p ng-if="::ctrl.pool.description">{$ ::ctrl.pool.description $}</p>
<tab heading="{$ 'Overview' | translate $}"> </div>
<div class="row"> <tabset>
<div class="col-md-6 detail"> <tab heading="{$ 'Overview' | translate $}">
<dl class="dl-horizontal"> <div class="row">
<dt translate>Protocol</dt> <div class="col-md-6 detail">
<dd>{$ ::ctrl.pool.protocol $}</dd> <dl class="dl-horizontal">
<dt translate>Load Balancer Algorithm</dt> <dt translate>Protocol</dt>
<dd>{$ ctrl.pool.lb_algorithm | decode:ctrl.loadBalancerAlgorithm $}</dd> <dd>{$ ::ctrl.pool.protocol $}</dd>
<dt translate>Session Persistence</dt> <dt translate>Load Balancer Algorithm</dt>
<dd>{$ ctrl.pool.session_persistence | noValue:('None' | translate) $}</dd> <dd>{$ ctrl.pool.lb_algorithm | decode:ctrl.loadBalancerAlgorithm $}</dd>
<dt translate>Admin State Up</dt> <dt translate>Session Persistence</dt>
<dd>{$ ctrl.pool.admin_state_up | yesno $}</dd> <dd>{$ ctrl.pool.session_persistence | noValue:('None' | translate) $}</dd>
<dt translate>Health Monitor ID</dt> <dt translate>Admin State Up</dt>
<dd> <dd>{$ ctrl.pool.admin_state_up | yesno $}</dd>
<a ng-href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}/pools/{$ ::ctrl.pool.id $}/healthmonitors/{$ ::ctrl.pool.healthmonitor_id $}" ng-if="ctrl.pool.healthmonitor_id"> <dt translate>Health Monitor ID</dt>
{$ ::ctrl.pool.healthmonitor_id $} <dd>
</a> <a ng-href="project/ngloadbalancersv2/{$ ::ctrl.loadbalancer.id $}/listeners/{$ ::ctrl.listener.id $}/pools/{$ ::ctrl.pool.id $}/healthmonitors/{$ ::ctrl.pool.healthmonitor_id $}" ng-if="ctrl.pool.healthmonitor_id">
<span ng-if="!ctrl.pool.healthmonitor_id"> {$ ::ctrl.pool.healthmonitor_id $}
{$ 'None' | translate $} </a>
</span> <span ng-if="!ctrl.pool.healthmonitor_id">
</dd> {$ 'None' | translate $}
<dt translate>Pool ID</dt> </span>
<dd>{$ ::ctrl.pool.id $}</dd> </dd>
<dt translate>Tenant ID</dt> <dt translate>Pool ID</dt>
<dd>{$ ::ctrl.pool.tenant_id $}</dd> <dd>{$ ::ctrl.pool.id $}</dd>
</dl> <dt translate>Tenant ID</dt>
<dd>{$ ::ctrl.pool.tenant_id $}</dd>
</dl>
</div>
</div> </div>
</div> </tab>
</tab> <tab heading="{$ 'Members' | translate $}" active="ctrl.membersTabActive">
<tab heading="{$ 'Members' | translate $}" active="ctrl.membersTabActive"> <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>
</tab> </tabset>
</tabset> </div>
</div> </div>

View File

@ -0,0 +1,53 @@
/*
* 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')
.directive('detailStatus', detailStatus);
detailStatus.$inject = [
'horizon.dashboard.project.lbaasv2.basePath'
];
/**
* @ngdoc directive
* @name horizon.dashboard.project.lbaasv2:detailStatus
* @description
* The `detailStatus` directive provides a status indicator while loading detail pages. It will
* show a loading indicator while the page is loading and an error indicator if there is an
* error loading the page.
* @restrict E
*
* @example
* ```
* <detail-status loading="ctrl.loading" error="ctrl.error"></detail-status>
* ```
*/
function detailStatus(basePath) {
var directive = {
restrict: 'E',
templateUrl: basePath + 'widgets/detail/detail-status.html',
scope: {
loading: '=',
error: '='
}
};
return directive;
}
}());

View File

@ -0,0 +1,87 @@
/*
* 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';
function digestMarkup(scope, compile, markup) {
var element = angular.element(markup);
compile(element)(scope);
scope.$apply();
return element;
}
describe('detailStatus directive', function() {
var $scope, $compile, markup, ctrl;
beforeEach(module('templates'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(inject(function($injector) {
$compile = $injector.get('$compile');
$scope = $injector.get('$rootScope').$new();
ctrl = {
loading: true,
error: false
};
$scope.ctrl = ctrl;
markup = '<detail-status loading="ctrl.loading" error="ctrl.error"></detail-status>';
}));
it('initially shows loading status', function() {
var element = digestMarkup($scope, $compile, markup);
expect(element).toBeDefined();
expect(element.children().length).toBe(1);
expect(element.find('.progress-bar').hasClass('progress-bar-striped')).toBe(true);
expect(element.find('.progress-bar').hasClass('progress-bar-danger')).toBe(false);
expect(element.find('.progress-bar > span').length).toBe(1);
expect(element.find('.progress-bar > span').hasClass('sr-only')).toBe(true);
expect(element.find('.progress-bar > span').text().trim()).toBe('Loading');
expect(element.find('.message').length).toBe(0);
});
it('indicates error status on error', function() {
var element = digestMarkup($scope, $compile, markup);
expect(element).toBeDefined();
ctrl.loading = false;
ctrl.error = true;
$scope.$apply();
expect(element.children().length).toBe(1);
expect(element.find('.progress-bar').hasClass('progress-bar-striped')).toBe(false);
expect(element.find('.progress-bar').hasClass('progress-bar-danger')).toBe(true);
expect(element.find('.progress-bar > span').length).toBe(1);
expect(element.find('.progress-bar > span').hasClass('sr-only')).toBe(false);
expect(element.find('.progress-bar > span').text().trim())
.toBe('An error occurred. Please try again later.');
expect(element.find('.error-actions').length).toBe(1);
expect(element.find('.error-actions > a').text().trim()).toBe('Back');
});
it('goes away when done loading', function() {
var element = digestMarkup($scope, $compile, markup);
expect(element).toBeDefined();
ctrl.loading = false;
ctrl.error = false;
$scope.$apply();
expect(element.children().length).toBe(0);
});
});
}());

View File

@ -0,0 +1,12 @@
<div ng-if="loading || error">
<div class="progress">
<div class="progress-bar" role="progressbar"
ng-class="{ 'progress-bar-striped active': !error, 'progress-bar-danger': error }">
<span ng-if="!error" class="sr-only" translate>Loading</span>
<span ng-if="error" translate>An error occurred. Please try again later.</span>
</div>
</div>
<div class="error-actions" ng-if="error">
<a href="javascript:window.history.back();" translate>Back</a>
</div>
</div>