Indicate table loading, error, and empty states

This adds a loading indicator to the tables so the user can see when
items are being loaded or there was an error loading the items. It
also adds the "No items to display" message if there are no items.

Closes-Bug: #1560541
Change-Id: I3f53e22e2899a562962d4a14734b23ad4f18ae29
This commit is contained in:
Justin Pomeroy 2016-03-21 14:34:16 -05:00
parent c68b80b0bb
commit 19fdae7029
13 changed files with 282 additions and 21 deletions

View File

@ -74,3 +74,15 @@
}
}
}
/* Progress indicator in the table while items are loading */
[table-status] {
.progress {
margin: 0px auto;
width: 25%;
height: $line-height-computed;
.progress-bar {
width: 100%;
}
}
}

View File

@ -46,6 +46,8 @@
var ctrl = this;
ctrl.items = [];
ctrl.src = [];
ctrl.loading = true;
ctrl.error = false;
ctrl.checked = {};
ctrl.loadbalancerId = $routeParams.loadbalancerId;
ctrl.batchActions = batchActions.init(ctrl.loadbalancerId);
@ -56,11 +58,21 @@
////////////////////////////////
function init() {
api.getListeners(ctrl.loadbalancerId).success(success);
ctrl.src = [];
ctrl.loading = true;
ctrl.error = false;
api.getListeners(ctrl.loadbalancerId).then(success, fail);
}
function success(response) {
ctrl.src = response.items;
ctrl.src = response.data.items;
ctrl.loading = false;
}
function fail(/*response*/) {
ctrl.src = [];
ctrl.loading = false;
ctrl.error = true;
}
}

View File

@ -18,12 +18,17 @@
describe('LBaaS v2 Listeners Table Controller', function() {
var controller, lbaasv2API, rowActions, batchActions;
var items = [];
var items = [{ foo: 'bar' }];
var apiFail = false;
function fakeAPI() {
return {
success: function(callback) {
callback({ items: items });
then: function(success, fail) {
if (apiFail && fail) {
fail();
} else {
success({ data: { items: items } });
}
}
};
}
@ -64,6 +69,8 @@
var ctrl = createController();
expect(ctrl.items).toEqual([]);
expect(ctrl.src).toEqual(items);
expect(ctrl.loading).toBe(false);
expect(ctrl.error).toBe(false);
expect(ctrl.checked).toEqual({});
expect(ctrl.loadbalancerId).toEqual('1234');
expect(rowActions.init).toHaveBeenCalledWith(ctrl.loadbalancerId);
@ -74,13 +81,16 @@
});
it('should invoke lbaasv2 apis', function() {
createController();
var ctrl = createController();
expect(lbaasv2API.getListeners).toHaveBeenCalled();
expect(ctrl.src.length).toBe(1);
});
it('should init the rowactions', function() {
createController();
expect(lbaasv2API.getListeners).toHaveBeenCalled();
it('should show error if loading fails', function() {
apiFail = true;
var ctrl = createController();
expect(ctrl.src.length).toBe(0);
expect(ctrl.error).toBe(true);
});
});

View File

@ -112,6 +112,8 @@
</td>
</tr>
<tr table-status table="table" column-count="7"></tr>
</tbody>
<!--
Table-footer:

View File

@ -46,6 +46,8 @@
var ctrl = this;
ctrl.items = [];
ctrl.src = [];
ctrl.loading = true;
ctrl.error = false;
ctrl.checked = {};
ctrl.batchActions = batchActions;
ctrl.rowActions = rowActions;
@ -57,11 +59,20 @@
////////////////////////////////
function init() {
api.getLoadBalancers(true).success(success);
ctrl.src = [];
ctrl.loading = true;
api.getLoadBalancers(true).then(success, fail);
}
function success(response) {
ctrl.src = response.items;
ctrl.src = response.data.items;
ctrl.loading = false;
}
function fail(/*response*/) {
ctrl.src = [];
ctrl.error = true;
ctrl.loading = false;
}
}

View File

@ -18,12 +18,17 @@
describe('LBaaS v2 Load Balancers Table Controller', function() {
var controller, lbaasv2API, scope;
var items = [];
var items = [{ foo: 'bar' }];
var apiFail = false;
function fakeAPI() {
return {
success: function(callback) {
callback({ items: items });
then: function(success, fail) {
if (apiFail && fail) {
fail();
} else {
success({ data: { items: items } });
}
}
};
}
@ -55,6 +60,8 @@
var ctrl = createController();
expect(ctrl.items).toEqual([]);
expect(ctrl.src).toEqual(items);
expect(ctrl.loading).toBe(false);
expect(ctrl.error).toBe(false);
expect(ctrl.checked).toEqual({});
expect(ctrl.batchActions).toBeDefined();
expect(ctrl.rowActions).toBeDefined();
@ -63,8 +70,16 @@
});
it('should invoke lbaasv2 apis', function() {
createController();
var ctrl = createController();
expect(lbaasv2API.getLoadBalancers).toHaveBeenCalled();
expect(ctrl.src.length).toBe(1);
});
it('should show error if loading fails', function() {
apiFail = true;
var ctrl = createController();
expect(ctrl.src.length).toBe(0);
expect(ctrl.error).toBe(true);
});
});

View File

@ -143,6 +143,8 @@
</td>
</tr>
<tr table-status table="table" column-count="9"></tr>
</tbody>
<!--
Table-footer:

View File

@ -46,6 +46,8 @@
var ctrl = this;
ctrl.items = [];
ctrl.src = [];
ctrl.loading = true;
ctrl.error = false;
ctrl.checked = {};
ctrl.loadbalancerId = $routeParams.loadbalancerId;
ctrl.listenerId = $routeParams.listenerId;
@ -58,11 +60,21 @@
////////////////////////////////
function init() {
api.getMembers(ctrl.poolId).success(success);
ctrl.src = [];
ctrl.loading = true;
ctrl.error = false;
api.getMembers(ctrl.poolId).then(success, fail);
}
function success(response) {
ctrl.src = response.items;
ctrl.src = response.data.items;
ctrl.loading = false;
}
function fail(/*response*/) {
ctrl.src = [];
ctrl.loading = false;
ctrl.error = true;
}
}

View File

@ -18,12 +18,17 @@
describe('LBaaS v2 Members Table Controller', function() {
var controller, lbaasv2API, scope;
var items = [];
var items = [{ foo: 'bar' }];
var apiFail = false;
function fakeAPI() {
return {
success: function(callback) {
callback({ items: items });
then: function(success, fail) {
if (apiFail && fail) {
fail();
} else {
success({ data: { items: items } });
}
}
};
}
@ -60,6 +65,8 @@
var ctrl = createController();
expect(ctrl.items).toEqual([]);
expect(ctrl.src).toEqual(items);
expect(ctrl.loading).toBe(false);
expect(ctrl.error).toBe(false);
expect(ctrl.checked).toEqual({});
expect(ctrl.loadbalancerId).toBeDefined();
expect(ctrl.listenerId).toBeDefined();
@ -68,8 +75,16 @@
});
it('should invoke lbaasv2 apis', function() {
createController();
var ctrl = createController();
expect(lbaasv2API.getMembers).toHaveBeenCalled();
expect(ctrl.src.length).toBe(1);
});
it('should show error if loading fails', function() {
apiFail = true;
var ctrl = createController();
expect(ctrl.src.length).toBe(0);
expect(ctrl.error).toBe(true);
});
});

View File

@ -73,6 +73,8 @@
</td>
</tr>
<tr table-status table="table" column-count="6"></tr>
</tbody>
<!--
Table-footer:

View File

@ -0,0 +1,54 @@
/*
* 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('tableStatus', tableStatus);
tableStatus.$inject = [
'horizon.dashboard.project.lbaasv2.basePath'
];
/**
* @ngdoc directive
* @name horizon.dashboard.project.lbaasv2:tableStatus
* @description
* The `tableStatus` directive provides a status indicator while loading a table. The table
* should have loading and error properties that give the status of the table, and an items
* array that holds the items being displayed in the table. The column count can be provided
* to fit the status row to an exact number of columns.
* @restrict A
*
* @example
* ```
* <tr table-status table="table" column-count="9"></tr>
* ```
*/
function tableStatus(basePath) {
var directive = {
restrict: 'A',
templateUrl: basePath + 'widgets/table/table-status.html',
scope: {
table: '=',
columnCount: '=?'
}
};
return directive;
}
}());

View File

@ -0,0 +1,102 @@
/*
* 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('tableStatus directive', function() {
var $scope, $compile, markup, table;
beforeEach(module('templates'));
beforeEach(module('horizon.dashboard.project.lbaasv2'));
beforeEach(inject(function($injector) {
$compile = $injector.get('$compile');
$scope = $injector.get('$rootScope').$new();
table = {
loading: true,
error: false,
items: []
};
$scope.table = table;
markup = '<tr table-status table="table"></tr>';
}));
it('initially shows loading status', function() {
var element = digestMarkup($scope, $compile, markup);
expect(element).toBeDefined();
expect(element.children().length).toBe(1);
expect(element.children().first().hasClass('no-rows-help')).toBe(false);
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);
});
it('indicates error status on error', function() {
var element = digestMarkup($scope, $compile, markup);
expect(element).toBeDefined();
table.loading = false;
table.error = true;
$scope.$apply();
expect(element.children().length).toBe(1);
expect(element.children().first().hasClass('no-rows-help')).toBe(false);
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.');
});
it('indicates no rows when there are no rows to display', function() {
var element = digestMarkup($scope, $compile, markup);
expect(element).toBeDefined();
table.loading = false;
table.error = false;
$scope.$apply();
expect(element.children().length).toBe(1);
expect(element.children().first().hasClass('no-rows-help')).toBe(true);
expect(element.find('.progress').length).toBe(0);
expect(element.find('span').length).toBe(1);
expect(element.find('span').text().trim()).toBe('No items to display.');
});
it('goes away when done loading and there are rows to display', function() {
var element = digestMarkup($scope, $compile, markup);
expect(element).toBeDefined();
table.loading = false;
table.error = false;
table.items = ['foo'];
$scope.$apply();
expect(element.children().length).toBe(0);
});
});
}());

View File

@ -0,0 +1,12 @@
<td ng-if="table.loading || table.error || table.items.length === 0"
colspan="{$ columnCount || 100 $}"
ng-class="{ 'no-rows-help': !table.loading && !table.error }">
<span translate ng-if="!table.loading && !table.error">No items to display.</span>
<div class="progress" ng-if="table.loading || table.error">
<div class="progress-bar" role="progressbar"
ng-class="{ 'progress-bar-striped active': !table.error, 'progress-bar-danger': table.error }">
<span ng-if="!table.error" class="sr-only" translate>Loading</span>
<span ng-if="table.error" translate>An error occurred. Please try again later.</span>
</div>
</div>
</td>