From 654e8a07d5784170d59cdef792aad3d6bb09bf3c Mon Sep 17 00:00:00 2001 From: Justin Pomeroy Date: Sun, 9 Jul 2017 23:44:22 +0800 Subject: [PATCH] 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: I6c04d8dd5b27b1ee6423176dd553cc231394ce2f Closes-Bug: #1561102 --- .../healthmonitors/detail.controller.js | 50 +++++++-- .../healthmonitors/detail.controller.spec.js | 73 ++++++++---- .../lbaasv2/healthmonitors/detail.html | 73 ++++++------ .../dashboard/project/lbaasv2/lbaasv2.scss | 14 ++- .../lbaasv2/listeners/detail.controller.js | 42 +++++-- .../listeners/detail.controller.spec.js | 66 +++++++---- .../project/lbaasv2/listeners/detail.html | 73 ++++++------ .../loadbalancers/detail.controller.js | 16 ++- .../loadbalancers/detail.controller.spec.js | 35 ++++-- .../project/lbaasv2/loadbalancers/detail.html | 105 +++++++++--------- .../lbaasv2/members/detail.controller.js | 62 ++++++++--- .../lbaasv2/members/detail.controller.spec.js | 74 ++++++++---- .../project/lbaasv2/members/detail.html | 67 +++++------ .../lbaasv2/pools/detail.controller.js | 46 ++++++-- .../lbaasv2/pools/detail.controller.spec.js | 77 +++++++++---- .../project/lbaasv2/pools/detail.html | 91 +++++++-------- .../widgets/detail/detail-status.directive.js | 53 +++++++++ .../detail/detail-status.directive.spec.js | 87 +++++++++++++++ .../lbaasv2/widgets/detail/detail-status.html | 12 ++ 19 files changed, 781 insertions(+), 335 deletions(-) create mode 100644 octavia_dashboard/static/dashboard/project/lbaasv2/widgets/detail/detail-status.directive.js create mode 100644 octavia_dashboard/static/dashboard/project/lbaasv2/widgets/detail/detail-status.directive.spec.js create mode 100644 octavia_dashboard/static/dashboard/project/lbaasv2/widgets/detail/detail-status.html diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/healthmonitors/detail.controller.js b/octavia_dashboard/static/dashboard/project/lbaasv2/healthmonitors/detail.controller.js index 60053f62..908b4618 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/healthmonitors/detail.controller.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/healthmonitors/detail.controller.js @@ -23,7 +23,8 @@ HealthMonitorDetailController.$inject = [ 'horizon.app.core.openstack-service-api.lbaasv2', 'horizon.dashboard.project.lbaasv2.healthmonitors.actions.rowActions', - '$routeParams' + '$routeParams', + '$q' ]; /** @@ -36,12 +37,15 @@ * @param api The LBaaS v2 API service. * @param rowActions The LBaaS v2 health monitor row actions service. * @param $routeParams The angular $routeParams service. + * @param $q The angular service for promises. * @returns undefined */ - function HealthMonitorDetailController(api, rowActions, $routeParams) { + function HealthMonitorDetailController(api, rowActions, $routeParams, $q) { var ctrl = this; + ctrl.loading = true; + ctrl.error = false; ctrl.actions = rowActions.init($routeParams.loadbalancerId, $routeParams.listenerId, $routeParams.poolId).actions; @@ -51,18 +55,46 @@ //////////////////////////////// function init() { - api.getHealthMonitor($routeParams.healthmonitorId).success(set('healthmonitor')); - api.getPool($routeParams.poolId).success(set('pool')); - api.getListener($routeParams.listenerId).success(set('listener')); - api.getLoadBalancer($routeParams.loadbalancerId).success(set('loadbalancer')); + ctrl.healthmonitor = null; + ctrl.pool = null; + ctrl.listener = null; + 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) { - return angular.bind(null, function setProp(property, value) { - ctrl[property] = value; + function success(property) { + return angular.bind(null, function setProp(property, response) { + ctrl[property] = response.data; }, 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; + } + } })(); diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/healthmonitors/detail.controller.spec.js b/octavia_dashboard/static/dashboard/project/lbaasv2/healthmonitors/detail.controller.spec.js index fca6b860..fd02239d 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/healthmonitors/detail.controller.spec.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/healthmonitors/detail.controller.spec.js @@ -17,26 +17,42 @@ 'use strict'; describe('LBaaS v2 Healthmonitor Detail Controller', function() { - var lbaasv2API, ctrl; + var lbaasv2API, $controller, apiFail, qAllFail; - function fakeAPI() { + function fakePromise(data, reject) { return { - success: function(callback) { - callback('foo'); + then: function(success, fail) { + if (reject) { + fail(); + } else { + success({ data: data }); + } + return fakePromise(); } }; } + function fakeAPI() { + return fakePromise('foo', apiFail); + } + function loadbalancerAPI() { - var loadbalancer = { provisioning_status: 'ACTIVE' }; - return { - success: function(callback) { - callback(loadbalancer); - }, - then: function(callback) { - callback({ data: loadbalancer }); + return fakePromise({ provisioning_status: 'ACTIVE' }); + } + + function qAll() { + return fakePromise(null, qAllFail); + } + + 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.dashboard.project.lbaasv2')); + beforeEach(module(function($provide) { + apiFail = false; + qAllFail = false; + + $provide.value('$q', { all: qAll }); + })); + beforeEach(inject(function($injector) { lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2'); spyOn(lbaasv2API, 'getHealthMonitor').and.callFake(fakeAPI); spyOn(lbaasv2API, 'getPool').and.callFake(fakeAPI); spyOn(lbaasv2API, 'getListener').and.callFake(fakeAPI); spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(loadbalancerAPI); - var controller = $injector.get('$controller'); - ctrl = controller('HealthMonitorDetailController', { - $routeParams: { - loadbalancerId: 'loadbalancerId', - listenerId: 'listenerId', - poolId: 'poolId', - healthmonitorId: 'healthmonitorId' - } - }); + $controller = $injector.get('$controller'); })); it('should invoke lbaasv2 apis', function() { + var ctrl = createController(); expect(lbaasv2API.getHealthMonitor).toHaveBeenCalledWith('healthmonitorId'); expect(lbaasv2API.getPool).toHaveBeenCalledWith('poolId'); expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId'); @@ -75,6 +91,21 @@ 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); + }); + }); })(); diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/healthmonitors/detail.html b/octavia_dashboard/static/dashboard/project/lbaasv2/healthmonitors/detail.html index d0eb2ca7..fe1b9651 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/healthmonitors/detail.html +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/healthmonitors/detail.html @@ -1,39 +1,42 @@
- -
-
-
-
Type
-
{$ ::ctrl.healthmonitor.type $}
-
Delay
-
{$ ::ctrl.healthmonitor.delay $}
-
Max Retries
-
{$ ::ctrl.healthmonitor.max_retries $}
-
Timeout
-
{$ ::ctrl.healthmonitor.timeout $}
-
HTTP Method
-
{$ ::ctrl.healthmonitor.http_method $}
-
Expected Codes
-
{$ ::ctrl.healthmonitor.expected_codes $}
-
URL Path
-
{$ ::ctrl.healthmonitor.url_path $}
-
Admin State Up
-
{$ ctrl.healthmonitor.admin_state_up | yesno $}
-
Monitor ID
-
{$ ::ctrl.healthmonitor.id $}
-
Tenant ID
-
{$ ::ctrl.healthmonitor.tenant_id $}
-
+ +
+ +
+
+
+
Type
+
{$ ::ctrl.healthmonitor.type $}
+
Delay
+
{$ ::ctrl.healthmonitor.delay $}
+
Max Retries
+
{$ ::ctrl.healthmonitor.max_retries $}
+
Timeout
+
{$ ::ctrl.healthmonitor.timeout $}
+
HTTP Method
+
{$ ::ctrl.healthmonitor.http_method $}
+
Expected Codes
+
{$ ::ctrl.healthmonitor.expected_codes $}
+
URL Path
+
{$ ::ctrl.healthmonitor.url_path $}
+
Admin State Up
+
{$ ctrl.healthmonitor.admin_state_up | yesno $}
+
Monitor ID
+
{$ ::ctrl.healthmonitor.id $}
+
Tenant ID
+
{$ ::ctrl.healthmonitor.tenant_id $}
+
+
\ No newline at end of file diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/lbaasv2.scss b/octavia_dashboard/static/dashboard/project/lbaasv2/lbaasv2.scss index b75dfe78..6e51091c 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/lbaasv2.scss +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/lbaasv2.scss @@ -68,8 +68,9 @@ } } -/* Progress indicator in the table while items are loading */ -[table-status] { +/* Progress indicator while data is loading */ +[table-status], +detail-status { .progress { margin: 0px auto; width: 25%; @@ -78,4 +79,13 @@ width: 100%; } } +} +detail-status { + .progress { + margin-top: 25vh; + } + .error-actions { + text-align: center; + margin-top: 10px; + } } \ No newline at end of file diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.js b/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.js index 680d09d6..3fe9dc33 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.js @@ -23,7 +23,8 @@ ListenerDetailController.$inject = [ 'horizon.app.core.openstack-service-api.lbaasv2', 'horizon.dashboard.project.lbaasv2.listeners.actions.rowActions', - '$routeParams' + '$routeParams', + '$q' ]; /** @@ -36,12 +37,15 @@ * @param api The LBaaS v2 API service. * @param rowActions The listener row actions service. * @param $routeParams The angular $routeParams service. + * @param $q The angular service for promises. * @returns undefined */ - function ListenerDetailController(api, rowActions, $routeParams) { + function ListenerDetailController(api, rowActions, $routeParams, $q) { var ctrl = this; + ctrl.loading = true; + ctrl.error = false; ctrl.actions = rowActions.init($routeParams.loadbalancerId).actions; init(); @@ -49,16 +53,40 @@ //////////////////////////////// function init() { - api.getListener($routeParams.listenerId).success(set('listener')); - api.getLoadBalancer($routeParams.loadbalancerId).success(set('loadbalancer')); + ctrl.listener = null; + 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) { - return angular.bind(null, function setProp(property, value) { - ctrl[property] = value; + function success(property) { + return angular.bind(null, function setProp(property, response) { + ctrl[property] = response.data; }, 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; + } + } })(); diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.spec.js b/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.spec.js index 8b70cc8a..586fd2c4 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.spec.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/detail.controller.spec.js @@ -17,26 +17,40 @@ 'use strict'; describe('LBaaS v2 Listener Detail Controller', function() { - var lbaasv2API, ctrl; + var lbaasv2API, $controller, apiFail, qAllFail; - function fakeAPI() { + function fakePromise(data, reject) { return { - success: function(callback) { - callback('foo'); + then: function(success, fail) { + if (reject) { + fail(); + } else { + success({ data: data }); + } + return fakePromise(); } }; } + function fakeAPI() { + return fakePromise('foo', apiFail); + } + function loadbalancerAPI() { - var loadbalancer = { provisioning_status: 'ACTIVE' }; - return { - success: function(callback) { - callback(loadbalancer); - }, - then: function(callback) { - callback({ data: loadbalancer }); + return fakePromise({ provisioning_status: 'ACTIVE' }); + } + + function qAll() { + return fakePromise(null, qAllFail); + } + + function createController() { + return $controller('ListenerDetailController', { + $routeParams: { + loadbalancerId: 'loadbalancerId', + listenerId: 'listenerId' } - }; + }); } /////////////////////// @@ -48,6 +62,10 @@ beforeEach(module('horizon.dashboard.project.lbaasv2')); beforeEach(module(function($provide) { + apiFail = false; + qAllFail = false; + + $provide.value('$q', { all: qAll }); $provide.value('$uibModal', {}); })); @@ -55,22 +73,32 @@ lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2'); spyOn(lbaasv2API, 'getListener').and.callFake(fakeAPI); spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(loadbalancerAPI); - var controller = $injector.get('$controller'); - ctrl = controller('ListenerDetailController', { - $routeParams: { - loadbalancerId: 'loadbalancerId', - listenerId: 'listenerId' - } - }); + $controller = $injector.get('$controller'); })); it('should invoke lbaasv2 apis', function() { + var ctrl = createController(); expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId'); expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('loadbalancerId'); expect(ctrl.loadbalancer).toEqual({ provisioning_status: 'ACTIVE' }); 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); + }); + }); })(); diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/detail.html b/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/detail.html index 588eee6d..d24e60d5 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/detail.html +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/listeners/detail.html @@ -1,39 +1,42 @@
- -
-
-
-
Protocol
-
{$ ::ctrl.listener.protocol $}
-
Protocol Port
-
{$ ::ctrl.listener.protocol_port $}
-
Connection Limit
-
{$ ctrl.listener.connection_limit | limit $}
-
Admin State Up
-
{$ ctrl.listener.admin_state_up | yesno $}
-
Default Pool ID
-
- - {$ ::ctrl.listener.default_pool_id $} - - - {$ 'None' | translate $} - -
-
Listener ID
-
{$ ::ctrl.listener.id $}
-
Tenant ID
-
{$ ::ctrl.listener.tenant_id $}
-
+ +
+ +
+
+
+
Protocol
+
{$ ::ctrl.listener.protocol $}
+
Protocol Port
+
{$ ::ctrl.listener.protocol_port $}
+
Connection Limit
+
{$ ctrl.listener.connection_limit | limit $}
+
Admin State Up
+
{$ ctrl.listener.admin_state_up | yesno $}
+
Default Pool ID
+
+ + {$ ::ctrl.listener.default_pool_id $} + + + {$ 'None' | translate $} + +
+
Listener ID
+
{$ ::ctrl.listener.id $}
+
Tenant ID
+
{$ ::ctrl.listener.tenant_id $}
+
+
\ No newline at end of file diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.js b/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.js index b252554e..e01f5f5d 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.js @@ -50,6 +50,8 @@ ) { var ctrl = this; + ctrl.loading = true; + ctrl.error = false; ctrl.actions = rowActions.actions; ctrl.operatingStatus = loadBalancersService.operatingStatus; ctrl.provisioningStatus = loadBalancersService.provisioningStatus; @@ -60,11 +62,21 @@ //////////////////////////////// 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) { - 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 diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.spec.js b/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.spec.js index 71924939..538f0b41 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.spec.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.controller.spec.js @@ -17,16 +17,28 @@ 'use strict'; describe('LBaaS v2 Load Balancer Detail Controller', function() { - var lbaasv2API, ctrl, $scope, $window; + var lbaasv2API, $scope, $window, $controller, apiFail; function fakeAPI() { return { - success: function(callback) { - callback({ id: '1234' }); + then: function(success, fail) { + 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')); @@ -36,6 +48,7 @@ beforeEach(module('horizon.dashboard.project.lbaasv2')); beforeEach(module(function($provide) { + apiFail = false; $provide.value('$uibModal', {}); })); @@ -44,19 +57,16 @@ spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(fakeAPI); $scope = $injector.get('$rootScope').$new(); $window = {}; - var controller = $injector.get('$controller'); - ctrl = controller('LoadBalancerDetailController', { - $scope: $scope, - $window: $window, - $routeParams: { loadbalancerId: '1234' } - }); + $controller = $injector.get('$controller'); })); it('should invoke lbaasv2 apis', function() { + createController(); expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('1234', true); }); it('should save changes to listeners tab active state', function() { + var ctrl = createController(); expect($window.listenersTabActive).toBeUndefined(); expect(ctrl.listenersTabActive).toBeUndefined(); ctrl.listenersTabActive = true; @@ -67,6 +77,13 @@ 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); + }); + }); })(); diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.html b/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.html index 0561244d..74c25724 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.html +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/loadbalancers/detail.html @@ -1,54 +1,57 @@
- - - -
-
-
-
Provider
-
{$ ::ctrl.loadbalancer.provider $}
-
Admin State Up
-
{$ ctrl.loadbalancer.admin_state_up | yesno $}
-
Floating IP Address
-
{$ ctrl.loadbalancer.floating_ip.ip | noValue:('None' | translate) $}
-
Load Balancer ID
-
{$ ::ctrl.loadbalancer.id $}
-
Subnet ID
-
- {$ ::ctrl.loadbalancer.vip_subnet_id $} -
-
Port ID
-
- {$ ::ctrl.loadbalancer.vip_port_id $} -
-
+ +
+ + + +
+
+
+
Provider
+
{$ ::ctrl.loadbalancer.provider $}
+
Admin State Up
+
{$ ctrl.loadbalancer.admin_state_up | yesno $}
+
Floating IP Address
+
{$ ctrl.loadbalancer.floating_ip.ip | noValue:('None' | translate) $}
+
Load Balancer ID
+
{$ ::ctrl.loadbalancer.id $}
+
Subnet ID
+
+ {$ ::ctrl.loadbalancer.vip_subnet_id $} +
+
Port ID
+
+ {$ ::ctrl.loadbalancer.vip_port_id $} +
+
+
-
- - - - - + + + + + +
\ No newline at end of file diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/members/detail.controller.js b/octavia_dashboard/static/dashboard/project/lbaasv2/members/detail.controller.js index c073b05d..8df0504f 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/members/detail.controller.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/members/detail.controller.js @@ -24,6 +24,7 @@ 'horizon.app.core.openstack-service-api.lbaasv2', 'horizon.dashboard.project.lbaasv2.members.actions.rowActions', '$routeParams', + '$q', 'horizon.dashboard.project.lbaasv2.loadbalancers.service', 'horizon.dashboard.project.lbaasv2.members.service' ]; @@ -38,16 +39,19 @@ * @param api The LBaaS v2 API service. * @param rowActions The pool members row actions service. * @param $routeParams The angular $routeParams service. + * @param $q The angular service for promises. * @param loadBalancersService The LBaaS v2 load balancers service. * @param membersService The LBaaS v2 members service. * @returns undefined */ function MemberDetailController( - api, rowActions, $routeParams, loadBalancersService, membersService + api, rowActions, $routeParams, $q, loadBalancersService, membersService ) { var ctrl = this; + ctrl.loading = true; + ctrl.error = false; ctrl.actions = rowActions.init($routeParams.loadbalancerId, $routeParams.poolId).actions; ctrl.loadbalancerId = $routeParams.loadbalancerId; ctrl.listenerId = $routeParams.listenerId; @@ -60,25 +64,53 @@ //////////////////////////////// function init() { - api.getMember($routeParams.poolId, $routeParams.memberId).success(memberSuccess); - api.getPool($routeParams.poolId).success(set('pool')); - api.getListener($routeParams.listenerId).success(set('listener')); - api.getLoadBalancer($routeParams.loadbalancerId).success(set('loadbalancer')); + ctrl.member = null; + ctrl.pool = null; + ctrl.listener = null; + 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) { - return angular.bind(null, function setProp(property, value) { - ctrl[property] = value; + function success(property) { + return angular.bind(null, function setProp(property, response) { + ctrl[property] = response.data; + + if (property === 'member') { + membersService.associateMemberStatuses( + ctrl.loadbalancerId, + ctrl.listenerId, + ctrl.poolId, + [ctrl.member]); + } + }, property); } - function memberSuccess(response) { - ctrl.member = response; - membersService.associateMemberStatuses( - ctrl.loadbalancerId, - ctrl.listenerId, - ctrl.poolId, - [ctrl.member]); + 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; } } diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/members/detail.controller.spec.js b/octavia_dashboard/static/dashboard/project/lbaasv2/members/detail.controller.spec.js index f6e58c06..93542c1d 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/members/detail.controller.spec.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/members/detail.controller.spec.js @@ -17,26 +17,42 @@ 'use strict'; 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 { - success: function(callback) { - callback('foo'); + then: function(success, fail) { + if (reject) { + fail(); + } else { + success({ data: data }); + } + return fakePromise(); } }; } + function fakeAPI() { + return fakePromise('foo', apiFail); + } + function loadbalancerAPI() { - var loadbalancer = { provisioning_status: 'ACTIVE' }; - return { - success: function(callback) { - callback(loadbalancer); - }, - then: function(callback) { - callback({ data: loadbalancer }); + return fakePromise({ provisioning_status: 'ACTIVE' }); + } + + function qAll() { + return fakePromise(null, qAllFail); + } + + 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(function($provide) { + apiFail = false; + qAllFail = false; + + $provide.value('$q', { all: qAll }); $provide.value('$uibModal', {}); $provide.value('horizon.dashboard.project.lbaasv2.members.actions.rowActions', { init: function() { @@ -68,25 +88,15 @@ spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(loadbalancerAPI); spyOn(actions, 'init').and.callThrough(); spyOn(membersService, 'associateMemberStatuses'); - controller = $injector.get('$controller'); - ctrl = controller('MemberDetailController', { - $routeParams: { - loadbalancerId: 'loadbalancerId', - listenerId: 'listenerId', - poolId: 'poolId', - memberId: 'memberId' - } - }); + $controller = $injector.get('$controller'); })); it('should invoke lbaasv2 apis', function() { + var ctrl = createController(); expect(lbaasv2API.getMember).toHaveBeenCalledWith('poolId','memberId'); expect(lbaasv2API.getPool).toHaveBeenCalledWith('poolId'); expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId'); expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('loadbalancerId'); - }); - - it('should initialize the controller properties correctly', function() { expect(ctrl.loadbalancerId).toBeDefined(); expect(ctrl.listenerId).toBeDefined(); expect(ctrl.poolId).toBeDefined(); @@ -97,10 +107,26 @@ }); it('should invoke the "associateMemberStatuses" method', function() { + var ctrl = createController(); expect(membersService.associateMemberStatuses).toHaveBeenCalledWith( 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); + }); + }); })(); diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/members/detail.html b/octavia_dashboard/static/dashboard/project/lbaasv2/members/detail.html index b32f7f14..ebb40791 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/members/detail.html +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/members/detail.html @@ -1,35 +1,38 @@
- -
-
-
-
Address
-
{$ ::ctrl.member.address $}
-
Protocol Port
-
{$ ::ctrl.member.protocol_port $}
-
Weight
-
{$ ctrl.member.weight $}
-
Operating Status
-
{$ ctrl.member.operating_status | decode:ctrl.operatingStatus $}
-
Provisioning Status
-
{$ ctrl.member.provisioning_status | decode:ctrl.provisioningStatus $}
-
Admin State Up
-
{$ ctrl.member.admin_state_up | yesno $}
-
Member ID
-
{$ ::ctrl.member.id $}
-
Tenant ID
-
{$ ::ctrl.member.tenant_id $}
-
+ +
+ +
+
+
+
Address
+
{$ ::ctrl.member.address $}
+
Protocol Port
+
{$ ::ctrl.member.protocol_port $}
+
Weight
+
{$ ctrl.member.weight $}
+
Operating Status
+
{$ ctrl.member.operating_status | decode:ctrl.operatingStatus $}
+
Provisioning Status
+
{$ ctrl.member.provisioning_status | decode:ctrl.provisioningStatus $}
+
Admin State Up
+
{$ ctrl.member.admin_state_up | yesno $}
+
Member ID
+
{$ ::ctrl.member.id $}
+
Tenant ID
+
{$ ::ctrl.member.tenant_id $}
+
+
-
\ No newline at end of file +
diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/pools/detail.controller.js b/octavia_dashboard/static/dashboard/project/lbaasv2/pools/detail.controller.js index aabea9f6..4f0e1883 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/pools/detail.controller.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/pools/detail.controller.js @@ -26,7 +26,8 @@ '$routeParams', 'horizon.framework.util.i18n.gettext', '$window', - '$scope' + '$scope', + '$q' ]; /** @@ -42,12 +43,15 @@ * @param gettext The horizon gettext function for translation. * @param $window Angular's reference to the browser window object. * @param $scope The angular scope object. + * @param $q The angular service for promises. * @returns undefined */ - function PoolDetailController(api, rowActions, $routeParams, gettext, $window, $scope) { + function PoolDetailController(api, rowActions, $routeParams, gettext, $window, $scope, $q) { var ctrl = this; + ctrl.loading = true; + ctrl.error = false; ctrl.loadBalancerAlgorithm = { ROUND_ROBIN: gettext('Round Robin'), LEAST_CONNECTIONS: gettext('Least Connections'), @@ -61,17 +65,43 @@ //////////////////////////////// function init() { - api.getPool($routeParams.poolId).success(set('pool')); - api.getListener($routeParams.listenerId).success(set('listener')); - api.getLoadBalancer($routeParams.loadbalancerId).success(set('loadbalancer')); + ctrl.pool = null; + ctrl.listener = null; + 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) { - return angular.bind(null, function setProp(property, value) { - ctrl[property] = value; + function success(property) { + return angular.bind(null, function setProp(property, response) { + ctrl[property] = response.data; }, 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 // active after reloading the route following an action. $scope.$watch(function() { diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/pools/detail.controller.spec.js b/octavia_dashboard/static/dashboard/project/lbaasv2/pools/detail.controller.spec.js index 87165230..894383db 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/pools/detail.controller.spec.js +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/pools/detail.controller.spec.js @@ -17,26 +17,43 @@ 'use strict'; 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 { - success: function(callback) { - callback('foo'); + then: function(success, fail) { + if (reject) { + fail(); + } else { + success({ data: data }); + } + return fakePromise(); } }; } + function fakeAPI() { + return fakePromise('foo', apiFail); + } + function loadbalancerAPI() { - var loadbalancer = { provisioning_status: 'ACTIVE' }; - return { - success: function(callback) { - callback(loadbalancer); - }, - then: function(callback) { - callback({ data: loadbalancer }); + return fakePromise({ provisioning_status: 'ACTIVE' }); + } + + function qAll() { + return fakePromise(null, qAllFail); + } + + 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.dashboard.project.lbaasv2')); + beforeEach(module(function($provide) { + apiFail = false; + qAllFail = false; + + $provide.value('$q', { all: qAll }); + })); + beforeEach(inject(function($injector) { lbaasv2API = $injector.get('horizon.app.core.openstack-service-api.lbaasv2'); spyOn(lbaasv2API, 'getPool').and.callFake(fakeAPI); @@ -54,19 +78,11 @@ spyOn(lbaasv2API, 'getLoadBalancer').and.callFake(loadbalancerAPI); $scope = $injector.get('$rootScope').$new(); $window = {}; - var controller = $injector.get('$controller'); - ctrl = controller('PoolDetailController', { - $scope: $scope, - $window: $window, - $routeParams: { - loadbalancerId: 'loadbalancerId', - listenerId: 'listenerId', - poolId: 'poolId' - } - }); + $controller = $injector.get('$controller'); })); it('should invoke lbaasv2 apis', function() { + var ctrl = createController(); expect(lbaasv2API.getPool).toHaveBeenCalledWith('poolId'); expect(lbaasv2API.getListener).toHaveBeenCalledWith('listenerId'); expect(lbaasv2API.getLoadBalancer).toHaveBeenCalledWith('loadbalancerId'); @@ -76,10 +92,12 @@ }); it('should define mapping for the load balancer algorithm', function() { + var ctrl = createController(); expect(ctrl.loadBalancerAlgorithm).toBeDefined(); }); it('should save changes to members tab active state', function() { + var ctrl = createController(); expect($window.membersTabActive).toBeUndefined(); expect(ctrl.membersTabActive).toBeUndefined(); ctrl.membersTabActive = true; @@ -90,6 +108,21 @@ 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); + }); + }); })(); diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/pools/detail.html b/octavia_dashboard/static/dashboard/project/lbaasv2/pools/detail.html index faaa2537..510deaa7 100644 --- a/octavia_dashboard/static/dashboard/project/lbaasv2/pools/detail.html +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/pools/detail.html @@ -1,47 +1,50 @@
- - - -
-
-
-
Protocol
-
{$ ::ctrl.pool.protocol $}
-
Load Balancer Algorithm
-
{$ ctrl.pool.lb_algorithm | decode:ctrl.loadBalancerAlgorithm $}
-
Session Persistence
-
{$ ctrl.pool.session_persistence | noValue:('None' | translate) $}
-
Admin State Up
-
{$ ctrl.pool.admin_state_up | yesno $}
-
Health Monitor ID
-
- - {$ ::ctrl.pool.healthmonitor_id $} - - - {$ 'None' | translate $} - -
-
Pool ID
-
{$ ::ctrl.pool.id $}
-
Tenant ID
-
{$ ::ctrl.pool.tenant_id $}
-
+ +
+ + + +
+
+
+
Protocol
+
{$ ::ctrl.pool.protocol $}
+
Load Balancer Algorithm
+
{$ ctrl.pool.lb_algorithm | decode:ctrl.loadBalancerAlgorithm $}
+
Session Persistence
+
{$ ctrl.pool.session_persistence | noValue:('None' | translate) $}
+
Admin State Up
+
{$ ctrl.pool.admin_state_up | yesno $}
+
Health Monitor ID
+
+ + {$ ::ctrl.pool.healthmonitor_id $} + + + {$ 'None' | translate $} + +
+
Pool ID
+
{$ ::ctrl.pool.id $}
+
Tenant ID
+
{$ ::ctrl.pool.tenant_id $}
+
+
-
- - - - - + + + + + +
\ No newline at end of file diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/widgets/detail/detail-status.directive.js b/octavia_dashboard/static/dashboard/project/lbaasv2/widgets/detail/detail-status.directive.js new file mode 100644 index 00000000..24cb0f91 --- /dev/null +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/widgets/detail/detail-status.directive.js @@ -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 + * ``` + * + * ``` + */ + + function detailStatus(basePath) { + var directive = { + restrict: 'E', + templateUrl: basePath + 'widgets/detail/detail-status.html', + scope: { + loading: '=', + error: '=' + } + }; + return directive; + } +}()); diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/widgets/detail/detail-status.directive.spec.js b/octavia_dashboard/static/dashboard/project/lbaasv2/widgets/detail/detail-status.directive.spec.js new file mode 100644 index 00000000..c26c5fde --- /dev/null +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/widgets/detail/detail-status.directive.spec.js @@ -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 = ''; + })); + + 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); + }); + + }); +}()); diff --git a/octavia_dashboard/static/dashboard/project/lbaasv2/widgets/detail/detail-status.html b/octavia_dashboard/static/dashboard/project/lbaasv2/widgets/detail/detail-status.html new file mode 100644 index 00000000..3278ed1a --- /dev/null +++ b/octavia_dashboard/static/dashboard/project/lbaasv2/widgets/detail/detail-status.html @@ -0,0 +1,12 @@ +
+
+
+ Loading + An error occurred. Please try again later. +
+
+
+ Back +
+
\ No newline at end of file