Merge branch '21541-arv-mount-keyerror-rebase' refs #21541
[arvados.git] / services / workbench2 / src / views-components / auto-logout / auto-logout.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { connect } from "react-redux";
6 import { useIdleTimer } from "react-idle-timer";
7 import { Dispatch } from "redux";
8
9 import { RootState } from "store/store";
10 import { SnackbarKind, snackbarActions } from "store/snackbar/snackbar-actions";
11 import { logout } from "store/auth/auth-action";
12 import parse from "parse-duration";
13 import React from "react";
14 import { min } from "lodash";
15
16 interface AutoLogoutDataProps {
17     sessionIdleTimeout: number;
18     lastWarningDuration: number;
19 }
20
21 interface AutoLogoutActionProps {
22     doLogout: () => void;
23     doWarn: (message: string, duration: number) => void;
24     doCloseWarn: () => void;
25 }
26
27 const mapStateToProps = (state: RootState, ownProps: any): AutoLogoutDataProps => ({
28     sessionIdleTimeout: parse(state.auth.config.clusterConfig.Workbench.IdleTimeout, 's') || 0,
29     lastWarningDuration: ownProps.lastWarningDuration || 60,
30 });
31
32 const mapDispatchToProps = (dispatch: Dispatch): AutoLogoutActionProps => ({
33     doLogout: () => dispatch<any>(logout(true, true)),
34     doWarn: (message: string, duration: number) =>
35         dispatch(snackbarActions.OPEN_SNACKBAR({
36             message, hideDuration: duration, kind: SnackbarKind.WARNING })),
37     doCloseWarn: () => dispatch(snackbarActions.CLOSE_SNACKBAR()),
38 });
39
40 export type AutoLogoutProps = AutoLogoutDataProps & AutoLogoutActionProps;
41
42 const debounce = (delay: number | undefined, fn: Function) => {
43     let timerId: NodeJS.Timer | null;
44     return (...args: any[]) => {
45         if (timerId) { clearTimeout(timerId); }
46         timerId = setTimeout(() => {
47             fn(...args);
48             timerId = null;
49         }, delay);
50     };
51 };
52
53 export const LAST_ACTIVE_TIMESTAMP = 'lastActiveTimestamp';
54
55 export const AutoLogoutComponent = (props: AutoLogoutProps) => {
56     let logoutTimer: NodeJS.Timer;
57     const lastWarningDuration = min([props.lastWarningDuration, props.sessionIdleTimeout])! * 1000;
58
59     // Runs once after render
60     React.useEffect(() => {
61         window.addEventListener('storage', handleStorageEvents);
62         // Component cleanup
63         return () => {
64             window.removeEventListener('storage', handleStorageEvents);
65         };
66     });
67
68     const handleStorageEvents = (e: StorageEvent) => {
69         if (e.key === LAST_ACTIVE_TIMESTAMP) {
70             // Other tab activity detected by a localStorage change event.
71             debounce(500, () => {
72                 handleOnActive();
73                 reset();
74             })();
75         }
76     };
77
78     const handleOnIdle = () => {
79         logoutTimer = setTimeout(
80             () => props.doLogout(), lastWarningDuration);
81         props.doWarn(
82             "Your session is about to be closed due to inactivity",
83             lastWarningDuration);
84     };
85
86     const handleOnActive = () => {
87         if (logoutTimer) { clearTimeout(logoutTimer); }
88         props.doCloseWarn();
89     };
90
91     const handleOnAction = () => {
92         // Notify the other tabs there was some activity.
93         const now = (new Date()).getTime();
94         localStorage.setItem(LAST_ACTIVE_TIMESTAMP, now.toString());
95     };
96
97     const { reset } = useIdleTimer({
98         timeout: (props.lastWarningDuration < props.sessionIdleTimeout)
99             ? 1000 * (props.sessionIdleTimeout - props.lastWarningDuration)
100             : 1,
101         onIdle: handleOnIdle,
102         onActive: handleOnActive,
103         onAction: handleOnAction,
104         debounce: 500
105     });
106
107     return <span />;
108 };
109
110 export const AutoLogout = connect(mapStateToProps, mapDispatchToProps)(AutoLogoutComponent);