Merge "Indicate table loading, error, and empty states"

This commit is contained in:
Jenkins 2016-10-04 00:11:14 +00:00 committed by Gerrit Code Review
commit 7597e0a10e
13 changed files with 282 additions and 21 deletions

View File

@ -67,3 +67,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>