From aba7b40c7e51c5e143e1670056e6e382b7e30457 Mon Sep 17 00:00:00 2001 From: Guilherme Lopes Date: Mon, 2 Jul 2018 20:27:48 -0500 Subject: [PATCH 01/10] added idleService --- core/XH.js | 3 +++ svc/IdleService.js | 65 ++++++++++++++++++++++++++++++++++++++++++++++ svc/index.js | 1 + 3 files changed, 69 insertions(+) create mode 100644 svc/IdleService.js diff --git a/core/XH.js b/core/XH.js index 41cc1331ca..f1006c2e59 100644 --- a/core/XH.js +++ b/core/XH.js @@ -24,6 +24,7 @@ import { FeedbackService, FetchService, IdentityService, + IdleService, LocalStorageService, PrefService, TrackService @@ -84,6 +85,7 @@ class XHClass { feedbackService = new FeedbackService(); fetchService = new FetchService(); identityService = new IdentityService(); + idleService = new IdleService(); localStorageService = new LocalStorageService(); prefService = new PrefService(); trackService = new TrackService(); @@ -346,6 +348,7 @@ class XHClass { this.environmentService, this.feedbackService, this.identityService, + this.idleService, this.trackService ); } diff --git a/svc/IdleService.js b/svc/IdleService.js new file mode 100644 index 0000000000..70a53703ee --- /dev/null +++ b/svc/IdleService.js @@ -0,0 +1,65 @@ +/* + * This file belongs to Hoist, an application development toolkit + * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) + * + * Copyright © 2018 Extremely Heavy Industries Inc. + */ +import {XH, HoistService} from '@xh/hoist/core'; +import {SECONDS, MINUTES} from '@xh/hoist/utils/DateTimeUtils'; +import {debounce, throttle} from 'lodash'; + +@HoistService() +export class IdleService { + + async initAsync() { + const delay = this.getTimeDelay(); + + if (delay > 0) { + this.resetTimer = throttle(this.resetTimer, 30 * SECONDS); + this.task = debounce(() => this.timeout(), delay, {trailing: true}); + + this.resetTimer(); + this.createAppListener(); + } + } + + getTimeDelay() { + return XH.getConf('xhIdleTimeoutMins', 0) * MINUTES; + } + + //------------------------------------- + // Implementation + //------------------------------------- + stop() { + this.task.cancel(); + this.destroyAppListener(); + + XH.handleException('This application is sleeping. Please reload to reactivate it.', { + requireReload: true, + showAsError: false, + title: 'Timeout' + }); + } + + createAppListener() { + window.addEventListener('keydown', this.resetTimer, true); + window.addEventListener('mousemove', this.resetTimer, true); + window.addEventListener('mousedown', this.resetTimer, true); + window.addEventListener('scroll', this.resetTimer, true); + } + + destroyAppListener() { + window.removeEventListener('keydown', this.resetTimer, true); + window.removeEventListener('mousemove', this.resetTimer, true); + window.removeEventListener('mousedown', this.resetTimer, true); + window.removeEventListener('scroll', this.resetTimer, true); + } + + timeout() { + this.stop(); + } + + resetTimer = () => { + this.task(); + } +} \ No newline at end of file diff --git a/svc/index.js b/svc/index.js index 99947e6ee2..8fae611eae 100644 --- a/svc/index.js +++ b/svc/index.js @@ -9,6 +9,7 @@ export * from './EnvironmentService'; export * from './ErrorTrackingService'; export * from './FeedbackService'; export * from './FetchService'; +export * from './IdleService'; export * from './LocalStorageService'; export * from './IdentityService'; export * from './PrefService'; From 0dcadf1a8b12b01f2ee62df84b21d0d0a3257497 Mon Sep 17 00:00:00 2001 From: Guilherme Lopes Date: Fri, 6 Jul 2018 01:44:52 -0500 Subject: [PATCH 02/10] XH.idleComponent should be configurable at app level --- app/AppContainer.js | 4 +++- app/impl/IdleWrapper.js | 22 ++++++++++++++++++++++ app/impl/index.js | 1 + core/HoistApp.js | 16 ++++++++++++++++ core/XH.js | 3 +++ svc/IdleService.js | 20 ++++++++------------ 6 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 app/impl/IdleWrapper.js diff --git a/app/AppContainer.js b/app/AppContainer.js index 0a13ac8cf2..d6cb3e89de 100644 --- a/app/AppContainer.js +++ b/app/AppContainer.js @@ -21,6 +21,7 @@ import { aboutDialog, exceptionDialog, impersonationBar, + idleWrapper, loginPanel, updateBar, versionBar @@ -58,7 +59,8 @@ export class AppContainer extends Component { render() { return div( this.renderContent(), - exceptionDialog() // Always render the exception dialog -- might need it :-( + exceptionDialog(), // Always render the exception dialog -- might need it :-( + idleWrapper() ); } diff --git a/app/impl/IdleWrapper.js b/app/impl/IdleWrapper.js new file mode 100644 index 0000000000..08ec32b7cd --- /dev/null +++ b/app/impl/IdleWrapper.js @@ -0,0 +1,22 @@ +/* + * This file belongs to Hoist, an application development toolkit + * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) + * + * Copyright © 2018 Extremely Heavy Industries Inc. + */ + +import {Component} from 'react'; +import {XH, HoistComponent, elemFactory} from '@xh/hoist/core'; + +@HoistComponent() +class IdleWrapper extends Component { + + render() { + const cmp = XH.idleComponent; + if (!cmp) return null; + + return cmp; + } +} + +export const idleWrapper = elemFactory(IdleWrapper); \ No newline at end of file diff --git a/app/impl/index.js b/app/impl/index.js index 16eed7ec39..012acf8818 100644 --- a/app/impl/index.js +++ b/app/impl/index.js @@ -8,6 +8,7 @@ export * from './AboutDialog'; export * from './ExceptionDialog'; export * from './ImpersonationBar'; +export * from './IdleWrapper'; export * from './LoginPanel'; export * from './UpdateBar'; export * from './VersionBar'; diff --git a/core/HoistApp.js b/core/HoistApp.js index 939186c685..0489b9de6f 100644 --- a/core/HoistApp.js +++ b/core/HoistApp.js @@ -6,6 +6,7 @@ */ import {defaultMethods} from '@xh/hoist/utils/ClassUtils'; import {HoistModel} from './HoistModel'; +import {alert} from '@xh/hoist/kit/blueprint'; /** * Mixin for defining a Hoist Application. An instance of this class will be initialized by Hoist @@ -62,6 +63,21 @@ export function HoistApp(C) { }; }, + /** + * App must implement this method with appropriate logic to display a message when the app + * has been idle for the amount of minutes defined by the `xhxhIdleTimeoutMins` config. + * @param {Function} callback - A callback function to be handled by the showIdleDialog + */ + showIdleDialog(callback) { + return alert({ + style: {paddingBottom: '20px'}, + isOpen: true, + intent: 'danger', + item: 'This application is sleeping. Please click OK to reactivate it.', + onClose: callback + }); + }, + /** * Call this once when application mounted in order to trigger initial authentication and * initialization of the application and its services. diff --git a/core/XH.js b/core/XH.js index f1006c2e59..61dd85e120 100644 --- a/core/XH.js +++ b/core/XH.js @@ -108,6 +108,9 @@ class XHClass { */ @observable.ref displayException; + /** A component to be rendered when the app times out */ + @observable idleComponent; + /** Show about dialog? */ @observable aboutIsOpen = false; diff --git a/svc/IdleService.js b/svc/IdleService.js index 70a53703ee..6217630813 100644 --- a/svc/IdleService.js +++ b/svc/IdleService.js @@ -7,6 +7,7 @@ import {XH, HoistService} from '@xh/hoist/core'; import {SECONDS, MINUTES} from '@xh/hoist/utils/DateTimeUtils'; import {debounce, throttle} from 'lodash'; +import {action} from '@xh/hoist/mobx'; @HoistService() export class IdleService { @@ -30,17 +31,6 @@ export class IdleService { //------------------------------------- // Implementation //------------------------------------- - stop() { - this.task.cancel(); - this.destroyAppListener(); - - XH.handleException('This application is sleeping. Please reload to reactivate it.', { - requireReload: true, - showAsError: false, - title: 'Timeout' - }); - } - createAppListener() { window.addEventListener('keydown', this.resetTimer, true); window.addEventListener('mousemove', this.resetTimer, true); @@ -55,8 +45,14 @@ export class IdleService { window.removeEventListener('scroll', this.resetTimer, true); } + @action timeout() { - this.stop(); + this.task.cancel(); + this.destroyAppListener(); + + XH.idleComponent = XH.app.showIdleDialog(() => { + window.location.reload(); + }); } resetTimer = () => { From d68f83e667e6db6a23f9162a93c31e508fc7f881 Mon Sep 17 00:00:00 2001 From: Guilherme Lopes Date: Fri, 6 Jul 2018 12:13:44 -0500 Subject: [PATCH 03/10] fire appSuspended event --- svc/IdleService.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/svc/IdleService.js b/svc/IdleService.js index 6217630813..a94e4edd21 100644 --- a/svc/IdleService.js +++ b/svc/IdleService.js @@ -50,6 +50,8 @@ export class IdleService { this.task.cancel(); this.destroyAppListener(); + window.dispatchEvent(new CustomEvent('appSuspended')); + XH.idleComponent = XH.app.showIdleDialog(() => { window.location.reload(); }); From b4ac8681e949c728d06d791e1aaee8d0f1601374 Mon Sep 17 00:00:00 2001 From: Guilherme Lopes Date: Fri, 6 Jul 2018 12:14:41 -0500 Subject: [PATCH 04/10] appLevel/userLevel disable idle detection --- svc/IdleService.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/svc/IdleService.js b/svc/IdleService.js index a94e4edd21..4057fe22cb 100644 --- a/svc/IdleService.js +++ b/svc/IdleService.js @@ -13,9 +13,11 @@ import {action} from '@xh/hoist/mobx'; export class IdleService { async initAsync() { - const delay = this.getTimeDelay(); + const appDisablesDetection = XH.app.disableIdleDetection, + userDisablesDetection = XH.getPref('xh.disableIdleDetection', false), + delay = this.getTimeDelay(); - if (delay > 0) { + if (!appDisablesDetection && !userDisablesDetection && delay > 0) { this.resetTimer = throttle(this.resetTimer, 30 * SECONDS); this.task = debounce(() => this.timeout(), delay, {trailing: true}); From a2f2c3a1c60ba19f87b35d7c98846bf114c45d61 Mon Sep 17 00:00:00 2001 From: Guilherme Lopes Date: Fri, 6 Jul 2018 12:17:50 -0500 Subject: [PATCH 05/10] Cancel all tasks --- svc/IdleService.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/svc/IdleService.js b/svc/IdleService.js index 4057fe22cb..570cd14589 100644 --- a/svc/IdleService.js +++ b/svc/IdleService.js @@ -7,6 +7,7 @@ import {XH, HoistService} from '@xh/hoist/core'; import {SECONDS, MINUTES} from '@xh/hoist/utils/DateTimeUtils'; import {debounce, throttle} from 'lodash'; +import {Timer} from '@xh/hoist/utils/Timer'; import {action} from '@xh/hoist/mobx'; @HoistService() @@ -51,9 +52,9 @@ export class IdleService { timeout() { this.task.cancel(); this.destroyAppListener(); + Timer.cancelAll(); window.dispatchEvent(new CustomEvent('appSuspended')); - XH.idleComponent = XH.app.showIdleDialog(() => { window.location.reload(); }); From 9fc960d25148f1b152c5e4c14afa7791494a3129 Mon Sep 17 00:00:00 2001 From: Guilherme Lopes Date: Mon, 9 Jul 2018 13:32:53 -0500 Subject: [PATCH 06/10] only add idleWrapper if LoadState is COMPLETE --- app/AppContainer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/AppContainer.js b/app/AppContainer.js index d6cb3e89de..981067b6b5 100644 --- a/app/AppContainer.js +++ b/app/AppContainer.js @@ -59,8 +59,7 @@ export class AppContainer extends Component { render() { return div( this.renderContent(), - exceptionDialog(), // Always render the exception dialog -- might need it :-( - idleWrapper() + exceptionDialog() // Always render the exception dialog -- might need it :-( ); } @@ -90,7 +89,8 @@ export class AppContainer extends Component { loadMask({model: XH.appLoadModel}), messageSource({model: XH.messageSourceModel}), feedbackDialog({model: XH.feedbackDialogModel}), - aboutDialog() + aboutDialog(), + idleWrapper() ); default: return null; From f91451dda9584e194565d99cd55b4466f767abe2 Mon Sep 17 00:00:00 2001 From: Guilherme Lopes Date: Mon, 9 Jul 2018 14:27:28 -0500 Subject: [PATCH 07/10] removed idleWrapper --- app/AppContainer.js | 3 +-- app/impl/IdleWrapper.js | 22 ---------------------- app/impl/index.js | 1 - 3 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 app/impl/IdleWrapper.js diff --git a/app/AppContainer.js b/app/AppContainer.js index 981067b6b5..52951f92d1 100644 --- a/app/AppContainer.js +++ b/app/AppContainer.js @@ -21,7 +21,6 @@ import { aboutDialog, exceptionDialog, impersonationBar, - idleWrapper, loginPanel, updateBar, versionBar @@ -90,7 +89,7 @@ export class AppContainer extends Component { messageSource({model: XH.messageSourceModel}), feedbackDialog({model: XH.feedbackDialogModel}), aboutDialog(), - idleWrapper() + XH.idleComponent ? XH.idleComponent : null ); default: return null; diff --git a/app/impl/IdleWrapper.js b/app/impl/IdleWrapper.js deleted file mode 100644 index 08ec32b7cd..0000000000 --- a/app/impl/IdleWrapper.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * This file belongs to Hoist, an application development toolkit - * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) - * - * Copyright © 2018 Extremely Heavy Industries Inc. - */ - -import {Component} from 'react'; -import {XH, HoistComponent, elemFactory} from '@xh/hoist/core'; - -@HoistComponent() -class IdleWrapper extends Component { - - render() { - const cmp = XH.idleComponent; - if (!cmp) return null; - - return cmp; - } -} - -export const idleWrapper = elemFactory(IdleWrapper); \ No newline at end of file diff --git a/app/impl/index.js b/app/impl/index.js index 012acf8818..16eed7ec39 100644 --- a/app/impl/index.js +++ b/app/impl/index.js @@ -8,7 +8,6 @@ export * from './AboutDialog'; export * from './ExceptionDialog'; export * from './ImpersonationBar'; -export * from './IdleWrapper'; export * from './LoginPanel'; export * from './UpdateBar'; export * from './VersionBar'; From 2ee13c2ea1499481408d09721015741ba6bb7fa8 Mon Sep 17 00:00:00 2001 From: Guilherme Lopes Date: Mon, 9 Jul 2018 14:27:39 -0500 Subject: [PATCH 08/10] code cleanup --- core/HoistApp.js | 6 +++--- svc/IdleService.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/HoistApp.js b/core/HoistApp.js index 0489b9de6f..c0689a5e71 100644 --- a/core/HoistApp.js +++ b/core/HoistApp.js @@ -64,11 +64,11 @@ export function HoistApp(C) { }, /** - * App must implement this method with appropriate logic to display a message when the app - * has been idle for the amount of minutes defined by the `xhxhIdleTimeoutMins` config. + * App may override this method with appropriate logic to display a message when the app + * has been idle for the amount of minutes defined by the `xhIdleTimeoutMins` config. * @param {Function} callback - A callback function to be handled by the showIdleDialog */ - showIdleDialog(callback) { + renderIdleDialog(callback) { return alert({ style: {paddingBottom: '20px'}, isOpen: true, diff --git a/svc/IdleService.js b/svc/IdleService.js index 570cd14589..980e0c4a8f 100644 --- a/svc/IdleService.js +++ b/svc/IdleService.js @@ -52,11 +52,11 @@ export class IdleService { timeout() { this.task.cancel(); this.destroyAppListener(); - Timer.cancelAll(); + this.fireEvent('appSuspended'); - window.dispatchEvent(new CustomEvent('appSuspended')); - XH.idleComponent = XH.app.showIdleDialog(() => { - window.location.reload(); + Timer.cancelAll(); + XH.idleComponent = XH.app.renderIdleDialog(() => { + window.location.reload(true); }); } From 57fcd340851597b2b80b8f320381ae1ce44c20db Mon Sep 17 00:00:00 2001 From: lbwexler Date: Tue, 10 Jul 2018 23:43:25 -0400 Subject: [PATCH 09/10] + Refactor of idleDetection/AppSuspension + enhancement to MessageModel for direct open + LoadState => AppState --- admin/App.js | 4 +- admin/AppComponent.js | 2 +- app/index.js | 9 --- cmp/message/MessageModel.js | 1 + core/AppState.js | 22 ++++++ core/HoistApp.js | 30 ++++---- core/XH.js | 53 +++++++------- core/index.js | 1 + {app/impl => impl}/AboutDialog.js | 0 {app/impl => impl}/AboutDialog.scss | 0 {app => impl}/AppContainer.js | 55 +++++++++------ {app/impl => impl}/ExceptionDialog.js | 0 {app/impl => impl}/ExceptionDialogDetails.js | 0 {app/impl => impl}/ExceptionDialogModel.js | 0 {app/impl => impl}/FeedbackDialog.js | 0 {app/impl => impl}/FeedbackDialogModel.js | 0 {app/impl => impl}/ImpersonationBar.js | 0 {app/impl => impl}/ImpersonationBarModel.js | 0 {app => impl}/LockoutPanel.js | 2 +- {app => impl}/LockoutPanel.scss | 0 {app/impl => impl}/LoginPanel.js | 0 {app/impl => impl}/LoginPanel.scss | 0 impl/SuspendedDialog.js | 41 +++++++++++ {app/impl => impl}/UpdateBar.js | 0 {app/impl => impl}/UpdateBar.scss | 0 {app/impl => impl}/VersionBar.js | 0 {app/impl => impl}/VersionBar.scss | 0 {app/impl => impl}/index.js | 4 +- svc/ConfigService.js | 6 +- svc/IdleService.js | 74 +++++++++----------- 30 files changed, 186 insertions(+), 118 deletions(-) delete mode 100644 app/index.js create mode 100644 core/AppState.js rename {app/impl => impl}/AboutDialog.js (100%) rename {app/impl => impl}/AboutDialog.scss (100%) rename {app => impl}/AppContainer.js (71%) rename {app/impl => impl}/ExceptionDialog.js (100%) rename {app/impl => impl}/ExceptionDialogDetails.js (100%) rename {app/impl => impl}/ExceptionDialogModel.js (100%) rename {app/impl => impl}/FeedbackDialog.js (100%) rename {app/impl => impl}/FeedbackDialogModel.js (100%) rename {app/impl => impl}/ImpersonationBar.js (100%) rename {app/impl => impl}/ImpersonationBarModel.js (100%) rename {app => impl}/LockoutPanel.js (96%) rename {app => impl}/LockoutPanel.scss (100%) rename {app/impl => impl}/LoginPanel.js (100%) rename {app/impl => impl}/LoginPanel.scss (100%) create mode 100644 impl/SuspendedDialog.js rename {app/impl => impl}/UpdateBar.js (100%) rename {app/impl => impl}/UpdateBar.scss (100%) rename {app/impl => impl}/VersionBar.js (100%) rename {app/impl => impl}/VersionBar.scss (100%) rename {app/impl => impl}/index.js (80%) diff --git a/admin/App.js b/admin/App.js index dfbe56b2e9..74f7f8c93e 100644 --- a/admin/App.js +++ b/admin/App.js @@ -42,7 +42,9 @@ export class App { } get componentClass() {return AppComponent} - + + get idleDetectionDisabled() {return true} + @action requestRefresh() { this.tabModel.requestRefresh(); diff --git a/admin/AppComponent.js b/admin/AppComponent.js index c2a1b7b7ce..5216240cda 100644 --- a/admin/AppComponent.js +++ b/admin/AppComponent.js @@ -7,7 +7,7 @@ import {Component} from 'react'; import {HoistComponent, XH} from '@xh/hoist/core'; -import {lockoutPanel} from '@xh/hoist/app'; +import {lockoutPanel} from '@xh/hoist/impl'; import {tabContainer, tabSwitcher} from '@xh/hoist/cmp/tab'; import {frame, panel} from '@xh/hoist/cmp/layout'; import {button} from '@xh/hoist/cmp/button'; diff --git a/app/index.js b/app/index.js deleted file mode 100644 index 373e1872e9..0000000000 --- a/app/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * This file belongs to Hoist, an application development toolkit - * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) - * - * Copyright © 2018 Extremely Heavy Industries Inc. - */ - -export * from './AppContainer'; -export * from './LockoutPanel'; \ No newline at end of file diff --git a/cmp/message/MessageModel.js b/cmp/message/MessageModel.js index a7e2b9b9f0..462404122a 100644 --- a/cmp/message/MessageModel.js +++ b/cmp/message/MessageModel.js @@ -58,6 +58,7 @@ export class MessageModel { */ constructor(config) { this.initialConfig = config; + if (config.isOpen) this.show(); } /** diff --git a/core/AppState.js b/core/AppState.js new file mode 100644 index 0000000000..bc9950bc47 --- /dev/null +++ b/core/AppState.js @@ -0,0 +1,22 @@ +/* + * This file belongs to Hoist, an application development toolkit + * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) + * + * Copyright © 2018 Extremely Heavy Industries Inc. + */ + + +/** + * Enumeration of possible App States + * + * @see XH.appState. + */ +export const AppState = { + PRE_AUTH: 'PRE_AUTH', + LOGIN_REQUIRED: 'LOGIN_REQUIRED', + ACCESS_DENIED: 'ACCESS_DENIED', + INITIALIZING: 'INITIALIZING', + RUNNING: 'RUNNING', + SUSPENDED: 'SUSPENDED', + LOAD_FAILED: 'LOAD_FAILED' +}; diff --git a/core/HoistApp.js b/core/HoistApp.js index c0689a5e71..58292607bf 100644 --- a/core/HoistApp.js +++ b/core/HoistApp.js @@ -5,8 +5,8 @@ * Copyright © 2018 Extremely Heavy Industries Inc. */ import {defaultMethods} from '@xh/hoist/utils/ClassUtils'; +import {Icon} from '@xh/hoist/icon'; import {HoistModel} from './HoistModel'; -import {alert} from '@xh/hoist/kit/blueprint'; /** * Mixin for defining a Hoist Application. An instance of this class will be initialized by Hoist @@ -64,18 +64,24 @@ export function HoistApp(C) { }, /** - * App may override this method with appropriate logic to display a message when the app - * has been idle for the amount of minutes defined by the `xhIdleTimeoutMins` config. - * @param {Function} callback - A callback function to be handled by the showIdleDialog + * Is app suspension by IdleService disabled? + * + * @see IdleService. App suspension is also configurable in soft config, and via user preference. */ - renderIdleDialog(callback) { - return alert({ - style: {paddingBottom: '20px'}, - isOpen: true, - intent: 'danger', - item: 'This application is sleeping. Please click OK to reactivate it.', - onClose: callback - }); + idleDetectionDisabled: { + get() {return false} + }, + + /** + * Component to indicate App has been suspended. + * + * The component will receive a single prop -- onReactivate -- a callback called when user has acknowledged + * the suspension and wishes to reload the app and continue working. + * + * @see IdleService. + */ + suspendedDialogClass: { + get() {return null} }, /** diff --git a/core/XH.js b/core/XH.js index 61dd85e120..6980302273 100644 --- a/core/XH.js +++ b/core/XH.js @@ -8,12 +8,12 @@ import ReactDOM from 'react-dom'; import {isPlainObject, defaults} from 'lodash'; -import {elem, HoistModel} from '@xh/hoist/core'; +import {elem, HoistModel, AppState} from '@xh/hoist/core'; import {Exception, ExceptionHandler} from '@xh/hoist/exception'; import {observable, setter, action} from '@xh/hoist/mobx'; import {MultiPromiseModel, never} from '@xh/hoist/promise'; import {RouterModel} from '@xh/hoist/router'; -import {appContainer} from '@xh/hoist/app'; +import {appContainer} from '@xh/hoist/impl'; import {MessageSourceModel} from '@xh/hoist/cmp/message'; import {throwIf} from '@xh/hoist/utils/JsUtils'; @@ -93,8 +93,8 @@ class XHClass { //----------------------------- // Observable State //----------------------------- - /** State of app loading -- see HoistLoadState for valid values. */ - @setter @observable loadState = LoadState.PRE_AUTH; + /** State of app -- see AppState for valid values. */ + @observable appState = AppState.PRE_AUTH; /** Currently authenticated user. */ @observable authUsername = null; @@ -157,6 +157,21 @@ class XHClass { ReactDOM.render(rootView, document.getElementById('root')); } + /** + * Transition the application state. + * + * @param {AppState} appState - state to transition to. + * + * Used by framework. Not intended for application use. + */ + @action + setAppState(appState) { + if (this.appState != appState) { + this.appState = appState; + this.fireEvent('appStateChanged', {appState}); + } + } + /** Route the app. See RouterModel.navigate. */ navigate(...args) { this.routerModel.navigate(...args); @@ -274,25 +289,26 @@ class XHClass { * Not for application use. */ async initAsync() { + const S = AppState; // Add xh-app class to body element to power Hoist CSS selectors document.body.classList.add('xh-app'); try { - this.setLoadState(LoadState.PRE_AUTH); + this.setAppState(S.PRE_AUTH); const authUser = await this.getAuthUserFromServerAsync(); if (!authUser) { throwIf(this.app.requireSSO, 'Failed to authenticate user via SSO.'); - this.setLoadState(LoadState.LOGIN_REQUIRED); + this.setAppState(S.LOGIN_REQUIRED); return; } await this.completeInitAsync(authUser.username); } catch (e) { - this.setLoadState(LoadState.FAILED); + this.setAppState(S.LOAD_FAILED); XH.handleException(e, {requireReload: true}); } } @@ -304,7 +320,9 @@ class XHClass { */ @action async completeInitAsync() { - this.setLoadState(LoadState.INITIALIZING); + const S = AppState; + + this.setAppState(S.INITIALIZING); try { await this.initServicesAsync() .wait(100); // delay is workaround for styling issues in dev TODO: Remove @@ -314,15 +332,15 @@ class XHClass { const access = this.app.checkAccess(XH.getUser()); if (!access.hasAccess) { this.accessDeniedMessage = access.message || 'Access denied.'; - this.setLoadState(LoadState.ACCESS_DENIED); + this.setAppState(S.ACCESS_DENIED); return; } await this.app.initAsync(); this.initRouterModel(); - this.setLoadState(LoadState.COMPLETE); + this.setAppState(S.RUNNING); } catch (e) { - this.setLoadState(LoadState.FAILED); + this.setAppState(S.LOAD_FAILED); XH.handleException(e, {requireReload: true}); } } @@ -425,16 +443,3 @@ class XHClass { } export const XH = window.XH = new XHClass(); -/** - * Enumeration of possible Load States for Hoist. - * - * See XH.loadState. - */ -export const LoadState = { - PRE_AUTH: 'PRE_AUTH', - LOGIN_REQUIRED: 'LOGIN_REQUIRED', - ACCESS_DENIED: 'ACCESS_DENIED', - INITIALIZING: 'INITIALIZING', - COMPLETE: 'COMPLETE', - FAILED: 'FAILED' -}; diff --git a/core/index.js b/core/index.js index c71bf28961..10710f0f9d 100644 --- a/core/index.js +++ b/core/index.js @@ -8,6 +8,7 @@ export * from './elem'; export * from './mixins/Reactive'; export * from './mixins/EventTarget'; +export * from './AppState'; export * from './HoistApp'; export * from './HoistComponent'; export * from './HoistModel'; diff --git a/app/impl/AboutDialog.js b/impl/AboutDialog.js similarity index 100% rename from app/impl/AboutDialog.js rename to impl/AboutDialog.js diff --git a/app/impl/AboutDialog.scss b/impl/AboutDialog.scss similarity index 100% rename from app/impl/AboutDialog.scss rename to impl/AboutDialog.scss diff --git a/app/AppContainer.js b/impl/AppContainer.js similarity index 71% rename from app/AppContainer.js rename to impl/AppContainer.js index 52951f92d1..c4e4e8ba3c 100644 --- a/app/AppContainer.js +++ b/impl/AppContainer.js @@ -8,7 +8,7 @@ import {Children, Component} from 'react'; import {ContextMenuTarget} from '@xh/hoist/kit/blueprint'; import {observable, observer, setter} from '@xh/hoist/mobx'; -import {elemFactory, LoadState, XH} from '@xh/hoist/core'; +import {elemFactory, elem, AppState, XH} from '@xh/hoist/core'; import {contextMenu} from '@xh/hoist/cmp/contextmenu'; import {loadMask} from '@xh/hoist/cmp/mask'; import {messageSource} from '@xh/hoist/cmp/message'; @@ -23,24 +23,25 @@ import { impersonationBar, loginPanel, updateBar, - versionBar -} from './impl'; - -import {lockoutPanel} from './'; + versionBar, + lockoutPanel, + SuspendedDialog +} from './'; /** * Top-level wrapper to provide core Hoist Application layout and infrastructure to an application's - * root Component. Provides initialized Hoist services and a standard viewport that also includes - * standard UI elements such as an impersonation bar header, version bar footer, app-wide load mask, - * context menu, and popup message support. + * root Component. Provides a standard viewport that includes standard UI elements such as an + * impersonation bar header, version bar footer, an app-wide load mask, a base context menu, + * popup message support, and exception rendering. * - * Construction of this container triggers the init of the core XH singleton, which queries for an - * authorized user and then proceeds to init all core Hoist and app-level services. + * Successful construction of this container indicates that all app code has been loaded, and is + * being mounted. This triggers a call to XH.initAsync() to begin the Hoist and Application loading + * lifecycle. * - * If the user is not yet known (and the app does *not* use SSO), this container will display a - * standardized loginPanel component to prompt for a username and password. Once the user is - * confirmed, this container will again mask until Hoist has completed its initialization, at - * which point the app's UI will be rendered. + * If the application is in the 'LOGIN_REQUIRED' state, this container will display a + * standardized loginPanel component to prompt for a username and password. During loading and initialization + * this componenent will render standard masks. Once the application has reached "RUNNING" state it will be fully + * rendered. * * @private */ @@ -63,21 +64,23 @@ export class AppContainer extends Component { } renderContent() { + const S = AppState; if (this.caughtException) return null; - switch (XH.loadState) { - case LoadState.PRE_AUTH: - case LoadState.INITIALIZING: + switch (XH.appState) { + case S.PRE_AUTH: + case S.INITIALIZING: return viewport(loadMask({isDisplayed: true})); - case LoadState.LOGIN_REQUIRED: + case S.LOGIN_REQUIRED: return loginPanel(); - case LoadState.ACCESS_DENIED: + case S.ACCESS_DENIED: return lockoutPanel({ message: this.unauthorizedMessage() }); - case LoadState.FAILED: + case S.LOAD_FAILED: return null; - case LoadState.COMPLETE: + case S.RUNNING: + case S.SUSPENDED: return viewport( vframe( impersonationBar(), @@ -89,7 +92,7 @@ export class AppContainer extends Component { messageSource({model: XH.messageSourceModel}), feedbackDialog({model: XH.feedbackDialogModel}), aboutDialog(), - XH.idleComponent ? XH.idleComponent : null + this.renderSuspendedDialog() ); default: return null; @@ -128,6 +131,14 @@ export class AppContainer extends Component { //------------------------ // Implementation //------------------------ + renderSuspendedDialog() { + const dialogClass = XH.app.suspendedDialogClass || SuspendedDialog; + + return XH.appState == AppState.SUSPENDED && dialogClass ? + elem(dialogClass, {onReactivate: () => XH.reloadApp()}) : + null; + } + unauthorizedMessage() { const user = XH.getUser(); diff --git a/app/impl/ExceptionDialog.js b/impl/ExceptionDialog.js similarity index 100% rename from app/impl/ExceptionDialog.js rename to impl/ExceptionDialog.js diff --git a/app/impl/ExceptionDialogDetails.js b/impl/ExceptionDialogDetails.js similarity index 100% rename from app/impl/ExceptionDialogDetails.js rename to impl/ExceptionDialogDetails.js diff --git a/app/impl/ExceptionDialogModel.js b/impl/ExceptionDialogModel.js similarity index 100% rename from app/impl/ExceptionDialogModel.js rename to impl/ExceptionDialogModel.js diff --git a/app/impl/FeedbackDialog.js b/impl/FeedbackDialog.js similarity index 100% rename from app/impl/FeedbackDialog.js rename to impl/FeedbackDialog.js diff --git a/app/impl/FeedbackDialogModel.js b/impl/FeedbackDialogModel.js similarity index 100% rename from app/impl/FeedbackDialogModel.js rename to impl/FeedbackDialogModel.js diff --git a/app/impl/ImpersonationBar.js b/impl/ImpersonationBar.js similarity index 100% rename from app/impl/ImpersonationBar.js rename to impl/ImpersonationBar.js diff --git a/app/impl/ImpersonationBarModel.js b/impl/ImpersonationBarModel.js similarity index 100% rename from app/impl/ImpersonationBarModel.js rename to impl/ImpersonationBarModel.js diff --git a/app/LockoutPanel.js b/impl/LockoutPanel.js similarity index 96% rename from app/LockoutPanel.js rename to impl/LockoutPanel.js index 6bbd3afd37..f609d430ac 100644 --- a/app/LockoutPanel.js +++ b/impl/LockoutPanel.js @@ -11,7 +11,7 @@ import {box, filler, vframe, viewport} from '@xh/hoist/cmp/layout'; import {PropTypes as PT} from 'prop-types'; import './LockoutPanel.scss'; -import {impersonationBar} from './impl'; +import {impersonationBar} from './'; /** * Panel for display to prevent user access to all content. diff --git a/app/LockoutPanel.scss b/impl/LockoutPanel.scss similarity index 100% rename from app/LockoutPanel.scss rename to impl/LockoutPanel.scss diff --git a/app/impl/LoginPanel.js b/impl/LoginPanel.js similarity index 100% rename from app/impl/LoginPanel.js rename to impl/LoginPanel.js diff --git a/app/impl/LoginPanel.scss b/impl/LoginPanel.scss similarity index 100% rename from app/impl/LoginPanel.scss rename to impl/LoginPanel.scss diff --git a/impl/SuspendedDialog.js b/impl/SuspendedDialog.js new file mode 100644 index 0000000000..4fe4162bd2 --- /dev/null +++ b/impl/SuspendedDialog.js @@ -0,0 +1,41 @@ +/* + * This file belongs to Hoist, an application development toolkit + * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) + * + * Copyright © 2018 Extremely Heavy Industries Inc. + */ + +import {Component} from 'react'; +import {dialog} from '@xh/hoist/kit/blueprint'; +import {XH, HoistComponent, elemFactory, AppState} from '@xh/hoist/core'; +import {frame, table, tbody, tr, th, td, filler} from '@xh/hoist/cmp/layout'; +import {toolbar} from '@xh/hoist/cmp/toolbar'; +import {button} from '@xh/hoist/cmp/button'; +import {Icon} from '@xh/hoist/icon'; +import {MessageModel, message} from '@xh/hoist/cmp/message'; +import './AboutDialog.scss'; + +/** + * Default display of application suspension. + * + * This display can be overridden by applications. + * @see HoistApp.suspendedDialogClass + * + * @private + */ +@HoistComponent() +export class SuspendedDialog extends Component { + + localModel = new MessageModel({ + title: 'Application Sleeping', + icon: Icon.moon(), + message: 'This application is sleeping due to inactivity. Please click below to reload it.', + confirmText: 'Reload', + onConfirm: this.props.onReactivate, + isOpen: true + }); + + render() { + return message({model: this.model}); + } +} \ No newline at end of file diff --git a/app/impl/UpdateBar.js b/impl/UpdateBar.js similarity index 100% rename from app/impl/UpdateBar.js rename to impl/UpdateBar.js diff --git a/app/impl/UpdateBar.scss b/impl/UpdateBar.scss similarity index 100% rename from app/impl/UpdateBar.scss rename to impl/UpdateBar.scss diff --git a/app/impl/VersionBar.js b/impl/VersionBar.js similarity index 100% rename from app/impl/VersionBar.js rename to impl/VersionBar.js diff --git a/app/impl/VersionBar.scss b/impl/VersionBar.scss similarity index 100% rename from app/impl/VersionBar.scss rename to impl/VersionBar.scss diff --git a/app/impl/index.js b/impl/index.js similarity index 80% rename from app/impl/index.js rename to impl/index.js index 16eed7ec39..7cf3873f45 100644 --- a/app/impl/index.js +++ b/impl/index.js @@ -4,11 +4,13 @@ * * Copyright © 2018 Extremely Heavy Industries Inc. */ - +export * from './AppContainer'; export * from './AboutDialog'; export * from './ExceptionDialog'; export * from './ImpersonationBar'; +export * from './LockoutPanel'; export * from './LoginPanel'; export * from './UpdateBar'; export * from './VersionBar'; export * from './FeedbackDialog'; +export * from './SuspendedDialog'; diff --git a/svc/ConfigService.js b/svc/ConfigService.js index 44e021f681..53aa8690d6 100644 --- a/svc/ConfigService.js +++ b/svc/ConfigService.js @@ -18,11 +18,7 @@ import {cloneDeep} from 'lodash'; * * Note that for a config to be available here on the client, it must have its `clientVisible` flag * set to true. This is to provide support for configurations that should *not* be sent down for - * possible inspection by end-users. - * - * Configs can be specified on the server with support for different values per supported Hoist - * Environment. Note however that the client only receives the values for the current environment. - * (Values for non-production environments inherit from production if left unspecified.) + * possible inspection by end users. * * Note that this service does *not* currently attempt to reload or update configs once the client * application has loaded. A refresh of the application is required to load new entries. diff --git a/svc/IdleService.js b/svc/IdleService.js index 980e0c4a8f..db2b9d86cc 100644 --- a/svc/IdleService.js +++ b/svc/IdleService.js @@ -4,63 +4,53 @@ * * Copyright © 2018 Extremely Heavy Industries Inc. */ -import {XH, HoistService} from '@xh/hoist/core'; -import {SECONDS, MINUTES} from '@xh/hoist/utils/DateTimeUtils'; -import {debounce, throttle} from 'lodash'; +import {XH, HoistService, AppState} from '@xh/hoist/core'; +import {MINUTES} from '@xh/hoist/utils/DateTimeUtils'; +import {debounce} from 'lodash'; import {Timer} from '@xh/hoist/utils/Timer'; -import {action} from '@xh/hoist/mobx'; +/** + * Manage the idling/suspension of this application after a certain period of user + * inactivity. + * + * This service is goverened by the property App.disableIdleDetection, the configuration + * 'xhIdleTimeoutMins', and the user-specific preference 'xh.disableIdleDetection' respectively. + * Any of these can be used to disable app suspension. + */ @HoistService() export class IdleService { - async initAsync() { - const appDisablesDetection = XH.app.disableIdleDetection, - userDisablesDetection = XH.getPref('xh.disableIdleDetection', false), - delay = this.getTimeDelay(); - - if (!appDisablesDetection && !userDisablesDetection && delay > 0) { - this.resetTimer = throttle(this.resetTimer, 30 * SECONDS); - this.task = debounce(() => this.timeout(), delay, {trailing: true}); + ACTIVITY_EVENTS = ['keydown', 'mousemove', 'mousedown', 'scroll']; - this.resetTimer(); - this.createAppListener(); + async initAsync() { + const timeout = XH.getConf('xhIdleTimeoutMins') * MINUTES, + appDisabled = XH.app.idleDetectionDisabled, + configDisabled = timeout <= 0, + userDisabled = XH.getPref('xhIdleDetectionDisabled'); + + if (!appDisabled && !configDisabled && !userDisabled) { + this.startCountdown = debounce(() => this.suspendApp(), timeout, {trailing: true}); + this.startCountdown(); + this.createAppListeners(); } } - getTimeDelay() { - return XH.getConf('xhIdleTimeoutMins', 0) * MINUTES; - } - //------------------------------------- // Implementation //------------------------------------- - createAppListener() { - window.addEventListener('keydown', this.resetTimer, true); - window.addEventListener('mousemove', this.resetTimer, true); - window.addEventListener('mousedown', this.resetTimer, true); - window.addEventListener('scroll', this.resetTimer, true); + createAppListeners() { + this.ACTIVITY_EVENTS.forEach(e => addEventListener(e, this.startCountdown, true)); } - destroyAppListener() { - window.removeEventListener('keydown', this.resetTimer, true); - window.removeEventListener('mousemove', this.resetTimer, true); - window.removeEventListener('mousedown', this.resetTimer, true); - window.removeEventListener('scroll', this.resetTimer, true); + destroyAppListeners() { + this.ACTIVITY_EVENTS.forEach(e => removeEventListener(e, this.startCountdown, true)); } - @action - timeout() { - this.task.cancel(); - this.destroyAppListener(); - this.fireEvent('appSuspended'); - - Timer.cancelAll(); - XH.idleComponent = XH.app.renderIdleDialog(() => { - window.location.reload(true); - }); - } - - resetTimer = () => { - this.task(); + suspendApp() { + if (XH.appState != AppState.SUSPENDED) { + XH.setAppState(AppState.SUSPENDED); + this.destroyAppListeners(); + Timer.cancelAll(); + } } } \ No newline at end of file From bc7287ebd1dca786805f97e81a18aba631c40b0a Mon Sep 17 00:00:00 2001 From: lbwexler Date: Wed, 11 Jul 2018 00:01:37 -0400 Subject: [PATCH 10/10] Linting and merge fixes --- core/HoistApp.js | 1 - core/XH.js | 5 ++--- impl/SuspendedDialog.js | 7 +------ 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/core/HoistApp.js b/core/HoistApp.js index 58292607bf..dc8f08a3fd 100644 --- a/core/HoistApp.js +++ b/core/HoistApp.js @@ -5,7 +5,6 @@ * Copyright © 2018 Extremely Heavy Industries Inc. */ import {defaultMethods} from '@xh/hoist/utils/ClassUtils'; -import {Icon} from '@xh/hoist/icon'; import {HoistModel} from './HoistModel'; /** diff --git a/core/XH.js b/core/XH.js index d78718bad2..d57882baf5 100644 --- a/core/XH.js +++ b/core/XH.js @@ -10,7 +10,7 @@ import {isPlainObject, defaults, flatten} from 'lodash'; import {elem, HoistModel, AppState} from '@xh/hoist/core'; import {Exception, ExceptionHandler} from '@xh/hoist/exception'; -import {observable, setter, action} from '@xh/hoist/mobx'; +import {observable, action} from '@xh/hoist/mobx'; import {MultiPromiseModel, never} from '@xh/hoist/promise'; import {RouterModel} from '@xh/hoist/router'; import {appContainer} from '@xh/hoist/impl'; @@ -357,9 +357,8 @@ class XHClass { } await this.app.initAsync(); - this.initRouterModel(); - this.setAppState(S.RUNNING); this.startRouter(); + this.setAppState(S.RUNNING); } catch (e) { this.setAppState(S.LOAD_FAILED); XH.handleException(e, {requireReload: true}); diff --git a/impl/SuspendedDialog.js b/impl/SuspendedDialog.js index 4fe4162bd2..f989f7bc53 100644 --- a/impl/SuspendedDialog.js +++ b/impl/SuspendedDialog.js @@ -6,14 +6,9 @@ */ import {Component} from 'react'; -import {dialog} from '@xh/hoist/kit/blueprint'; -import {XH, HoistComponent, elemFactory, AppState} from '@xh/hoist/core'; -import {frame, table, tbody, tr, th, td, filler} from '@xh/hoist/cmp/layout'; -import {toolbar} from '@xh/hoist/cmp/toolbar'; -import {button} from '@xh/hoist/cmp/button'; +import {HoistComponent} from '@xh/hoist/core'; import {Icon} from '@xh/hoist/icon'; import {MessageModel, message} from '@xh/hoist/cmp/message'; -import './AboutDialog.scss'; /** * Default display of application suspension.