diff --git a/config/router.config.js b/config/router.config.js
index e874831a2..5501fc9e7 100644
--- a/config/router.config.js
+++ b/config/router.config.js
@@ -74,6 +74,16 @@ module.exports = [
name: 'profile',
component: './Profile',
},
+ {
+ path: '/result',
+ name: 'result',
+ component: './Result',
+ },
+ {
+ path: '/expiring-results',
+ name: 'expiring-results',
+ component: './ExpiringResults',
+ },
{
path: '/exception/403',
name: 'exception-403',
diff --git a/mock/api.js b/mock/api.js
index 051cc820a..9ced9f538 100644
--- a/mock/api.js
+++ b/mock/api.js
@@ -15,15 +15,23 @@ export const mockControllers = new Array(DEFAULT_SIZE).fill().map((value, index)
}));
export const mockResults = hostname =>
- new Array(DEFAULT_SIZE).fill().map((value, index) => ({
+ new Array(DEFAULT_SIZE).fill().map(() => ({
'@metadata.controller_dir': hostname,
config: casual.word,
controller: hostname,
end: moment.utc(),
- // Since dataset id is a long hex string, removed "-" characters here to make it look like real data
- id: casual.uuid.replace(/-/g, ''),
- result: `${index}${hostname.slice(0, -6)}${index}`,
- start: moment.utc(),
+ id: createUniqueKey(),
+ result: `${casual.word}.${casual.word}.${casual.word}`,
+ start: moment.utc().subtract(Math.random() * 10 + 10, 'days'),
+ serverMetadata: {
+ dashboard: {
+ saved: false,
+ seen: false,
+ },
+ 'dataset.access': 'public',
+ 'dataset.owner': 'roger@example.com',
+ 'server.deletion': moment.utc().add('days', Math.random() * 10 + 10),
+ },
}));
export const mockSamples = {
@@ -330,7 +338,8 @@ export default {
'POST /api/v1/controllers/list': mockControllers,
'POST /api/v1/datasets/list': (req, res) => {
const data = {};
- data[req.body.controller] = mockResults(req.body.controller);
+ const controller = req.body.controller || 'mock-controller';
+ data[controller] = mockResults(controller);
res.send(data);
},
'POST /api/v1/datasets/detail': (req, res) => {
diff --git a/src/common/menu.js b/src/common/menu.js
index 748593061..865d89aad 100644
--- a/src/common/menu.js
+++ b/src/common/menu.js
@@ -27,6 +27,21 @@ export const menuData = [
},
],
},
+ {
+ name: 'Overview',
+ icon: 'overview',
+ path: '/overview',
+ routes: [
+ {
+ name: 'Expiring Results',
+ path: '/expiring-results',
+ },
+ {
+ name: 'result',
+ path: '/result',
+ },
+ ],
+ },
{
name: 'Search',
path: '/search',
diff --git a/src/components/AuthLayout/index.js b/src/components/AuthLayout/index.js
index ebbe0e4c9..075f1aa31 100644
--- a/src/components/AuthLayout/index.js
+++ b/src/components/AuthLayout/index.js
@@ -240,7 +240,7 @@ class AuthLayout extends Component {
this.navigate('controllers')}
+ onClick={() => this.navigate('')}
>
here
diff --git a/src/components/LoginForm/index.js b/src/components/LoginForm/index.js
new file mode 100644
index 000000000..f268dcd0e
--- /dev/null
+++ b/src/components/LoginForm/index.js
@@ -0,0 +1,123 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Form,
+ FormGroup,
+ TextInput,
+ Checkbox,
+ ActionGroup,
+ Button,
+ Title,
+} from '@patternfly/react-core';
+import { connect } from 'dva';
+import styles from './index.less';
+
+const mapStateToProps = state => {
+ const { auth } = state;
+ return { auth };
+};
+
+const LoginForm = props => {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [errors, setErrors] = useState({
+ email: '',
+ });
+ const [btnDisabled, setBtnDisabled] = useState(true);
+
+ const handleUsernameChange = val => {
+ setUsername(val);
+ setErrors({
+ ...errors,
+ });
+ };
+
+ const handleLoginSubmit = () => {
+ const { dispatch } = props;
+ dispatch({
+ type: 'auth/loginUser',
+ payload: {
+ username,
+ password,
+ },
+ });
+ };
+
+ /* eslint-disable no-restricted-syntax */
+ const validateForm = () => {
+ if (username.trim() === '' || password.trim() === '') {
+ return false;
+ }
+ for (const dep of Object.entries(errors)) {
+ if (dep[1].length > 0) {
+ return false;
+ }
+ }
+ // if we reach here, it means
+ // we have covered all of the edge cases.
+ return true;
+ };
+
+ useEffect(
+ () => {
+ if (validateForm()) {
+ setBtnDisabled(false);
+ } else setBtnDisabled(true);
+ },
+ [username, password]
+ );
+
+ const form = (
+
+ );
+ return {form} ;
+};
+
+export default connect(mapStateToProps)(LoginForm);
diff --git a/src/pages/LoginHandler/index.less b/src/components/LoginForm/index.less
similarity index 66%
rename from src/pages/LoginHandler/index.less
rename to src/components/LoginForm/index.less
index e3566fcc9..648ca6016 100644
--- a/src/pages/LoginHandler/index.less
+++ b/src/components/LoginForm/index.less
@@ -1,5 +1,5 @@
.section {
- padding: 5% 10% 5% 10% !important;
+ padding: 5% 10% 5% 10%;
}
.btn {
@@ -7,10 +7,6 @@
color: white;
}
-.inlineLink {
- font-size: var(--pf-global--FontSize--xl);
-}
-
.pf-c-form__group .pf-c-form__label {
font-size: 50px !important;
}
diff --git a/src/components/LoginHint/index.js b/src/components/LoginHint/index.js
index d1b9a3f5e..5c05f0a97 100644
--- a/src/components/LoginHint/index.js
+++ b/src/components/LoginHint/index.js
@@ -9,7 +9,7 @@ import styles from './index.less';
store,
auth: auth.auth,
}))
-class Overview extends Component {
+class LoginHint extends Component {
navigateToAuth = () => {
const { dispatch } = this.props;
dispatch(routerRedux.push(`/auth`));
@@ -49,4 +49,4 @@ class Overview extends Component {
}
}
-export default Overview;
+export default LoginHint;
diff --git a/src/components/LoginModal/index.js b/src/components/LoginModal/index.js
new file mode 100644
index 000000000..b7b721b77
--- /dev/null
+++ b/src/components/LoginModal/index.js
@@ -0,0 +1,94 @@
+import React from 'react';
+import {
+ Modal,
+ ModalVariant,
+ Button,
+ TextContent,
+ Text,
+ TextVariants,
+} from '@patternfly/react-core';
+import { routerRedux } from 'dva/router';
+import { connect } from 'dva';
+import LoginForm from '@/components/LoginForm';
+
+@connect(auth => ({
+ auth: auth.auth,
+}))
+class LoginModal extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isModalOpen: false,
+ modalView: false,
+ };
+ }
+
+ componentDidMount() {
+ this.handleModalToggle();
+ }
+
+ handleModalToggle = () => {
+ this.setState(({ isModalOpen }) => ({
+ isModalOpen: !isModalOpen,
+ }));
+ };
+
+ handleModalCancel = () => {
+ const { dispatch } = this.props;
+ this.setState(({ isModalOpen }) => ({
+ isModalOpen: !isModalOpen,
+ }));
+ dispatch(routerRedux.push(`/`));
+ };
+
+ handleLoginModal = () => {
+ this.setState({
+ modalView: true,
+ });
+ };
+
+ handleSignupModal = () => {
+ const { dispatch } = this.props;
+ this.setState(({ isModalOpen }) => ({
+ isModalOpen: !isModalOpen,
+ }));
+ dispatch(routerRedux.push(`/signup`));
+ };
+
+ render() {
+ const { isModalOpen, modalView } = this.state;
+ const loginAction = (
+
+
+
+ This action requires login. Please login to Pbench Dashboard to continue.
+
+
+
+ Login
+
+
+ Signup
+
+
+ Cancel
+
+
+ );
+ const modalContent = !modalView ? loginAction : ;
+ return (
+
+
+ {modalContent}
+
+
+ );
+ }
+}
+
+export default LoginModal;
diff --git a/src/components/RowSelection/index.js b/src/components/RowSelection/index.js
index adfdea5c1..e0f36ee34 100644
--- a/src/components/RowSelection/index.js
+++ b/src/components/RowSelection/index.js
@@ -1,22 +1,59 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
-
+import React from 'react';
+import { Dropdown, DropdownItem, DropdownSeparator, DropdownToggle } from '@patternfly/react-core';
+import CaretDownIcon from '@patternfly/react-icons/dist/js/icons/caret-down-icon';
import Button from '../Button';
-export default class RowSelection extends PureComponent {
- static propTypes = {
- selectedItems: PropTypes.array.isRequired,
- compareActionName: PropTypes.string.isRequired,
- onCompare: PropTypes.func.isRequired,
- style: PropTypes.object,
+export default class RowSelection extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isManageRunDropdownOpen: false,
+ };
+ }
+
+ onSelectManageRunDropdown = () => {
+ const { isManageRunDropdownOpen } = this.state;
+ this.setState({
+ isManageRunDropdownOpen: !isManageRunDropdownOpen,
+ });
};
- static defaultProps = {
- style: {},
+ onToggleManageRunDropdown = isManageRunDropdownOpen => {
+ this.setState({
+ isManageRunDropdownOpen,
+ });
};
render() {
- const { selectedItems, compareActionName, onCompare, style } = this.props;
+ const {
+ selectedItems,
+ compareActionName,
+ onCompare,
+ saveRuns,
+ removeResultFromSeen,
+ favoriteResult,
+ deleteResult,
+ style,
+ } = this.props;
+ const { isManageRunDropdownOpen } = this.state;
+
+ const manageRunDropdown = [
+
+ {' '}
+ Save Runs
+ ,
+
+ {' '}
+ Mark as unread
+ ,
+
+ Mark Favorited
+ ,
+ ,
+
+ Delete runs
+ ,
+ ];
return (
@@ -27,6 +64,21 @@ export default class RowSelection extends PureComponent {
disabled={!(selectedItems > 0)}
style={{ marginRight: 8 }}
/>
+
+ Manage runs
+
+ }
+ isOpen={isManageRunDropdownOpen}
+ dropdownItems={manageRunDropdown}
+ style={{ display: selectedItems === 0 ? 'none' : 'inline-block', float: 'right' }}
+ />
{selectedItems > 0 ? `Selected ${selectedItems} items` : ''}
diff --git a/src/components/Table/index.js b/src/components/Table/index.js
index fd00ba1fc..8a7261d14 100644
--- a/src/components/Table/index.js
+++ b/src/components/Table/index.js
@@ -62,7 +62,18 @@ function fuzzyTextFilterFn(rows, id, filterValue) {
fuzzyTextFilterFn.autoRemove = val => !val;
-function Table({ columns, data, isCheckable, onCompare, loadingData, onRowClick }) {
+function Table({
+ columns,
+ data,
+ isCheckable,
+ onCompare,
+ saveRuns,
+ removeResultFromSeen,
+ favoriteResult,
+ deleteResult,
+ loadingData,
+ onRowClick,
+}) {
const filterTypes = React.useMemo(
() => ({
fuzzyText: fuzzyTextFilterFn,
@@ -164,6 +175,12 @@ function Table({ columns, data, isCheckable, onCompare, loadingData, onRowClick
selectedItems={Object.keys(selectedRowIds).length}
compareActionName="Compare"
onCompare={() => onCompare(selectedFlatRows)}
+ saveRuns={() => saveRuns(selectedFlatRows)}
+ removeResultFromSeen={() =>
+ removeResultFromSeen(selectedFlatRows.map(item => item.original.result))
+ }
+ favoriteResult={() => favoriteResult(selectedFlatRows.map(item => item.original.result))}
+ deleteResult={() => deleteResult(selectedFlatRows.map(item => item.original.result))}
/>
)}
{loadingData ? (
diff --git a/src/e2e/search.e2e.js b/src/e2e/search.e2e.js
index 52c26809b..9822afc0c 100644
--- a/src/e2e/search.e2e.js
+++ b/src/e2e/search.e2e.js
@@ -14,7 +14,7 @@ beforeAll(async () => {
await page.goto('http://localhost:8000/dashboard/');
await page.click('#nav-toggle > svg');
- await page.click('#page-sidebar > div > nav > ul > li:nth-child(2) > a');
+ await page.click('#page-sidebar > div > nav > ul > li:nth-child(3) > a');
await page.click('#nav-toggle > svg');
// Intercept network requests
diff --git a/src/models/dashboard.js b/src/models/dashboard.js
index e4d61fc5b..54241efbc 100644
--- a/src/models/dashboard.js
+++ b/src/models/dashboard.js
@@ -84,7 +84,6 @@ export default {
const tocTree = Object.keys(tocResult)
.map(path => path.split('/').slice(1))
.reduce((items, path) => insertTocTreeData(tocResult, items, path), []);
-
yield put({
type: 'getTocResult',
payload: tocTree,
@@ -168,6 +167,12 @@ export default {
payload,
});
},
+ *updateResults({ payload }, { put }) {
+ yield put({
+ type: 'modifyResults',
+ payload,
+ });
+ },
},
reducers: {
@@ -207,22 +212,16 @@ export default {
iterations: payload,
};
},
- modifySelectedControllers(state, { payload }) {
- return {
- ...state,
- selectedControllers: payload,
- };
- },
- modifySelectedResults(state, { payload }) {
+ modifyConfigCategories(state, { payload }) {
return {
...state,
- selectedResults: payload,
+ iterationParams: payload,
};
},
- modifyConfigCategories(state, { payload }) {
+ modifyResults(state, { payload }) {
return {
...state,
- iterationParams: payload,
+ results: payload,
};
},
},
diff --git a/src/models/user.js b/src/models/user.js
index f70823395..97d98490b 100644
--- a/src/models/user.js
+++ b/src/models/user.js
@@ -4,27 +4,22 @@ export default {
state: {
favoriteControllers: [],
favoriteResults: [],
- // user: {},
+ seenResults: [],
},
effects: {
- // *loadUser({ payload }, { put }) {
- // yield put({
- // type: 'modifyUser',
- // payload,
- // });
- // },
- // *logoutUser({ put }) {
- // yield put({
- // type: 'removeUser',
- // });
- // },
*favoriteController({ payload }, { put }) {
yield put({
type: 'modifyFavoritedControllers',
payload,
});
},
+ *markResultSeen({ payload }, { put }) {
+ yield put({
+ type: 'modifySeenResults',
+ payload,
+ });
+ },
*removeControllerFromFavorites({ payload }, { put }) {
yield put({
type: 'removeFavoriteController',
@@ -43,6 +38,12 @@ export default {
payload,
});
},
+ *removeResultFromSeen({ payload }, { put }) {
+ yield put({
+ type: 'removeSeenResults',
+ payload,
+ });
+ },
},
reducers: {
@@ -64,10 +65,18 @@ export default {
favoriteControllers: [...state.favoriteControllers, payload],
};
},
+ modifySeenResults(state, { payload }) {
+ return {
+ ...state,
+ seenResults: [...state.seenResults, payload],
+ };
+ },
modifyFavoritedResults(state, { payload }) {
return {
...state,
- favoriteResults: [...state.favoriteResults, payload],
+ favoriteResults: Array.isArray(payload)
+ ? [...state.favoriteResults, ...payload]
+ : [...state.favoriteResults, payload],
};
},
removeFavoriteController(state, { payload }) {
@@ -82,5 +91,11 @@ export default {
favoriteResults: state.favoriteResults.filter(item => item !== payload),
};
},
+ removeSeenResults(state, { payload }) {
+ return {
+ ...state,
+ seenResults: state.seenResults.filter(item => !payload.includes(item)),
+ };
+ },
},
};
diff --git a/src/pages/ExpiringResults/index.js b/src/pages/ExpiringResults/index.js
new file mode 100644
index 000000000..b5aa56c3f
--- /dev/null
+++ b/src/pages/ExpiringResults/index.js
@@ -0,0 +1,343 @@
+import React, { Component } from 'react';
+import { routerRedux } from 'dva/router';
+import { connect } from 'dva';
+import {
+ Grid,
+ GridItem,
+ Card,
+ TextContent,
+ Text,
+ TextVariants,
+ Button,
+ Progress,
+ ProgressSize,
+ ProgressMeasureLocation,
+ ProgressVariant,
+ Tooltip,
+ Modal,
+ Alert,
+ AlertGroup,
+} from '@patternfly/react-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faStar, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { EllipsisVIcon } from '@patternfly/react-icons';
+import { formatDate, getDiffDays, getDiffDate } from '../../utils/moment_constants';
+import Table from '@/components/Table';
+import styles from './index.less';
+
+@connect(({ global, user, dashboard }) => ({
+ selectedDateRange: global.selectedDateRange,
+ selectedControllers: global.selectedControllers,
+ user: user.user,
+ seenResults: user.seenResults,
+ favoriteResults: user.favoriteResults,
+ results: dashboard.results,
+}))
+class ExpiringResults extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ totalResultData: [],
+ toBeDeletedData: [],
+ isModalOpen: false,
+ actionMessage: '',
+ };
+ }
+
+ componentDidMount() {
+ const { dispatch, selectedControllers, selectedDateRange } = this.props;
+ dispatch({
+ type: 'dashboard/fetchResults',
+ payload: {
+ selectedDateRange,
+ controller: selectedControllers,
+ },
+ }).then(() => {
+ const { results } = this.props;
+ this.setState({
+ totalResultData: results[selectedControllers[0] || 'mock-controller'],
+ });
+ });
+ }
+
+ favoriteResult = row => {
+ const { dispatch } = this.props;
+ // dispatch an action to favorite controller
+ dispatch({
+ type: 'user/favoriteResult',
+ payload: row,
+ });
+ };
+
+ unfavoriteResult = row => {
+ const { dispatch } = this.props;
+ // dispatch an action to favorite controller
+ dispatch({
+ type: 'user/removeResultFromFavorites',
+ payload: row,
+ });
+ };
+
+ deleteResult = (e, rows) => {
+ // Stop propagation from going to the next page
+ if (e !== null) {
+ e.stopPropagation();
+ }
+ const { totalResultData } = this.state;
+ const { dispatch } = this.props;
+ const updatedResult = totalResultData.filter(item => !rows.includes(item.result));
+ dispatch({ type: 'dashboard/updateResults', payload: updatedResult })
+ .then(() => {
+ this.setState({ totalResultData: updatedResult });
+ })
+ .then(() => {
+ this.handleModalToggle(e, []);
+ })
+ .then(() => {
+ this.setState({ actionMessage: 'data succesfully deleted' });
+ setTimeout(() => {
+ this.setState({ actionMessage: '' });
+ }, 3000);
+ });
+ };
+
+ removeResultFromSeen = row => {
+ const { dispatch } = this.props;
+ dispatch({
+ type: 'user/removeResultFromSeen',
+ payload: row,
+ });
+ };
+
+ showDropdown = (e, id) => {
+ // Stop propagation from going to the next page
+ e.stopPropagation();
+
+ const dropdownElem = document.getElementById(id);
+ if (dropdownElem.style.display === 'none') {
+ dropdownElem.style.display = 'block';
+ } else {
+ dropdownElem.style.display = 'none';
+ }
+ };
+
+ retrieveResults = row => {
+ const { dispatch } = this.props;
+ this.markResultSeen(row);
+ dispatch({
+ type: 'global/updateSelectedResults',
+ payload: [row],
+ }).then(() => {
+ dispatch(
+ routerRedux.push({
+ pathname: '/result',
+ })
+ );
+ });
+ };
+
+ markResultSeen = row => {
+ const { dispatch } = this.props;
+ dispatch({
+ type: 'user/markResultSeen',
+ payload: row.result,
+ });
+ };
+
+ handleModalToggle(e, rows) {
+ // Stop propagation from going to the next page
+ if (e !== null) {
+ e.stopPropagation();
+ }
+ this.setState(({ isModalOpen }) => ({ isModalOpen: !isModalOpen, toBeDeletedData: rows }));
+ }
+
+ render() {
+ const { favoriteResults, seenResults } = this.props;
+ const { totalResultData, toBeDeletedData, isModalOpen, actionMessage } = this.state;
+ const seenDataColumns = [
+ {
+ Header: 'Result',
+ accessor: 'result',
+ Cell: cell => {
+ const row = cell.row.original;
+ return seenResults.includes(row.result) ? (
+
+
+ {cell.value}
+
+
+
+ {row.controller}
+
+
+ ) : (
+
+
+
+ {cell.value}
+
+
+
+
+ {row.controller}
+
+
+ );
+ },
+ },
+ {
+ Header: 'End Time',
+ accessor: 'end',
+ Cell: cell => (
+
+ {formatDate('with time', cell.value)}
+
+ ),
+ },
+ {
+ Header: 'Scheduled for deletion on',
+ accessor: 'deletion',
+ Cell: cell => {
+ const value = cell.row.original.serverMetadata['server.deletion'];
+ const remainingDays = getDiffDays(value);
+ return (
+
+
+ {getDiffDate(value)}
+
+
+
+ );
+ },
+ },
+ { Header: 'Status', accessor: 'status' },
+ {
+ Header: '',
+ accessor: 'fav',
+ Cell: cell =>
+ favoriteResults.includes(cell.row.original.result) ? (
+ {
+ e.stopPropagation();
+ this.unfavoriteResult(cell.row.original.result);
+ }}
+ />
+ ) : (
+ {
+ e.stopPropagation();
+ this.favoriteResult(cell.row.original.result);
+ }}
+ />
+ ),
+ },
+ {
+ Header: '',
+ accessor: 'action',
+ Cell: cell => {
+ const row = cell.row.original;
+ return (
+
+
this.showDropdown(e, `newrun${row.result}`)}
+ className="dropbtn"
+ />
+
+
+
this.removeResultFromSeen(e, [row.result])}
+ >
+ Mark unread
+
+
this.handleModalToggle(e, [row.result])}
+ >
+ Delete
+
+
+
+
+ );
+ },
+ },
+ ];
+ return (
+
+ {actionMessage !== '' ? (
+
+
+
+ ) : (
+ <>>
+ )}
+
+
+
+ All runs
+
+
+
+
+
+
{
+ this.retrieveResults(record);
+ }}
+ />
+
+
+
+
+
+ this.handleModalToggle(e, [])}
+ actions={[
+ this.deleteResult(e, toBeDeletedData)}
+ >
+ Delete
+ ,
+ this.handleModalToggle(e, [])}>
+ Cancel
+ ,
+ ]}
+ >
+
+ Overview
+ Are you sure you want to delete the following runs?
+ {toBeDeletedData.map(item => (
+
+
+
+ {item}
+
+
+ ))}
+
+
+
+ );
+ }
+}
+
+export default ExpiringResults;
diff --git a/src/pages/ExpiringResults/index.less b/src/pages/ExpiringResults/index.less
new file mode 100644
index 000000000..a8ee02e5f
--- /dev/null
+++ b/src/pages/ExpiringResults/index.less
@@ -0,0 +1,31 @@
+.paddingBig {
+ margin: 16px;
+}
+
+.dropdownContent {
+ position: absolute;
+ z-index: 1;
+ min-width: fit-content;
+ margin-left: -60%;
+ overflow: auto;
+ background-color: #f1f1f1;
+ box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
+}
+
+.dropdownLink {
+ display: block;
+ padding: 12px 16px;
+ color: black;
+ text-decoration: none;
+}
+
+.dropdownLink:hover {
+ background-color: #ddd;
+}
+
+.label {
+ margin: 3px;
+ padding: 2px 8px;
+ background-color: rgb(220 239 255);
+ border-radius: 25px;
+}
diff --git a/src/pages/LoginHandler/index.js b/src/pages/LoginHandler/index.js
index a4cab0fa2..6e45ba65e 100644
--- a/src/pages/LoginHandler/index.js
+++ b/src/pages/LoginHandler/index.js
@@ -1,125 +1,11 @@
-import React, { useState, useEffect } from 'react';
-import {
- Form,
- FormGroup,
- TextInput,
- Checkbox,
- ActionGroup,
- Button,
- Title,
-} from '@patternfly/react-core';
+import React from 'react';
import AuthLayout from '@/components/AuthLayout';
-import { connect } from 'dva';
-import styles from './index.less';
-import { validateEmail } from '@/utils/validator';
+import LoginForm from '@/components/LoginForm';
-const mapStateToProps = state => {
- const { auth } = state;
- return { auth };
-};
-
-const LoginHandler = props => {
- const [username, setUsername] = useState('');
- const [password, setPassword] = useState('');
- const [errors, setErrors] = useState({
- email: '',
- });
- const [btnDisabled, setBtnDisabled] = useState(true);
-
- const handleUsernameChange = val => {
- setUsername(val);
- const validEmail = validateEmail(val);
- setErrors({
- ...errors,
- ...validEmail,
- });
- };
-
- const handleLoginSubmit = () => {
- const { dispatch } = props;
- dispatch({
- type: 'auth/loginUser',
- payload: {
- username,
- password,
- },
- });
- };
-
- /* eslint-disable no-restricted-syntax */
- const validateForm = () => {
- if (username.trim() === '' || password.trim() === '') {
- return false;
- }
- for (const dep of Object.entries(errors)) {
- if (dep[1].length > 0) {
- return false;
- }
- }
- // if we reach here, it means
- // we have covered all of the edge cases.
- return true;
- };
-
- useEffect(
- () => {
- if (validateForm()) {
- setBtnDisabled(false);
- } else setBtnDisabled(true);
- },
- [username, password]
- );
-
- const form = (
-
+function LoginHandler() {
+ return (
+ } heading="Log into your Pbench Account" backOpt="true" />
);
- return ;
-};
+}
-export default connect(mapStateToProps)(LoginHandler);
+export default LoginHandler;
diff --git a/src/pages/LoginHandler/index.test.js b/src/pages/LoginHandler/index.test.js
index 8d4063c86..3bd19c5c9 100644
--- a/src/pages/LoginHandler/index.test.js
+++ b/src/pages/LoginHandler/index.test.js
@@ -4,9 +4,8 @@ import Adapter from 'enzyme-adapter-react-16';
import AuthLayout from '@/components/AuthLayout';
import LoginHandler from './index';
-const mockDispatch = jest.fn();
configure({ adapter: new Adapter() });
-const wrapper = shallow( , {
+const wrapper = shallow( , {
disableLifecycleMethods: true,
});
diff --git a/src/pages/Overview/index.js b/src/pages/Overview/index.js
index fbb303c5c..e89e1d199 100644
--- a/src/pages/Overview/index.js
+++ b/src/pages/Overview/index.js
@@ -1,16 +1,825 @@
-import React, { Component } from 'react';
-// import { routerRedux } from 'dva/router';
+import React from 'react';
+import { routerRedux } from 'dva/router';
+import {
+ Grid,
+ GridItem,
+ Card,
+ TextContent,
+ Text,
+ TextVariants,
+ Button,
+ Progress,
+ ProgressSize,
+ ProgressMeasureLocation,
+ ProgressVariant,
+ Tooltip,
+ Accordion,
+ AccordionItem,
+ AccordionContent,
+ AccordionToggle,
+ Label,
+ Modal,
+ Alert,
+ AlertGroup,
+} from '@patternfly/react-core';
+import { connect } from 'dva';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faStar, faStopwatch, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { OutlinedClockIcon, UndoAltIcon, EllipsisVIcon } from '@patternfly/react-icons';
+import { formatDate, getDiffDays, getDiffDate } from '../../utils/moment_constants';
+import Table from '@/components/Table';
+import styles from './index.less';
-class Overview extends Component {
+const expiringLimit = 15;
+
+@connect(({ global, user, loading, dashboard }) => ({
+ selectedDateRange: global.selectedDateRange,
+ selectedControllers: global.selectedControllers,
+ user: user.user,
+ favoriteResults: user.favoriteResults,
+ seenResults: user.seenResults,
+ results: dashboard.results,
+ loading: loading.effects['dashboard/fetchResults'],
+}))
+class Overview extends React.Component {
constructor(props) {
super(props);
- this.state = {};
+ this.state = {
+ totalResultData: [],
+ newData: [],
+ savedData: [],
+ expiringData: [],
+ expanded: true,
+ isModalOpen: false,
+ toBeDeletedData: [],
+ actionMessage: '',
+ };
+ }
+
+ componentDidMount() {
+ this.fetchRunResult();
+ }
+
+ onToggle() {
+ const { expanded } = this.state;
+ if (expanded) {
+ this.setState({ expanded: false });
+ } else {
+ this.setState({ expanded: true });
+ }
+ }
+
+ getRunResult() {
+ const { results, selectedControllers } = this.props;
+ const data = results[selectedControllers[0] || 'mock-controller'];
+ this.setState(
+ {
+ totalResultData: data,
+ },
+ () => this.getSeparatedResults()
+ );
+ }
+
+ getSeparatedResults() {
+ const { totalResultData } = this.state;
+ const savedData = totalResultData.filter(x => x.serverMetadata.dashboard.saved === true);
+ const newData = totalResultData.filter(x => x.serverMetadata.dashboard.saved !== true);
+ const expiringData = totalResultData.filter(
+ x => getDiffDays(x.serverMetadata['server.deletion']) < expiringLimit
+ );
+ this.setState({ newData, savedData, expiringData });
+ }
+
+ compareResults = selectedRows => {
+ const { dispatch } = this.props;
+
+ dispatch({
+ type: 'global/updateSelectedResults',
+ payload: selectedRows.map(row => row.original.result),
+ });
+
+ dispatch(
+ routerRedux.push({
+ pathname: '/comparison-select',
+ })
+ );
+ };
+
+ favoriteResult = result => {
+ const { dispatch } = this.props;
+ // dispatch an action to favorite controller
+ dispatch({
+ type: 'user/favoriteResult',
+ payload: result,
+ });
+ };
+
+ unfavoriteResult = result => {
+ const { dispatch } = this.props;
+ // dispatch an action to unfavorite controller
+ dispatch({
+ type: 'user/removeResultFromFavorites',
+ payload: result,
+ });
+ };
+
+ markResultSeen = row => {
+ const { dispatch } = this.props;
+ dispatch({
+ type: 'user/markResultSeen',
+ payload: row.result,
+ });
+ };
+
+ removeResultFromSeen = (e, row) => {
+ // Stop propagation from going to the next page
+ if (e !== null) {
+ e.stopPropagation();
+ }
+ const { dispatch } = this.props;
+ dispatch({
+ type: 'user/removeResultFromSeen',
+ payload: row,
+ });
+ };
+
+ showDropdown = (e, id) => {
+ // Stop propagation from going to the next page
+ e.stopPropagation();
+
+ const dropdownElem = document.getElementById(id);
+ if (dropdownElem.style.display === 'none') {
+ dropdownElem.style.display = 'block';
+ } else {
+ dropdownElem.style.display = 'none';
+ }
+ };
+
+ saveRuns = (e, rows) => {
+ // Stop propagation from going to the next page
+ if (e !== null) {
+ e.stopPropagation();
+ }
+ const { totalResultData } = this.state;
+ const { dispatch } = this.props;
+ const keys = rows.map(({ original }) => original.result);
+ keys.forEach(key => {
+ totalResultData.filter(item => item.result === key)[0].serverMetadata.dashboard.saved = true;
+ });
+ dispatch({
+ type: 'dashboard/updateResults',
+ payload: totalResultData,
+ }).then(() => {
+ this.setState({
+ totalResultData,
+ });
+ this.getSeparatedResults();
+ });
+ };
+
+ deleteResult = (e, rows) => {
+ // Stop propagation from going to the next page
+ if (e !== null) {
+ e.stopPropagation();
+ }
+ const { totalResultData } = this.state;
+ const { dispatch } = this.props;
+ const updatedResult = totalResultData.filter(item => !rows.includes(item.result));
+ dispatch({ type: 'dashboard/updateResults', payload: updatedResult })
+ .then(() => {
+ this.setState({ totalResultData: updatedResult });
+ this.getSeparatedResults();
+ })
+ .then(() => {
+ this.handleModalToggle(e, []);
+ })
+ .then(() => {
+ this.setState({ actionMessage: 'data succesfully deleted' });
+ setTimeout(() => {
+ this.setState({ actionMessage: '' });
+ }, 3000);
+ });
+ };
+
+ retrieveResults = row => {
+ const { dispatch } = this.props;
+ this.markResultSeen(row);
+ dispatch({
+ type: 'global/updateSelectedResults',
+ payload: [row],
+ }).then(() => {
+ dispatch(
+ routerRedux.push({
+ pathname: '/result',
+ })
+ );
+ });
+ };
+
+ navigateToExpiringResult = () => {
+ const { dispatch } = this.props;
+ dispatch(
+ routerRedux.push({
+ pathname: '/expiring-results',
+ })
+ );
+ };
+
+ fetchRunResult() {
+ const { dispatch, results, selectedDateRange, selectedControllers } = this.props;
+ if (Object.keys(results).length === 0) {
+ dispatch({
+ type: 'dashboard/fetchResults',
+ payload: {
+ selectedDateRange,
+ controller: selectedControllers,
+ },
+ }).then(() => this.getRunResult());
+ } else {
+ this.getRunResult();
+ }
+ }
+
+ handleModalToggle(e, rows) {
+ // Stop propagation from going to the next page
+ if (e !== null) {
+ e.stopPropagation();
+ }
+ this.setState(({ isModalOpen }) => ({ isModalOpen: !isModalOpen, toBeDeletedData: rows }));
}
render() {
+ const {
+ newData,
+ savedData,
+ expiringData,
+ toBeDeletedData,
+ expanded,
+ isModalOpen,
+ actionMessage,
+ } = this.state;
+ const { favoriteResults, seenResults } = this.props;
+ const newDataColumns = [
+ {
+ Header: 'Result',
+ accessor: 'result',
+ Cell: cell => {
+ const row = cell.row.original;
+ return seenResults.includes(row.result) ? (
+
+
+ {cell.value}
+
+
+
+ {row.controller}
+
+
+ ) : (
+
+
+
+ {cell.value}
+
+
+
+
+ {row.controller}
+
+
+ );
+ },
+ },
+ {
+ Header: 'End Time',
+ accessor: 'end',
+ Cell: cell => (
+
+ {formatDate('without time', cell.value)}
+
+ ),
+ },
+ {
+ Header: 'Scheduled for deletion on',
+ accessor: 'deletion',
+ Cell: cell => {
+ const value = cell.row.original.serverMetadata['server.deletion'];
+ const remainingDays = getDiffDays(value);
+ return (
+
+
+ {getDiffDate(value)}
+
+
+
+ );
+ },
+ },
+ {
+ Header: '',
+ accessor: 'fav',
+ Cell: cell =>
+ favoriteResults.includes(cell.row.original.result) ? (
+ {
+ e.stopPropagation();
+ this.unfavoriteResult(cell.row.original.result);
+ }}
+ />
+ ) : (
+ {
+ e.stopPropagation();
+ this.favoriteResult(cell.row.original.result);
+ }}
+ />
+ ),
+ },
+ {
+ Header: '',
+ accessor: 'action',
+ Cell: cell => {
+ const row = cell.row.original;
+ return (
+
+
this.showDropdown(e, `newrun${row.result}`)}
+ className="dropbtn"
+ />
+
+
+
this.saveRuns(e, [cell.row])}>
+ Save Runs
+
+
this.removeResultFromSeen(e, [row.result])}
+ >
+ Mark unread
+
+
this.handleModalToggle(e, [row.result])}
+ >
+ Delete
+
+
+
+
+ );
+ },
+ },
+ ];
+
+ const savedDataColumns = [
+ {
+ Header: 'Result',
+ accessor: 'result',
+ Cell: cell => {
+ const row = cell.row.original;
+ return seenResults.includes(row.result) ? (
+
+
+ {cell.value}
+
+
+
+ {row.controller}
+
+
+ ) : (
+
+
+
+ {cell.value}
+
+
+
+
+ {row.controller}
+
+
+ );
+ },
+ },
+ {
+ Header: 'End Time',
+ accessor: 'end',
+ Cell: cell => (
+
+ {formatDate('without time', cell.value)}
+
+ ),
+ },
+ {
+ Header: 'Scheduled for deletion on',
+ accessor: 'deletion',
+ Cell: cell => {
+ const value = cell.row.original.serverMetadata['server.deletion'];
+ const remainingDays = getDiffDays(value);
+ return (
+
+
+ {getDiffDate(value)}
+
+
+
+ );
+ },
+ },
+ {
+ Header: 'Status',
+ accessor: 'status',
+ Cell: cell => cell.row.original.serverMetadata['dataset.access'],
+ },
+ {
+ Header: '',
+ accessor: 'fav',
+ Cell: cell =>
+ favoriteResults.includes(cell.row.original.result) ? (
+ {
+ e.stopPropagation();
+ this.unfavoriteResult(cell.row.original.result);
+ }}
+ />
+ ) : (
+ {
+ e.stopPropagation();
+ this.favoriteResult(cell.row.original.result);
+ }}
+ />
+ ),
+ },
+ {
+ Header: '',
+ accessor: 'action',
+ Cell: cell => {
+ const row = cell.row.original;
+ return (
+
+
this.showDropdown(e, `newrun${row.result}`)}
+ className="dropbtn"
+ />
+
+
+
this.removeResultFromSeen(e, [row.result])}
+ >
+ Mark unread
+
+
this.handleModalToggle(e, [row.result])}
+ >
+ Delete
+
+
+
+
+ );
+ },
+ },
+ ];
+
+ const expiringSoonTableColumn = [
+ {
+ Header: 'Result',
+ accessor: 'result',
+ Cell: cell => {
+ const row = cell.row.original;
+ let isSeen = false;
+ if (seenResults !== []) {
+ seenResults.forEach(item => {
+ if (item.key === row.key) {
+ isSeen = true;
+ }
+ });
+ }
+ if (isSeen) {
+ return (
+
+
+ {cell.value}
+
+
+
+
+
+
+ {formatDate('utc', row.deletion)}
+
+
+
+
+
+ );
+ }
+ return (
+
+
+
+ {cell.value}
+
+
+
+
+
+
+ {formatDate('with time', row.deletion)}
+
+
+
+ );
+ },
+ },
+ ];
+
return (
-
-
Overview Page
+
+ {actionMessage !== '' && (
+
+
+
+ )}
+
+
+
+
+ Overview
+
+
+
+
+
+
+
+
+
0 ? styles.expiringCard : styles.noExpiringRunCard
+ }
+ >
+
+ this.onToggle()}
+ isExpanded={expanded}
+ style={{ '--pf-c-accordion__toggle--before--BackgroundColor': 'white' }}
+ >
+
+
+
+
+ Expiring Soon
+
+
+ {expiringData.length} runs
+ {' '}
+
+
+
+
+
+
+
+
+ {expiringData.length > 0 ? (
+
+
+
+
+ These runs will be automatically deleted from the sysem if left
+ unacknowledged.
+
+ Learn more
+
+
+
+
+
+
+ ) : (
+
+
+
+
+ {' '}
+ You have no runs expiring soon
+
+
+ Runs that have expiration date within next 10days will appear here.
+ These runs will be autumatically removed from the system if left
+ unacknowledged. Learn More.
+
+
+
+ )}
+
+
+ this.navigateToExpiringResult()}
+ >
+ View all warnings
+
+
+
+
+
+
+
+
+
+
+ {' '}
+ Saved Runs
+
+
+ {savedData.length} runs
+ {' '}
+
+
+ this.navigateToExpiringResult()} variant="secondary">
+ Go to all runs
+
+
+
+
+
+
+
+ {savedData.length > 0 ? (
+
this.compareResults(selectedRowIds)}
+ columns={savedDataColumns}
+ onRowClick={record => {
+ this.retrieveResults(record);
+ }}
+ data={savedData}
+ saveRuns={selectedRowIds => this.saveRuns(null, selectedRowIds)}
+ removeResultFromSeen={(e, selectedRowIds) =>
+ this.removeResultFromSeen(e, selectedRowIds)
+ }
+ favoriteResult={selectedRowIds => {
+ this.favoriteResult(selectedRowIds);
+ }}
+ deleteResult={selectedRowIds =>
+ this.handleModalToggle(null, selectedRowIds)
+ }
+ isCheckable
+ />
+ ) : (
+
+
+
+ You have no saved runs
+
+ Runs that you have saved will appear here. These runs will be
+ autumatically removed from the system if left unacknowledged.{' '}
+ Learn More.
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ {' '}
+ New and unmanaged Runs
+
+
+ {newData.length} runs
+ {' '}
+
+
+ }
+ onClick={() => this.fetchRunResult()}
+ >
+ Refresh results
+
+
+
+
+
+
+
+
this.compareResults(selectedRowIds)}
+ columns={newDataColumns}
+ onRowClick={record => {
+ this.retrieveResults(record);
+ }}
+ data={newData}
+ saveRuns={selectedRowIds => this.saveRuns(null, selectedRowIds)}
+ removeResultFromSeen={selectedRowIds =>
+ this.removeResultFromSeen(null, selectedRowIds)
+ }
+ favoriteResult={selectedRowIds => {
+ this.favoriteResult(selectedRowIds);
+ }}
+ deleteResult={selectedRowIds => this.handleModalToggle(null, selectedRowIds)}
+ isCheckable
+ />
+
+
+
+
+
+
+ this.handleModalToggle(e, [])}
+ actions={[
+ this.deleteResult(e, toBeDeletedData)}
+ >
+ Delete
+ ,
+ this.handleModalToggle(e, [])}>
+ Cancel
+ ,
+ ]}
+ >
+
+ Overview
+ Are you sure you want to delete the following runs?
+ {toBeDeletedData.map(item => (
+
+
+
+ {item}
+
+
+ ))}
+
+
);
}
diff --git a/src/pages/Overview/index.less b/src/pages/Overview/index.less
new file mode 100644
index 000000000..341352028
--- /dev/null
+++ b/src/pages/Overview/index.less
@@ -0,0 +1,180 @@
+*::-webkit-scrollbar {
+ width: 1em;
+}
+*::-webkit-scrollbar-thumb {
+ height: 1em;
+ background-color: rgba(0, 0, 0, 0.15);
+ background-clip: padding-box;
+ border: 0.5em solid rgba(0, 0, 0, 0);
+ -webkit-border-radius: 1em;
+ -webkit-box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.025);
+}
+*::-webkit-scrollbar-button {
+ display: none;
+ width: 0;
+ height: 0;
+}
+*::-webkit-scrollbar-corner {
+ background-color: transparent;
+}
+
+div.pf-c-backdrop {
+ background-color: None !important;
+}
+
+li {
+ list-style-type: none;
+}
+
+ul {
+ margin-left: 0 !important;
+ padding-left: 0 !important;
+ list-style: none !important;
+}
+
+a {
+ color: none !important;
+ text-decoration: none !important;
+}
+
+.displayFlex {
+ display: flex;
+ width: 100%;
+}
+
+.flexEnd {
+ float: right;
+ width: 50%;
+}
+
+.flexItem {
+ align-self: flex-end;
+ width: 50%;
+}
+
+.centertext {
+ text-align: center;
+}
+
+.paddingSmall {
+ margin: 8px;
+ margin-left: 16px;
+}
+
+.paddingLeft {
+ padding-left: 4px;
+}
+
+.paddingRight {
+ padding-right: 4px;
+}
+
+.marginLeft {
+ margin-left: 4px;
+}
+
+.marginRight {
+ margin-right: 4px;
+}
+
+.savedRunCard {
+ min-height: 188vh;
+ max-height: 188vh;
+ overflow: scroll;
+}
+
+.savedRunCardExpanded {
+ max-height: 147.5vh;
+ min-height: 147.5vh;
+}
+
+.newRunCard {
+ height: 200vh;
+ overflow: scroll;
+}
+
+.expiringCard {
+ max-height: 50vh;
+ overflow: scroll;
+}
+
+.newRunTable {
+ overflow: scroll;
+}
+
+.expiringValues {
+ overflow: scroll;
+}
+
+.paddingBig {
+ margin: 16px;
+}
+
+.marginBottom {
+ margin-bottom: 3vh;
+}
+
+.subCard {
+ border-top: solid 1px #cfcfcf;
+}
+
+.subText {
+ color: #4c4c4c;
+ font-size: smaller;
+}
+
+.icons {
+ margin-right: 8px;
+}
+
+.label {
+ margin: 3px;
+ padding: 2px 8px;
+ background-color: rgb(220 239 255);
+ border-radius: 25px;
+}
+
+.dropdownContent {
+ position: absolute;
+ z-index: 1;
+ min-width: fit-content;
+ margin-left: -60%;
+ overflow: auto;
+ background-color: #f1f1f1;
+ box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
+}
+
+.dropdownLink {
+ display: block;
+ padding: 12px 16px;
+ color: black;
+ text-decoration: none;
+}
+
+.dropdownLink:hover {
+ background-color: #ddd;
+}
+
+.stopwatchIcon {
+ font-size: 5rem;
+}
+
+button.pf-c-accordion__toggle.pf-m-expanded {
+ --pf-c-accordion__toggle--before--BackgroundColor: white !important;
+}
+
+.expiringCard,
+.newRunCard,
+.savedRunCard {
+ border: 1px solid #bec1c3;
+}
+
+.noExpiringRunCard {
+ border-top: solid 3px red;
+ max-height: 50vh;
+ overflow: scroll;
+}
+
+.expiringTable {
+ background-color: red !important;
+}
diff --git a/src/pages/Result/index.js b/src/pages/Result/index.js
new file mode 100644
index 000000000..95fc9f345
--- /dev/null
+++ b/src/pages/Result/index.js
@@ -0,0 +1,66 @@
+import React, { Component } from 'react';
+import { connect } from 'dva';
+import {
+ TextContent,
+ Text,
+ TextVariants,
+ Hint,
+ HintBody,
+ Flex,
+ FlexItem,
+} from '@patternfly/react-core';
+import { ExclamationTriangleIcon } from '@patternfly/react-icons';
+import styles from './index.less';
+import Summary from '../Summary';
+
+@connect(({ global, loading }) => ({
+ selectedResults: global.selectedResults,
+ loading: loading.effects['dashboard/fetchResults'],
+}))
+class Result extends Component {
+ render() {
+ const { selectedResults } = this.props;
+ const acceptanceStatus = selectedResults[0].saved;
+ const unAcceptedHint = (
+
+
+
+
+
+
+
+
+ You haven't managed the run yet
+
+
+
+ Accept this run
+
+
+ Delete run
+
+
+
+
+
+
+ );
+
+ return (
+
+
+
+ {selectedResults[0].result}
+ {selectedResults[0].controller}
+
+ {acceptanceStatus === true ? '' : unAcceptedHint}
+
+
+
+
+
+ );
+ }
+}
+
+export default Result;
diff --git a/src/pages/Result/index.less b/src/pages/Result/index.less
new file mode 100644
index 000000000..f18e8e015
--- /dev/null
+++ b/src/pages/Result/index.less
@@ -0,0 +1,38 @@
+.paddingBig {
+ margin: 16px;
+}
+
+.paddingSmall {
+ margin-bottom: 8px;
+}
+
+.info {
+ color: #2c9af3;
+}
+
+.warning {
+ color: #faad14;
+}
+
+.actionBtn {
+ margin-right: 12px;
+ font-size: smaller;
+}
+
+.customAccepteddHint {
+ border: 0;
+ border-top: 2px solid #2c9af3;
+}
+
+.customUnAccepteddHint {
+ background-color: #faad141a;
+ border: 0;
+ border-top: 2px solid #faad14;
+}
+
+.label {
+ margin: 3px;
+ padding: 2px 8px;
+ background-color: rgb(220 239 255);
+ border-radius: 25px;
+}
diff --git a/src/utils/moment_constants.js b/src/utils/moment_constants.js
index af60106f5..1d882e572 100644
--- a/src/utils/moment_constants.js
+++ b/src/utils/moment_constants.js
@@ -37,6 +37,25 @@ export function getAllMonthsWithinRange(endpoints, index, selectedDateRange) {
return queryString;
}
+export const formatDate = (format, givenDate) => {
+ switch (format) {
+ case 'utc':
+ return moment(givenDate).format();
+ case 'without time':
+ return moment(givenDate).format('Do MMMM YYYY');
+ case 'with time':
+ return moment(givenDate).format('Do MMMM YYYY, h:mm:ss a');
+ default:
+ return givenDate;
+ }
+};
+
export const getDiffDate = givenDate => {
return moment(givenDate).fromNow();
};
+
+export const getDiffDays = givenDate => {
+ const futureDate = moment(givenDate);
+ const currDate = moment(new Date());
+ return futureDate.diff(currDate, 'days');
+};