Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IdleService #427

Merged
merged 11 commits into from
Jul 11, 2018
4 changes: 3 additions & 1 deletion admin/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export class App {
}

get componentClass() {return AppComponent}


get idleDetectionDisabled() {return true}

@action
requestRefresh() {
this.tabModel.requestRefresh();
Expand Down
2 changes: 1 addition & 1 deletion admin/AppComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {panel} from '@xh/hoist/cmp/layout';
import {button} from '@xh/hoist/cmp/button';
Expand Down
9 changes: 0 additions & 9 deletions app/index.js

This file was deleted.

1 change: 1 addition & 0 deletions cmp/message/MessageModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class MessageModel {
*/
constructor(config) {
this.initialConfig = config;
if (config.isOpen) this.show();
}

/**
Expand Down
22 changes: 22 additions & 0 deletions core/AppState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* This file belongs to Hoist, an application development toolkit
* developed by Extremely Heavy Industries (www.xh.io | [email protected])
*
* 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'
};
21 changes: 21 additions & 0 deletions core/HoistApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,27 @@ export function HoistApp(C) {
};
},

/**
* Is app suspension by IdleService disabled?
*
* @see IdleService. App suspension is also configurable in soft config, and via user preference.
*/
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}
},

/**
* Call this once when application mounted in order to trigger initial authentication and
* initialization of the application and its services.
Expand Down
61 changes: 35 additions & 26 deletions core/XH.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
import ReactDOM from 'react-dom';
import {isPlainObject, defaults, flatten} 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 {observable, 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';

Expand All @@ -24,6 +24,7 @@ import {
FeedbackService,
FetchService,
IdentityService,
IdleService,
LocalStorageService,
PrefService,
TrackService
Expand Down Expand Up @@ -84,15 +85,16 @@ class XHClass {
feedbackService = new FeedbackService();
fetchService = new FetchService();
identityService = new IdentityService();
idleService = new IdleService();
localStorageService = new LocalStorageService();
prefService = new PrefService();
trackService = new TrackService();

//-----------------------------
// 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;
Expand Down Expand Up @@ -152,6 +154,22 @@ 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});
}
}


/** Trigger a full reload of the app. */
@action
reloadApp() {
Expand Down Expand Up @@ -261,7 +279,7 @@ class XHClass {
/**
* Show a modal 'confirm' dialog with message and default OK/Cancel buttons.
*
* @param {Object} config - see MessageModel.show() for available options.
* @param {Object} config - see MessageModel.show() for available options.
*/
confirm(config) {
config = defaults({}, config, {confirmText: 'OK', cancelText: 'Cancel'});
Expand Down Expand Up @@ -289,25 +307,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});
}
}
Expand All @@ -319,7 +338,9 @@ class XHClass {
*/
@action
async completeInitAsync() {
this.setLoadState(LoadState.INITIALIZING);
const S = AppState;

this.setAppState(S.INITIALIZING);
try {
// Delay to workaround hot-reload styling issues in dev.
const delay = XH.isDevelopmentMode ? 300 : 1;
Expand All @@ -331,15 +352,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.startRouter();
this.setLoadState(LoadState.COMPLETE);
this.setAppState(S.RUNNING);
} catch (e) {
this.setLoadState(LoadState.FAILED);
this.setAppState(S.LOAD_FAILED);
XH.handleException(e, {requireReload: true});
}
}
Expand Down Expand Up @@ -368,6 +389,7 @@ class XHClass {
this.environmentService,
this.feedbackService,
this.identityService,
this.idleService,
this.trackService
);
}
Expand Down Expand Up @@ -442,16 +464,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'
};
1 change: 1 addition & 0 deletions core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
File renamed without changes.
File renamed without changes.
56 changes: 34 additions & 22 deletions app/AppContainer.js → impl/AppContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
*/
Expand All @@ -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(),
Expand All @@ -88,7 +91,8 @@ export class AppContainer extends Component {
loadMask({model: XH.appLoadModel}),
messageSource({model: XH.messageSourceModel}),
feedbackDialog({model: XH.feedbackDialogModel}),
aboutDialog()
aboutDialog(),
this.renderSuspendedDialog()
);
default:
return null;
Expand Down Expand Up @@ -127,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();

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion app/LockoutPanel.js → impl/LockoutPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading