Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"root": false,
"root": true,
"extends": [
"airbnb-base",
"standard",
Expand Down
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [v13.2.0] - 2025-05-14

### Added

- **CUMULUS-4048**
- Added a session timeout warning modal that pops up five minutes before the session expires
- **CUMULUS-4088**
- Added sortKey=[-timestamp] and set no limit to queries to get collection and provider options
- **CUMULUS-4094**
- Added `INITIAL_DATE_RANGE_IN_DAYS` environment variable to allow control over how many days objects will be loaded at startup of the dashboard

## [v13.1.0] - 2025-03-25

### Changed
Expand All @@ -21,6 +34,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Fixed

- **CUMULUS-3263**
- Updated datestring format to show the correct hours
- **CUMULUS-4008**
- Updated the Dockerfile with improved NPM install command to prevent error messages.

Expand Down Expand Up @@ -1462,7 +1477,8 @@ Fix for serving the dashboard through the Cumulus API.
### Added

- Versioning and changelog [CUMULUS-197] by @kkelly51
[Unreleased]: https://github.com/nasa/cumulus-dashboard/compare/v13.1.0...HEAD
[Unreleased]: https://github.com/nasa/cumulus-dashboard/compare/v13.2.0...HEAD
[v13.2.0]: https://github.com/nasa/cumulus-dashboard/compare/v13.1.0...v13.2.0
[v13.1.0]: https://github.com/nasa/cumulus-dashboard/compare/v13.0.0...v13.1.0
[v13.0.0]: https://github.com/nasa/cumulus-dashboard/compare/v12.2.0...v13.0.0
[v12.2.0]: https://github.com/nasa/cumulus-dashboard/compare/v12.1.0...v12.2.0
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Setting the following environment variables can override the default values.
| LABELS | Select `daac` localization. | *daac* |
| STAGE | Identifier displayed at top of dashboard page: e.g. PROD, UAT | *development* |
| KIBANAROOT | \<optional\> Points to a Kibana endpoint. | |
| INITIAL_DATE_RANGE_IN_DAYS| \<optional\> Number of days to load up before at start | |


## Quick start
Expand Down
4 changes: 2 additions & 2 deletions app/src/js/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ export const getOptionsCollectionName = (options) => ({
type: types.OPTIONS_COLLECTIONNAME,
method: 'GET',
url: new URL('collections', root).href,
params: { limit: 100, fields: 'name,version' }
params: { limit: 'null', fields: 'name,version', sort_key: ['-timestamp'] }
}
});

Expand Down Expand Up @@ -514,7 +514,7 @@ export const getOptionsProviderName = () => ({
type: types.OPTIONS_PROVIDERNAME,
method: 'GET',
url: new URL('providers', root).href,
params: { limit: 100, fields: 'id' }
params: { limit: 'null', fields: 'id', sort_key: ['-timestamp'] }
}
});

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 SessionTimeoutModal from '../SessionTimeoutModal/session-timeout-modal';

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

const SessionTimeoutModal = ({
tokenExpiration,
title = 'Session Expiration Warning',
children = 'Your session will expire in 5 minutes. Please re-login if you would like to stay signed in.',
dispatch,
}) => {
const [hasModal, setHasModal] = useState(false);
const [modalClosed, setModalClosed] = useState(false);

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

const handleClose = () => {
setHasModal(false);
setModalClosed(true);
};

useEffect(() => {
const interval = setInterval(() => {
// the modal will only pop up when the user is logged in, not already shown, or closed
if (!tokenExpiration || hasModal || modalClosed) {
return;
}

const currentTime = Math.ceil(Date.now() / 1000);
const secondsLeft = tokenExpiration - currentTime;

if (secondsLeft <= 300) {
setHasModal(true);
}
}, 1000);

return () => clearInterval(interval);
}, [tokenExpiration, hasModal, modalClosed]);

return (
<DefaultModal
title={title}
className="SessionTimeoutModal"
onCancel={handleClose}
onCloseModal={handleClose}
onConfirm={handleLogout}
showModal={hasModal}
hasConfirmButton={true}
hasCancelButton={true}
cancelButtonText="Dismiss"
confirmButtonText="Re-login"
>
{children}
</DefaultModal>
);
};

SessionTimeoutModal.propTypes = {
tokenExpiration: PropTypes.number,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
children: PropTypes.string,
dispatch: PropTypes.func,
};

export default connect((state) => {
const token = get(state, 'api.tokens.token');
const jwtData = token ? jwtDecode(token) : null;
const tokenExpiration = get(jwtData, 'exp');

return { tokenExpiration };
})(SessionTimeoutModal);
3 changes: 2 additions & 1 deletion app/src/js/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ const config = {
oauthMethod: process.env.AUTH_METHOD || 'earthdata',
kibanaRoot: process.env.KIBANAROOT || '',
graphicsPath: process.env.BUCKET || '',
initialDateRange: process.env.INITIAL_DATE_RANGE_IN_DAYS || 'All',
enableRecovery: computeBool(process.env.ENABLE_RECOVERY, false),
servedByCumulusAPI: computeBool(process.env.SERVED_BY_CUMULUS_API, '')
servedByCumulusAPI: computeBool(process.env.SERVED_BY_CUMULUS_API, ''),
};

module.exports = config;
7 changes: 2 additions & 5 deletions app/src/js/reducers/datepicker.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { createReducer } from '@reduxjs/toolkit';
import { msPerDay, findDateRangeByValue } from '../utils/datepicker';
import config from '../config';
import {
DATEPICKER_DATECHANGE,
DATEPICKER_DROPDOWN_FILTER,
DATEPICKER_HOUR_FORMAT,
} from '../actions/types';

// Also becomes default props for Datepicker
export const initialState = () => ({
startDateTime: null,
endDateTime: null,
...computeDateTimeDelta(config.initialDateRange),
dateRange: findDateRangeByValue('Custom'),
hourFormat: '12HR'
});
Expand All @@ -24,7 +23,6 @@ export const initialState = () => ({
const computeDateTimeDelta = (timeDeltaInDays) => {
let endDateTime = null;
let startDateTime = null;

if (!Number.isNaN(+timeDeltaInDays)) {
endDateTime = Date.now();
startDateTime = endDateTime - timeDeltaInDays * msPerDay;
Expand All @@ -47,7 +45,6 @@ const recentData = () => ({
export default createReducer(initialState(), {
[DATEPICKER_DROPDOWN_FILTER]: (state, action) => {
const { data } = action;

switch (data.dateRange.label) {
case 'Custom':
case 'All':
Expand Down
2 changes: 1 addition & 1 deletion app/src/js/utils/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const fullDate = (datestring) => {
if (!datestring) {
return nullValue;
}
return moment(datestring).format('kk:mm:ss MM/DD/YY');
return moment(datestring).format('HH:mm:ss MM/DD/YY');
};

export const dateOnly = (datestring) => {
Expand Down
1 change: 1 addition & 0 deletions ava-conditional.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
HIDE_PDR: 'false',
KIBANAROOT: 'https://fake.com/linktokibana',
AUTH_METHOD: 'launchpad',
INITIAL_DATE_RANGE_IN_DAYS: '',
BUCKET: 'https://example.com/bucket',
STAGE: 'production',
DAAC_NAME: 'Cumulus_Daac'
Expand Down
1 change: 1 addition & 0 deletions bamboo/set-bamboo-env-variables.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ declare -a param_list=(
"bamboo_SIT_HIDE_PDR"
"bamboo_SIT_STAGE"
"bamboo_SIT_DASHBOARD_BUCKET"
"bamboo_INITIAL_DATE_RANGE_IN_DAYS"
)

## Strip 'bamboo_SECRET_' from secret keys
Expand Down
2 changes: 2 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ COPY package.json package-lock.json ./
RUN npm ci --legacy-peer-deps --no-optional

ARG APIROOT
ARG INITIAL_DATE_RANGE_IN_DAYS
ARG AUTH_METHOD
ARG AWS_REGION
ARG DAAC_NAME
Expand All @@ -20,6 +21,7 @@ RUN bash -c "echo -e API ROOT IS : $APIROOT"

RUN \
APIROOT=$APIROOT \
INITIAL_DATE_RANGE_IN_DAYS=$INITIAL_DATE_RANGE_IN_DAYS \
AUTH_METHOD=$AUTH_METHOD \
AWS_REGION=$AWS_REGION \
DAAC_NAME=$DAAC_NAME \
Expand Down
2 changes: 2 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ services:
- ENABLE_RECOVERY
- HIDE_PDR
- KIBANAROOT
- INITIAL_DATE_RANGE_IN_DAYS
- SERVED_BY_CUMULUS_API
- STAGE
dashboard:
Expand All @@ -32,5 +33,6 @@ services:
- ENABLE_RECOVERY
- HIDE_PDR
- KIBANAROOT
- INITIAL_DATE_RANGE_IN_DAYS
- SERVED_BY_CUMULUS_API
- STAGE
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cumulus/cumulus-dashboard",
"version": "13.1.0",
"version": "13.2.0",
"description": "A dashboard for Cumulus API",
"repository": {
"type": "git",
Expand Down
60 changes: 60 additions & 0 deletions test/components/SessionTimeoutModal/session-timeout-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import test from 'ava';
import React from 'react';
import { Provider } from 'react-redux';
import { render, screen, act } from '@testing-library/react';
import sinon from 'sinon';
import thunk from 'redux-thunk';
import configureMockStore from 'redux-mock-store';
import { requestMiddleware } from '../../../app/src/js/middleware/request';
import jwt from 'jsonwebtoken';
import SessionTimeoutModal from '../../../app/src/js/components/SessionTimeoutModal/session-timeout-modal';

const middlewares = [requestMiddleware, thunk];
const mockStore = configureMockStore(middlewares);

function createDummyToken(expiration) {
return jwt.sign({ exp: expiration }, '', { algorithm: 'none' });
}

let clock;

test.before(() => {
clock = sinon.useFakeTimers();
});

test.after.always(() => {
clock.restore();
});

test('SessionTimeout modal shows up 5 minutes before token expiration', async (t) => {
const futureExp = Math.floor(Date.now() / 1000) + 400; // expires in 400 seconds
const dummyToken = createDummyToken(futureExp);

const store = mockStore({
api: {
tokens: { token: dummyToken },
},
});

render(
<Provider store={store}>
<SessionTimeoutModal />
</Provider>
);

t.falsy(screen.queryByText('Your session will expire in 5 minutes'));

await act(async () => {
clock.tick(100000); // fast-forwards a 100 seconds, to within 5 minutes of expiration
await Promise.resolve();
});

const modalText = screen.getByText(/Your session will expire in 5 minutes/);
t.truthy(modalText);

const logoutButton = screen.getByRole('button', { name: /Dismiss/i });
const closeButton = screen.getByRole('button', { name: /Re-login/i });

t.truthy(logoutButton);
t.truthy(closeButton);
});
6 changes: 5 additions & 1 deletion test/components/header/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const initialState = {
}
};

test('Header contains correct number of nav items and excludes PDRs and Logs', function (t) {
test('Header contains sessionTimeoutModal, correct number of nav items and excludes PDRs and Logs', function (t) {
const dispatch = () => {};
const api = {
authenticated: true
Expand All @@ -53,6 +53,10 @@ test('Header contains correct number of nav items and excludes PDRs and Logs', f
</MemoryRouter>
</Provider>
);

const modal = container.querySelector('.session-timeout-modal');
t.truthy(modal);

const navigation = container.querySelectorAll('nav li');
t.is(navigation.length, 9);

Expand Down
2 changes: 1 addition & 1 deletion test/reducers/datepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import test from 'ava';
import sinon from 'sinon';
import reducer, { initialState } from '../../app/src/js/reducers/datepicker';
import { allDateRanges, msPerDay, findDateRangeByValue } from '../../app/src/js/utils/datepicker';
import { msPerDay, findDateRangeByValue } from '../../app/src/js/utils/datepicker';
import {
DATEPICKER_DATECHANGE,
DATEPICKER_DROPDOWN_FILTER,
Expand Down
Loading
Loading