Skip to content
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- **CUMULUS-3624**
- Added Inactivity Modal component
- Added unit test for component

## [v12.2.0] - 2024-09-04

This version of the dashboard requires Cumulus API >= v18.4.0
Expand Down
1 change: 1 addition & 0 deletions app/src/js/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const MainRoutes = () => (
const App = () => {
const [store] = useState(ourConfigureStore({}));
const isLoggedIn = () => store.getState().api.authenticated;
console.log('app authenticated ', store);

return (
// <ErrorBoundary> // Add after troublshooting other errors
Expand Down
1 change: 0 additions & 1 deletion app/src/js/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ export const searchCollections = (infix) => ({ type: types.SEARCH_COLLECTIONS, i
export const clearCollectionsSearch = () => ({ type: types.CLEAR_COLLECTIONS_SEARCH });
export const filterCollections = (param) => ({ type: types.FILTER_COLLECTIONS, param });
export const clearCollectionsFilter = (paramKey) => ({ type: types.CLEAR_COLLECTIONS_FILTER, paramKey });

export const getCumulusInstanceMetadata = () => ({
[CALL_API]: {
type: types.ADD_INSTANCE_META,
Expand Down
2 changes: 2 additions & 0 deletions app/src/js/components/Header/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { window } from '../../utils/browser';
import { strings } from '../locale';
import linkToKibana from '../../utils/kibana';
import { getPersistentQueryParams } from '../../utils/url-helper';
import InactivityModal from '../Modal/InactivityModal';

const paths = [
[strings.collections, '/collections/all'],
Expand Down Expand Up @@ -121,6 +122,7 @@ const Header = ({
<li>&nbsp;</li>
)}
</nav>
<div className='inactivity'><InactivityModal /></div>
</div>
</div>
);
Expand Down
109 changes: 109 additions & 0 deletions app/src/js/components/Modal/InactivityModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { useEffect, useState, useCallback } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import DefaultModal from './modal';
import { logout } from '../../actions';

const InactivityModal = ({
title = 'Session Timeout Warning',
children = `We have noticed that you have been inactive for a while.
We will close this session in 5 minutes. If you want to stay signed in, select ‘Continue Session’.`,
dispatch
}) => {
const [lastKeyPress, setLastKeyPress] = useState(Date.now());
const [hasModal, setHasModal] = useState(false);

function handleConfirm() {
setLastKeyPress(Date.now());
closeModal();
}

const handleLogout = useCallback(() => {
dispatch(logout()).then(() => {
if (get(window, 'location.reload')) {
window.location.reload();
}
});
}, [dispatch]);

function closeModal() { // the X botton
setLastKeyPress(Date.now());
if (hasModal) {
setHasModal(false); // hide modal if user resumes activity
}
}

// Effect to setup event listeners for keyboard activity
useEffect(() => {
// Function to update the lastKeyPress time
const handleKeypress = () => {
setLastKeyPress(Date.now());
if (hasModal) {
setHasModal(false); // hide modal if user resumes activity
}
};
window.addEventListener('keydown', handleKeypress);

return () => {
window.removeEventListener('keydown', handleKeypress);
};
}, [hasModal]);

// Effect to handle showing the modal after 30 minutes of inactivity
useEffect(() => {
const checkInactivity = setInterval(() => {
if (Date.now() - lastKeyPress > 1800000 && !hasModal) { // 1800000 ms = 30 minutes
Copy link
Contributor

@jennyhliu jennyhliu Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make the inactivity time configurable?

Copy link
Contributor

@jennyhliu jennyhliu Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between this inactivity timeout and session timeout? How do they work together?
The launchpad session can timeout regardless this inactivity, is it correct? Does the launchpad Session get refresh when we click 'Continue Session'?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a cypress test to verify the functionality?
I don't think this is necessary as the other modals do not have tests written for this so it shouldn't be a requirement. If it is necessary I would be happy to do it but will need to be shown how as I normally do not deal with Cypress.

The Modal says 'We will close this session in 5 minutes. ', but after 5 minutes, the Session is not closed or log me out, and the modal window is still there. I click 'Continue Session', the modal window closes and the session is still there.
In progress

Can we make the inactivity time configurable?
Done

What's the difference between this inactivity timeout and session timeout? How do they work together?
The launchpad session can timeout regardless this inactivity, is it correct? Does the launchpad Session get refresh when we click 'Continue Session'?

Currently the session timeout just keeps track if the user is logged in or not. I am not sure what this timeout is set for. The inactivity modal will keep track of the users activity. If there is no activity after 30 min then a pop up will show and a second timer will start. This second timer is set for 5 min. If the user wishes to continue the session then they will need to interact with the modal and select "Continue session" otherwise the user will be logged out. In the future the session timeout and the inactivity timeout will be merged but this will be addressed in another ticket.

setHasModal(true);
}
}, 60000); // Check every minute (60000 ms)

return () => clearInterval(checkInactivity);
}, [hasModal, lastKeyPress]);

return (
<div>
<DefaultModal
title = {title}
className='IAModal'
onCancel={handleLogout}
onCloseModal={closeModal}
onConfirm={handleConfirm}
showModal={hasModal}
hasConfirmButton={true}
hasCancelButton={true}
cancelButtonText='Close Session'
confirmButtonText='Continue Session'
children = {children}
/>
</div>
);
};

const mapStateToProps = (state) => ({
hasModal: state.hasModal,
lastKeyPress: state.lastKeyPress,
// logoutTimer: state.logoutTimer
});

InactivityModal.propTypes = {
hasModal: PropTypes.bool,
lastKeyPress: PropTypes.string,
// logoutTimer: PropTypes.object,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
children: PropTypes.string,
className: PropTypes.string,
cancelButtonText: PropTypes.string,
confirmButtonText: PropTypes.string,
showModal: PropTypes.bool,
onCloseModal: PropTypes.func,
onConfirm: PropTypes.func,
onCancel: PropTypes.func,
hasCancelButton: PropTypes.bool,
hasConfirmButton: PropTypes.bool,
dispatch: PropTypes.func,
};

export { InactivityModal };

export default connect(mapStateToProps)(InactivityModal);
4 changes: 2 additions & 2 deletions app/src/js/components/home.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,6 @@ const Home = ({
<h1 className='heading--xlarge'>{strings.dashboard}</h1>
</div>
</div>

<div className='page__content page__content--nosidebar'>
{pageSection(
<>Select date and time to refine your results. <em>Time is UTC.</em></>,
Expand Down Expand Up @@ -180,7 +179,8 @@ Home.propTypes = {
rules: PropTypes.object,
stats: PropTypes.object,
dispatch: PropTypes.func,
location: PropTypes.object
location: PropTypes.object,
inactivityModal: PropTypes.object
};

export { Home };
Expand Down
1 change: 1 addition & 0 deletions app/src/js/components/oauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const OAuth = ({
queryParams,
}) => {
const [token, setToken] = useState(null);
console.log('oauth authenticated: ', api);

useEffect(() => {
if (api.authenticated) {
Expand Down
2 changes: 1 addition & 1 deletion app/src/js/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const reducers = {
reconciliationReports,
recoveryStatus,
sorts,
locationQueryParams,
locationQueryParams
};

export const createRootReducer = (history) => combineReducers({
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions test/components/inactivity-modal/inactivity-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use strict';

import test from 'ava';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
import React from 'react';
import { shallow, configure } from 'enzyme';
import { InactivityModal } from '../../../app/src/js/components/Modal/InactivityModal';

configure({ adapter: new Adapter() });

test('Inactivity Modal should render when hasModal is true', function (t) {
const hasModal = true ;
const hasCancelButton = 'button--cancel';
const hasConfirmButton = 'button--submit';

// Create a shallow render of the component
const modal = shallow(
<InactivityModal
hasModal={hasModal}
hasCancelButton={hasCancelButton}
hasConfirmButton={hasConfirmButton}
/>
);
t.true(modal.find('DefaultModal').exists(),'Modal should be present when hasModal is true');
t.is(modal.find('DefaultModal').prop('hasCancelButton'), true);
t.is(modal.find('DefaultModal').prop('hasConfirmButton'), true);
});