Skip to content

Commit ad9298c

Browse files
gacondinoetcart
andauthored
CUMULUS-3624-FINAL (#1215)
* first commit * created unit test and edited functionality/modal text * added to changelog * updated test file and component file * added to changelog * tried fixing export and import of const * update package-lock.json * Revert "update package-lock.json" This reverts commit 2237d81. * added form-data fix * added elliptic * added elliptic to see if all passing * Update package.json * Update test-inactivity-modal.js --------- Co-authored-by: etcart <[email protected]>
1 parent 6a5c9ae commit ad9298c

File tree

5 files changed

+188
-1
lines changed

5 files changed

+188
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
2727

2828
### Added
2929

30+
- **CUMULUS-3624**
31+
- Added an inactivity modal to prompt inactive users and to logout after a period of no user interactions after the modal appears
3032
- **CUMULUS-3849**
3133
- Added a network error modal that pops up when the user is offline
3234
- **CUMULUS-4048**

app/src/js/components/Header/header.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import linkToKibana from '../../utils/kibana';
1717
import { getPersistentQueryParams } from '../../utils/url-helper';
1818
import SessionTimeoutModal from '../SessionTimeoutModal/session-timeout-modal';
1919
import NetworkErrorModal from '../NetworkErrorModal/network-error-modal';
20+
import InactivityModal from '../InactivityModal/inactivity-modal';
2021

2122
const paths = [
2223
[strings.collections, '/collections/all'],
@@ -126,6 +127,7 @@ const Header = ({
126127
</div>
127128
<div className="session-timeout-modal"><SessionTimeoutModal/></div>
128129
<div className="network-error-modal"><NetworkErrorModal/></div>
130+
<div className="inactivity-modal"><InactivityModal/></div>
129131
</div>
130132
);
131133
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React, { useCallback, useEffect, useRef, useState } from 'react';
2+
import PropTypes from 'prop-types';
3+
import get from 'lodash/get';
4+
import { connect } from 'react-redux';
5+
import DefaultModal from '../Modal/modal';
6+
import { logout } from '../../actions';
7+
8+
export const INACTIVITY_LIMIT = 300000; // 5 minutes in milliseconds
9+
export const MODAL_TIMEOUT = 120000; // 2 minutes in milliseconds
10+
11+
const InactivityModal = ({
12+
title = 'Inactivity Warning',
13+
children = 'You have been inactive for a while. Move your cursor or press a key to continue using the application. If no action is taken, you will be logged out.',
14+
dispatch,
15+
}) => {
16+
const [hasModal, setHasModal] = useState(false);
17+
const timerRef = useRef(null);
18+
const modalTimeoutRef = useRef(null);
19+
20+
const handleLogout = useCallback(() => {
21+
dispatch(logout()).then(() => {
22+
if (get(window, 'location.reload')) {
23+
window.location.reload();
24+
}
25+
});
26+
}, [dispatch]);
27+
28+
const handleClose = () => {
29+
setHasModal(false);
30+
clearTimeout(timerRef.current);
31+
clearTimeout(modalTimeoutRef.current);
32+
};
33+
34+
const resetTimer = useCallback(() => {
35+
setHasModal(false);
36+
clearTimeout(timerRef.current);
37+
clearTimeout(modalTimeoutRef.current);
38+
timerRef.current = setTimeout(() => {
39+
setHasModal(true);
40+
modalTimeoutRef.current = setTimeout(() => {
41+
handleLogout();
42+
}, MODAL_TIMEOUT); // Logout after modal timeout
43+
}, INACTIVITY_LIMIT); // Show modal after 5 minutes of inactivity
44+
}, [handleLogout]);
45+
46+
const handleActivity = useCallback(() => {
47+
resetTimer();
48+
}, [resetTimer]);
49+
50+
useEffect(() => {
51+
const events = ['mousemove', 'keydown', 'click', 'scroll'];
52+
events.forEach((event) => window.addEventListener(event, handleActivity));
53+
54+
resetTimer();
55+
56+
return () => {
57+
events.forEach((event) => window.removeEventListener(event, handleActivity));
58+
clearTimeout(timerRef.current);
59+
clearTimeout(modalTimeoutRef.current);
60+
};
61+
}, [handleActivity, resetTimer]);
62+
63+
return (
64+
<DefaultModal
65+
data-Id="inactivity-modal"
66+
title={title}
67+
className="InactivityModal"
68+
onCancel={handleClose}
69+
onCloseModal={handleClose}
70+
showModal={hasModal}
71+
hasConfirmButton={false}
72+
hasCancelButton={false}
73+
>
74+
{children}
75+
</DefaultModal>
76+
);
77+
};
78+
79+
InactivityModal.propTypes = {
80+
title: PropTypes.string,
81+
children: PropTypes.string,
82+
dispatch: PropTypes.func,
83+
};
84+
85+
export default connect((state) => ({
86+
token: get(state, 'api.tokens.token'),
87+
}))(InactivityModal);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import test from 'ava';
2+
import React from 'react';
3+
import { Provider } from 'react-redux';
4+
import { render, screen, act, cleanup } from '@testing-library/react';
5+
import configureMockStore from 'redux-mock-store';
6+
import { requestMiddleware } from '../../../app/src/js/middleware/request';
7+
import thunk from 'redux-thunk';
8+
import sinon from 'sinon';
9+
import InactivityModal, { INACTIVITY_LIMIT, MODAL_TIMEOUT } from '../../../app/src/js/components/InactivityModal/inactivity-modal';
10+
11+
const middlewares = [requestMiddleware, thunk];
12+
const mockStore = configureMockStore(middlewares);
13+
14+
let clock;
15+
16+
test.before(() => {
17+
clock = sinon.useFakeTimers();
18+
});
19+
20+
test.after.always(() => {
21+
clock.restore();
22+
cleanup();
23+
});
24+
25+
test('modal is displayed after inactivity timeout', async (t) => {
26+
const store = mockStore({ api: { tokens: { token: 'dummy' } } });
27+
28+
render(
29+
<Provider store={store}>
30+
<InactivityModal />
31+
</Provider>
32+
);
33+
34+
t.falsy(screen.queryByText(/You have been inactive for a while/));
35+
36+
await act(async () => {
37+
clock.tick(INACTIVITY_LIMIT-1000); // fast-forward to just before inactivity limit
38+
await Promise.resolve();
39+
});
40+
41+
t.truthy(screen.queryByText(/You have been inactive for a while/));
42+
});
43+
44+
test('modal closes on user activity', async (t) => {
45+
const store = mockStore({ api: { tokens: { token: 'dummy' } } });
46+
47+
render(
48+
<Provider store={store}>
49+
<InactivityModal dispatch={store.dispatch}/>
50+
</Provider>
51+
);
52+
53+
await act(async () => {
54+
clock.tick(INACTIVITY_LIMIT-1000); // fast-forward to inactivity limit
55+
await Promise.resolve();
56+
});
57+
58+
t.truthy(screen.queryByText(/You have been inactive for a while/));
59+
60+
// Simulate user activity
61+
await act(async () => {
62+
window.dispatchEvent(new MouseEvent('mousemove', { bubbles: true }));
63+
clock.tick(0);
64+
await Promise.resolve();
65+
});
66+
67+
await act(() => Promise.resolve());
68+
69+
t.falsy(screen.queryByTestId('inactivity-modal'));
70+
});
71+
72+
test('modal hides as logout runs after modal timeout', async (t) => {
73+
const store = mockStore({ api: { tokens: { token: 'dummy' } } });
74+
75+
render(
76+
<Provider store={store}>
77+
<InactivityModal />
78+
</Provider>
79+
);
80+
81+
await act(async () => {
82+
clock.tick(INACTIVITY_LIMIT-1000); // fast-forward to inactivity limit
83+
await Promise.resolve();
84+
});
85+
86+
t.truthy(screen.queryByText(/You have been inactive for a while/));
87+
await act(async () => {
88+
clock.tick(MODAL_TIMEOUT-1000); // fast-forward to modal timeout
89+
await Promise.resolve();
90+
});
91+
92+
t.falsy(screen.queryByTestId('inactivity-modal'));
93+
});

test/components/header/header.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const initialState = {
2828
}
2929
};
3030

31-
test('Header contains sessionTimeoutModal, networkErrorModal correct number of nav items and excludes PDRs and Logs', function (t) {
31+
test('Header contains sessionTimeoutModal, networkErrorModal, inactivityModal correct number of nav items and excludes PDRs and Logs', function (t) {
3232
const dispatch = () => {};
3333
const api = {
3434
authenticated: true
@@ -60,6 +60,9 @@ test('Header contains sessionTimeoutModal, networkErrorModal correct number of n
6060
const modalNetwork = container.querySelector('.network-error-modal');
6161
t.truthy(modalNetwork);
6262

63+
const modalInactivity = container.querySelector('.inactivity-modal');
64+
t.truthy(modalInactivity);
65+
6366
const navigation = container.querySelectorAll('nav li');
6467
t.is(navigation.length, 9);
6568

0 commit comments

Comments
 (0)