From 0cb01f7da9fcb4fc3bb49cc5039f6b712466bf74 Mon Sep 17 00:00:00 2001 From: Lucas Di Pentima Date: Fri, 4 Sep 2020 19:48:44 -0300 Subject: [PATCH] 16679: Adds AutoLogout component that closes the session if configured. When Workbench.IdleTimeout is set to a non-zero value, it is used to check for user inactivity; 1 minute before closing the session, a warning snackbar is displayed until the session is auto-closed or user activity is detected. Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima --- package.json | 2 + src/common/config.ts | 2 + .../auto-logout/auto-logout.tsx | 72 +++++++++++++++++++ src/views/main-panel/main-panel-root.tsx | 5 +- src/views/main-panel/main-panel.tsx | 4 +- src/views/workbench/workbench.tsx | 3 + yarn.lock | 10 +++ 7 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 src/views-components/auto-logout/auto-logout.tsx diff --git a/package.json b/package.json index 57c6e311..346d4910 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "lodash.mergewith": "4.6.2", "lodash.template": "4.5.0", "mem": "4.0.0", + "parse-duration": "0.4.4", "prop-types": "15.7.2", "query-string": "6.9.0", "react": "16.8.6", @@ -47,6 +48,7 @@ "react-dom": "16.8.6", "react-dropzone": "5.1.1", "react-highlight-words": "0.14.0", + "react-idle-timer": "4.3.6", "react-redux": "5.0.7", "react-router": "4.3.1", "react-router-dom": "4.3.1", diff --git a/src/common/config.ts b/src/common/config.ts index dd812c65..0f935602 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -58,6 +58,7 @@ export interface ClusterConfigJSON { SSHHelpPageHTML: string; SSHHelpHostSuffix: string; SiteName: string; + IdleTimeout: string; }; Login: { LoginCluster: string; @@ -216,6 +217,7 @@ export const mockClusterConfigJSON = (config: Partial): Clust SSHHelpPageHTML: "", SSHHelpHostSuffix: "", SiteName: "", + IdleTimeout: "0s", }, Login: { LoginCluster: "", diff --git a/src/views-components/auto-logout/auto-logout.tsx b/src/views-components/auto-logout/auto-logout.tsx new file mode 100644 index 00000000..52a1950a --- /dev/null +++ b/src/views-components/auto-logout/auto-logout.tsx @@ -0,0 +1,72 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { connect } from "react-redux"; +import { useIdleTimer } from "react-idle-timer"; +import { Dispatch } from "redux"; + +import { RootState } from "~/store/store"; +import { SnackbarKind, snackbarActions } from "~/store/snackbar/snackbar-actions"; +import { logout } from "~/store/auth/auth-action"; +import parse from "parse-duration"; +import * as React from "react"; +import { min } from "lodash"; + +interface AutoLogoutDataProps { + sessionIdleTimeout: number; + lastWarningDuration: number; +} + +interface AutoLogoutActionProps { + doLogout: () => void; + doWarn: (message: string, duration: number) => void; + doCloseWarn: () => void; +} + +const mapStateToProps = (state: RootState, ownProps: any): AutoLogoutDataProps => { + return { + sessionIdleTimeout: parse(state.auth.config.clusterConfig.Workbench.IdleTimeout, 's') || 0, + lastWarningDuration: ownProps.lastWarningDuration || 60, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch): AutoLogoutActionProps => ({ + doLogout: () => dispatch(logout(true)), + doWarn: (message: string, duration: number) => + dispatch(snackbarActions.OPEN_SNACKBAR({ + message, hideDuration: duration, kind: SnackbarKind.WARNING })), + doCloseWarn: () => dispatch(snackbarActions.CLOSE_SNACKBAR()), +}); + +type AutoLogoutProps = AutoLogoutDataProps & AutoLogoutActionProps; + +export const AutoLogout = connect(mapStateToProps, mapDispatchToProps)( + (props: AutoLogoutProps) => { + let logoutTimer: NodeJS.Timer; + const lastWarningDuration = min([props.lastWarningDuration, props.sessionIdleTimeout])! * 1000 ; + + const handleOnIdle = () => { + logoutTimer = setTimeout( + () => props.doLogout(), lastWarningDuration); + props.doWarn( + "Your session is about to be closed due to inactivity", + lastWarningDuration); + }; + + const handleOnActive = () => { + clearTimeout(logoutTimer); + props.doCloseWarn(); + }; + + useIdleTimer({ + timeout: (props.lastWarningDuration < props.sessionIdleTimeout) + ? 1000 * (props.sessionIdleTimeout - props.lastWarningDuration) + : 1, + onIdle: handleOnIdle, + onActive: handleOnActive, + debounce: 500 + }); + + return ; + }); diff --git a/src/views/main-panel/main-panel-root.tsx b/src/views/main-panel/main-panel-root.tsx index 5806f5b8..acaa43ad 100644 --- a/src/views/main-panel/main-panel-root.tsx +++ b/src/views/main-panel/main-panel-root.tsx @@ -31,13 +31,14 @@ export interface MainPanelRootDataProps { isNotLinking: boolean; isLinkingPath: boolean; siteBanner: string; + sessionIdleTimeout: number; } type MainPanelRootProps = MainPanelRootDataProps & WithStyles; export const MainPanelRoot = withStyles(styles)( ({ classes, loading, working, user, buildInfo, uuidPrefix, - isNotLinking, isLinkingPath, siteBanner }: MainPanelRootProps) => + isNotLinking, isLinkingPath, siteBanner, sessionIdleTimeout }: MainPanelRootProps) => loading ? : <> @@ -53,7 +54,7 @@ export const MainPanelRoot = withStyles(styles)( {user ? (user.isActive || (!user.isActive && isLinkingPath) - ? + ? : ) : } diff --git a/src/views/main-panel/main-panel.tsx b/src/views/main-panel/main-panel.tsx index 5828c6db..edbf5cc4 100644 --- a/src/views/main-panel/main-panel.tsx +++ b/src/views/main-panel/main-panel.tsx @@ -4,6 +4,7 @@ import { RootState } from '~/store/store'; import { connect } from 'react-redux'; +import parse from 'parse-duration'; import { MainPanelRoot, MainPanelRootDataProps } from '~/views/main-panel/main-panel-root'; import { isSystemWorking } from '~/store/progress-indicator/progress-indicator-reducer'; import { isWorkbenchLoading } from '~/store/workbench/workbench-actions'; @@ -19,7 +20,8 @@ const mapStateToProps = (state: RootState): MainPanelRootDataProps => { uuidPrefix: state.auth.localCluster, isNotLinking: state.linkAccountPanel.status === LinkAccountPanelStatus.NONE || state.linkAccountPanel.status === LinkAccountPanelStatus.INITIAL, isLinkingPath: state.router.location ? matchLinkAccountRoute(state.router.location.pathname) !== null : false, - siteBanner: state.auth.config.clusterConfig.Workbench.SiteName + siteBanner: state.auth.config.clusterConfig.Workbench.SiteName, + sessionIdleTimeout: parse(state.auth.config.clusterConfig.Workbench.IdleTimeout, 's') || 0 }; }; diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 906c649c..b0f90894 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -98,6 +98,7 @@ import { FedLogin } from './fed-login'; import { CollectionsContentAddressPanel } from '~/views/collection-content-address-panel/collection-content-address-panel'; import { AllProcessesPanel } from '../all-processes-panel/all-processes-panel'; import { NotFoundPanel } from '../not-found-panel/not-found-panel'; +import { AutoLogout } from '~/views-components/auto-logout/auto-logout'; type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content'; @@ -132,6 +133,7 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ interface WorkbenchDataProps { isUserActive: boolean; isNotLinking: boolean; + sessionIdleTimeout: number; } type WorkbenchPanelProps = WithStyles & WorkbenchDataProps; @@ -148,6 +150,7 @@ const saveSplitterSize = (size: number) => localStorage.setItem('splitterSize', export const WorkbenchPanel = withStyles(styles)((props: WorkbenchPanelProps) => + { props.sessionIdleTimeout > 0 && }