From b7e658ee231a2368a0d56f7a89aad2a61bb3229b Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Fri, 6 Oct 2017 13:30:47 +0100 Subject: [PATCH] Improve the loading indicators on initial console load (#1286) * Improve the loading indicators on initial console load * A better way of having a global busy state and indicator * Fix unit tests * Fix lint issue --- .../app-core/frontend/i18n/en_US/common.json | 2 + .../frontend/src/utils/busy.service.js | 72 +++++++++++++++++++ .../src/view/application.directive.js | 15 +++- .../frontend/src/view/application.html | 4 +- .../global-spinner.directive.js | 23 +++--- .../global-spinner/global-spinner.html | 15 ++-- .../global-spinner/global-spinner.scss | 33 ++++++++- .../frontend/i18n/en_US/app-wall.json | 1 + .../frontend/i18n/en_US/cloud-foundry.json | 1 + .../view/applications/applications.module.js | 9 ++- .../src/view/applications/list/list.module.js | 19 ++++- .../applications/applications.module.spec.js | 5 +- .../applications/list/list.module.spec.js | 3 +- 13 files changed, 174 insertions(+), 28 deletions(-) create mode 100644 components/app-core/frontend/src/utils/busy.service.js diff --git a/components/app-core/frontend/i18n/en_US/common.json b/components/app-core/frontend/i18n/en_US/common.json index 330b919a47..d604d7c4aa 100644 --- a/components/app-core/frontend/i18n/en_US/common.json +++ b/components/app-core/frontend/i18n/en_US/common.json @@ -6,6 +6,8 @@ "terms": "Terms of Use", "endpoint": "Endpoint", + "preparing.console": "Preparing Console ...", + "buttons": { "ok": "OK", "cancel": "Cancel", diff --git a/components/app-core/frontend/src/utils/busy.service.js b/components/app-core/frontend/src/utils/busy.service.js new file mode 100644 index 0000000000..e86129b053 --- /dev/null +++ b/components/app-core/frontend/src/utils/busy.service.js @@ -0,0 +1,72 @@ +(function () { + 'use strict'; + angular + .module('app.utils') + .factory('appBusyService', appBusyServiceFactory); + + /** + * @namespace appBusyService + * @memberof app.utils + * @name appBusyService + * @description The application busy service + * @param {object} appEventService - the event service + * @returns {object} the busy service + */ + function appBusyServiceFactory() { + + var nextBusyId = 0; + + // Maintain a list of outstanding busy messages - only show the most recent + var busyStack = []; + + var busyStates = {}; + + return { + + busyState: {}, + + _update: function () { + if (busyStack.length === 0) { + this.busyState.active = false; + } else { + // Get the last item - that is the most recent + var newestId = busyStack[busyStack.length - 1]; + var busyInfo = busyStates[newestId]; + this.busyState.label = busyInfo.label; + this.busyState.local = busyInfo.local || false; + this.busyState.active = true; + } + }, + + set: function (label, nonModal) { + var id = nextBusyId; + nextBusyId++; + busyStates[id] = { + id: id, + label: label, + local: nonModal || false + }; + busyStack.push(id); + this._update(); + return id; + }, + + clear: function (id) { + if (busyStack.length === 0) { + return; + } + var newestId = busyStack[busyStack.length - 1]; + delete busyStates[id]; + _.remove(busyStack, function (v) { + return v === id; + }); + + // If we removed what was the newest, then we need to show the next newest, or hide the busy message if none left + if (id === newestId) { + this. _update(); + } + } + }; + } + +})(); diff --git a/components/app-core/frontend/src/view/application.directive.js b/components/app-core/frontend/src/view/application.directive.js index 77f0f856b2..23e4cee234 100644 --- a/components/app-core/frontend/src/view/application.directive.js +++ b/components/app-core/frontend/src/view/application.directive.js @@ -39,6 +39,7 @@ * @param {$rootScope} $rootScope - Angular $rootScope service * @param {$scope} $scope - Angular $scope service * @param {appLoggedInService} appLoggedInService - Logged In Service + * @param {appBusyService} appBusyService - Application Busy Service * @property {app.utils.appEventService} appEventService - the event bus service * @property {app.model.modelManager} modelManager - the application model manager * @property {app.view.appUpgradeCheck} appUpgradeCheck - the upgrade check service @@ -61,7 +62,8 @@ $window, $rootScope, $scope, - appLoggedInService + appLoggedInService, + appBusyService ) { var vm = this; @@ -82,6 +84,9 @@ vm.logout = logout; vm.reload = reload; + // Global spinner state + vm.spinner = appBusyService.busyState; + if (loginManager.isEnabled()) { $timeout(function () { verifySessionOrCheckUpgrade(); @@ -187,9 +192,12 @@ vm.failedLogin = false; vm.serverErrorOnLogin = false; vm.serverFailedToRespond = false; - vm.showGlobalSpinner = true; + //vm.showGlobalSpinner = true; + //vm.globalSpinnerLabel = 'preparing.console'; vm.redirectState = false; + vm.appBusyId = appBusyService.set('preparing.console'); + // If we have a setup error, then we don't want to continue login to some other page // We will redirect to our error page instead var continueLogin = true; @@ -239,7 +247,8 @@ } }) .finally(function () { - vm.showGlobalSpinner = false; + vm.appBusyId = appBusyService.clear(vm.appBusyId); + if (continueLogin) { // When we notify listeners that login has completed, in some cases we don't want them // to redirect to their page - we might want to control that they go to the endpoints dashboard (for example). diff --git a/components/app-core/frontend/src/view/application.html b/components/app-core/frontend/src/view/application.html index 4546c87a33..1d7b5923c8 100644 --- a/components/app-core/frontend/src/view/application.html +++ b/components/app-core/frontend/src/view/application.html @@ -14,6 +14,8 @@ diff --git a/components/app-framework/src/widgets/global-spinner/global-spinner.directive.js b/components/app-framework/src/widgets/global-spinner/global-spinner.directive.js index c7b83d553d..183e964a3a 100644 --- a/components/app-framework/src/widgets/global-spinner/global-spinner.directive.js +++ b/components/app-framework/src/widgets/global-spinner/global-spinner.directive.js @@ -10,18 +10,23 @@ bindToController: { classes: '@?', spinnerActive: '=', - spinnerType: '@?' + spinnerType: '@?', + spinnerLabel: '=?', + spinnerLocal: '=?' }, controller: GlobalSpinnerController, controllerAs: 'globalSpinnerCtrl', - link: function (scope) { - scope.$watch('spinnerActive', function (spinnerActive) { - if (spinnerActive) { - $document.find('body').addClass('global-spinner-active'); - } else { - $document.find('body').removeClass('global-spinner-active'); - } - }); + link: function (scope, element, attrs, ctrl) { + // Check to see if other spinners are already active + if (!ctrl.spinnerLocal) { + scope.$watch('spinnerActive', function (spinnerActive) { + if (spinnerActive) { + $document.find('body').addClass('global-spinner-active'); + } else { + $document.find('body').removeClass('global-spinner-active'); + } + }); + } }, scope: {}, templateUrl: 'framework/widgets/global-spinner/global-spinner.html', diff --git a/components/app-framework/src/widgets/global-spinner/global-spinner.html b/components/app-framework/src/widgets/global-spinner/global-spinner.html index 1aaa029915..8e08d7d0cf 100644 --- a/components/app-framework/src/widgets/global-spinner/global-spinner.html +++ b/components/app-framework/src/widgets/global-spinner/global-spinner.html @@ -1,7 +1,10 @@ -
- - - - +
+
+ +
{{ globalSpinnerCtrl.spinnerLabel}}
+ + + +
diff --git a/components/app-framework/src/widgets/global-spinner/global-spinner.scss b/components/app-framework/src/widgets/global-spinner/global-spinner.scss index 9cd5244e6f..faf1e259d9 100644 --- a/components/app-framework/src/widgets/global-spinner/global-spinner.scss +++ b/components/app-framework/src/widgets/global-spinner/global-spinner.scss @@ -1,5 +1,5 @@ -.global-spinner { - background-color: $global-spinner-bg-color; + +.global-spinner, .global-spinner-local { position: fixed; top: 0px; left: 0px; @@ -11,14 +11,41 @@ align-items: center; height: 100%; width: 100%; - z-index: $global-spinner-z-index; spinner { height: $global-spinner-height; width: $global-spinner-width; } + + .global-spinner-content.global-spinnter-with-label { + background-color: $gray-darker; + color: $white; + padding: $console-half-space $console-unit-space * 2; + border-radius: 4px; + font-size: 16px; + text-align: center; + + > div { + margin-bottom: $console-half-space; + } + + bounce-spinner .bounce-spinner > div { + background-color: $white; + width: 16px; + height: 16px; + } + } } .global-spinner-active { overflow-y: hidden; } + +.global-spinner { + background-color: $global-spinner-bg-color; + z-index: $global-spinner-z-index; +} + +.global-spinner-local { + pointer-events: none; +} \ No newline at end of file diff --git a/components/cloud-foundry/frontend/i18n/en_US/app-wall.json b/components/cloud-foundry/frontend/i18n/en_US/app-wall.json index 85c2750a8d..9828f01006 100644 --- a/components/cloud-foundry/frontend/i18n/en_US/app-wall.json +++ b/components/cloud-foundry/frontend/i18n/en_US/app-wall.json @@ -3,6 +3,7 @@ "app-wall": "@:applications", "search-placeholder": "Search by name", "add-application": "Add Application", + "retrieving": "Retrieving Applications", "reset": "Reset", "options": { "show": "Show Options", diff --git a/components/cloud-foundry/frontend/i18n/en_US/cloud-foundry.json b/components/cloud-foundry/frontend/i18n/en_US/cloud-foundry.json index ad962c323b..d4bcc3d3c3 100644 --- a/components/cloud-foundry/frontend/i18n/en_US/cloud-foundry.json +++ b/components/cloud-foundry/frontend/i18n/en_US/cloud-foundry.json @@ -1,5 +1,6 @@ { "cloud-foundry": "Cloud Foundry", + "cf.busy": "Collecting Cloud Foundry Metadata", "org": "Org", "space": "Space", "menu": { diff --git a/components/cloud-foundry/frontend/src/view/applications/applications.module.js b/components/cloud-foundry/frontend/src/view/applications/applications.module.js index 0885c848da..068a06a7e5 100644 --- a/components/cloud-foundry/frontend/src/view/applications/applications.module.js +++ b/components/cloud-foundry/frontend/src/view/applications/applications.module.js @@ -36,21 +36,26 @@ * @param {app.model.modelManager} modelManager - the model management service * @param {app.utils.appEventService} appEventService - the event bus service * @param {object} appLoggedInService - Logged In Service + * @param {object} appBusyService - the appBusyService service * @constructor */ - function ApplicationsController($scope, $q, $state, appUtilsService, modelManager, appEventService, appLoggedInService) { + function ApplicationsController($scope, $q, $state,appUtilsService, modelManager, appEventService, appLoggedInService, appBusyService) { var authService = modelManager.retrieve('cloud-foundry.model.auth'); var initialized = $q.defer(); + var vm = this; if (appLoggedInService.isLoggedIn()) { initialized.resolve(); } function init() { + vm.appBusyId = appBusyService.set('cf.busy'); return initialized.promise .then(function () { - return authService.initialize(); + return authService.initialize().finally(function () { + appBusyService.clear(vm.appBusyId); + }); }); } diff --git a/components/cloud-foundry/frontend/src/view/applications/list/list.module.js b/components/cloud-foundry/frontend/src/view/applications/list/list.module.js index 33b45c5746..fd3f4066eb 100644 --- a/components/cloud-foundry/frontend/src/view/applications/list/list.module.js +++ b/components/cloud-foundry/frontend/src/view/applications/list/list.module.js @@ -32,9 +32,11 @@ * @param {object} cfAppWallActions - service providing collection of actions that can be taken on the app wall (add, * deploy, etc) * @param {appLocalStorage} appLocalStorage - service provides access to the local storage facility of the web browser + * @param {appBusyService} appBusyService - the application busy service + * */ function ApplicationsListController($scope, $translate, $state, $timeout, $q, $window, modelManager, appErrorService, - appUtilsService, cfOrganizationModel, cfAppWallActions, appLocalStorage) { + appUtilsService, cfOrganizationModel, cfAppWallActions, appLocalStorage, appBusyService) { var vm = this; @@ -46,6 +48,12 @@ vm.model = modelManager.retrieve('cloud-foundry.model.application'); vm.loading = true; + if (!vm.model.hasApps) { + vm.appBusyId = appBusyService.set('app-wall.retrieving', true); + } else { + vm.appBusyId = undefined; + } + vm.isSpaceDeveloper = false; vm.clusters = [{label: 'app-wall.select-endpoint-all', value: 'all', translateLabel: true}]; vm.organizations = [{label: 'app-wall.select-org-all', value: 'all', translateLabel: true}]; @@ -110,6 +118,11 @@ appErrorService.clearAppError(); // Ensure that remove the resize handler on the window angular.element($window).off('resize', onResize); + + // Clear the busy indicator if it is shown + if (vm.appBusyId) { + appBusyService.clear(vm.appBusyId); + } }); $scope.$watch(function () { @@ -154,6 +167,10 @@ .finally(function () { // Ensure ready is always set after initial load. Ready will show filters, no services/app message, etc vm.ready = true; + if (vm.appBusyId) { + appBusyService.clear(vm.appBusyId); + vm.appBusyId = undefined; + } }); } diff --git a/components/cloud-foundry/frontend/test/unit/view/applications/applications.module.spec.js b/components/cloud-foundry/frontend/test/unit/view/applications/applications.module.spec.js index b99d6fedb7..431eed2b65 100644 --- a/components/cloud-foundry/frontend/test/unit/view/applications/applications.module.spec.js +++ b/components/cloud-foundry/frontend/test/unit/view/applications/applications.module.spec.js @@ -17,14 +17,15 @@ var modelManager = $injector.get('modelManager'); appEventService = $injector.get('appEventService'); var appLoggedInService = $injector.get('appLoggedInService'); + var appBusyService = $injector.get('appBusyService'); authService = modelManager.retrieve('cloud-foundry.model.auth'); - spyOn(authService, 'initialize'); + spyOn(authService, 'initialize').and.callThrough(); $scope = $injector.get('$rootScope').$new(); var ApplicationsController = $state.get('cf.applications').controller; - $controller = new ApplicationsController($scope, $q, $state, appUtilsService, modelManager, appEventService, appLoggedInService); + $controller = new ApplicationsController($scope, $q, $state, appUtilsService, modelManager, appEventService, appLoggedInService, appBusyService); expect($controller).toBeDefined(); })); diff --git a/components/cloud-foundry/frontend/test/unit/view/applications/list/list.module.spec.js b/components/cloud-foundry/frontend/test/unit/view/applications/list/list.module.spec.js index efe18f2326..ccdd09ed74 100644 --- a/components/cloud-foundry/frontend/test/unit/view/applications/list/list.module.spec.js +++ b/components/cloud-foundry/frontend/test/unit/view/applications/list/list.module.spec.js @@ -28,6 +28,7 @@ var cfAppWallActions = $injector.get('cfAppWallActions'); var $window = $injector.get('$window'); var appLocalStorage = $injector.get('appLocalStorage'); + var appBusyService = $injector.get('appBusyService'); var userCnsiModel = modelManager.retrieve('app.model.serviceInstance.user'); if (Object.keys(userCnsiModel.serviceInstances).length === 0) { @@ -53,7 +54,7 @@ var ApplicationsListController = $state.get('cf.applications.list').controller; $controller = new ApplicationsListController($scope, $translate, $state, $timeout, $q, $window, modelManager, - errorService, appUtilsService, cfOrganizationModel, cfAppWallActions, appLocalStorage); + errorService, appUtilsService, cfOrganizationModel, cfAppWallActions, appLocalStorage, appBusyService); expect($controller).toBeDefined(); var listAllOrgs = mock.cloudFoundryAPI.Organizations.ListAllOrganizations('default');