GitLab will be upgraded on June 23rd evening. During the upgrade the service will be unavailable, sorry for the inconvenience.

Commit ff4f5bf0 authored by Marjolaine Bodin's avatar Marjolaine Bodin

Merge branch 'issue_496' into 'milestone-logbook-search-settings'

Issue 496

See merge request !508
parents 4f7ee7f1 028b3390
Pipeline #46546 passed with stage
in 2 minutes and 58 seconds
import { SET_LOGBOOK_CONTEXT } from '../constants/actionTypes';
import {
SET_CATEGORY_TYPE,
UNSET_CATEGORY_TYPE,
SET_LOGBOOK_CONTEXT,
} from '../constants/actionTypes';
/**
* Set the logbook context ie the context in which the logbook is used. Here the context corresponds to :
......@@ -13,3 +17,25 @@ export function setLogbookContextAction(context) {
context,
};
}
/**
* Set a type, category to be shown in the logbook page
* @param {*} type object with the different filters status
*/
export function setCategoryTypes(categoryType) {
return {
type: SET_CATEGORY_TYPE,
categoryType,
};
}
/**
* Unet a type, category to be shown in the logbook page
* @param {*} type object with the different filters status
*/
export function unsetCategoryTypes(categoryType) {
return {
type: UNSET_CATEGORY_TYPE,
categoryType,
};
}
.returnCheckbox {
margin-bottom: 10px !important;
}
.categoryText {
margin-top: 10px;
}
import React from 'react';
import { Col, Grid, Panel, Row, Checkbox } from 'react-bootstrap';
import {
EVENT_CATEGORY_COMMANDLINE,
EVENT_CATEGORY_ERROR,
EVENT_CATEGORY_INFO,
ANNOTATION,
NOTIFICATION,
} from '../../../constants/eventTypes';
import { useDispatch, useSelector } from 'react-redux';
import styles from './EventFilterPanel.module.css';
import { setCategoryTypes, unsetCategoryTypes } from '../../../actions/logbook';
const checkBoxes = [
{
name: 'showComments',
label: 'Comments',
value: [{ type: ANNOTATION }],
},
{
name: 'showInformation',
label: 'Information',
value: [{ type: NOTIFICATION, category: EVENT_CATEGORY_INFO }],
},
{
name: 'showError',
label: 'Errors',
value: [{ type: NOTIFICATION, category: EVENT_CATEGORY_ERROR }],
},
{
name: 'showCommandLine',
label: 'Command Lines',
value: [{ type: NOTIFICATION, category: EVENT_CATEGORY_COMMANDLINE }],
},
];
/*
* React component which renders a panel showing the filter that can be applied on events
*/
export default function SettingLogbookMenuPanel() {
const categoryTypes = useSelector((state) => state.logbook.categoryTypes);
const dispatch = useDispatch();
const getValueByName = (name) =>
checkBoxes.find((cb) => cb.name === name).value;
/** Checkbox will be checked if and only if its values are in the categoryTypes */
const isChecked = (values) => {
let found = true;
values.forEach((value) => {
if (
categoryTypes &&
categoryTypes.find(
(ct) => ct.type === value.type && ct.category === value.category
) == null
) {
found = false;
}
});
return found;
};
return (
<Panel>
<Panel.Heading>
<Panel.Title componentClass="h2">Settings</Panel.Title>
</Panel.Heading>
<Panel.Body>
<Grid fluid>
<Col md={2} sm={6} xs={6}>
<h4>Log types</h4>
<Grid>
<Row>
{checkBoxes.map((checkbox) => (
<Row>
<Col md={2} sm={6} xs={6}>
<Checkbox
className={styles.returnCheckbox}
name={checkbox.name}
checked={isChecked(checkbox.value)}
value={checkbox.value}
onClick={(e) => {
if (e.target.checked) {
dispatch(
setCategoryTypes(getValueByName(e.target.name))
);
} else {
dispatch(
unsetCategoryTypes(getValueByName(e.target.name))
);
}
}}
>
{checkbox.label}
</Checkbox>
</Col>
</Row>
))}
</Row>
</Grid>
</Col>
</Grid>
</Panel.Body>
</Panel>
);
}
......@@ -28,7 +28,7 @@ const parseCSV = (csv, metric) => {
for (let j = 0; j < line.length; j++) {
let value = parseFloat(line[j]);
if (metric.toLowerCase() === 'volume') {
value = parseFloat(value / GB).toFixed(4);
value = parseFloat(value / GB).toFixed(1);
}
line[j] = value > 0 ? value : null;
}
......
......@@ -28,10 +28,10 @@ const UI = {
},
logbook: {
/** Number of logbook events to display per page. EVENTS_PER_PAGE should be lower than EVENTS_PER_DOWNLOAD */
EVENTS_PER_PAGE: 1000,
EVENTS_PER_PAGE: 4,
/** Maximum number of logbook events downloaded from the server. This enables to store a larger set of
events than those required for a single page thus reducing HTTP requests to the server. */
EVENTS_PER_DOWNLOAD: 1000,
EVENTS_PER_DOWNLOAD: 4,
/* the field used to sort events. Most of the time 'creationDate' is used. Possible values: 'creationDate', '_id', 'createdAt', 'updatedAt' */
SORT_EVENTS_BY: '_id',
/* the order the events sorted by SORT_EVENTS_BY will be ordered. Possible values: 1 (for ascending order), -1 (for descending order)*/
......
......@@ -29,6 +29,9 @@ export const FETCH_DATASETS_BY_DOI_PENDING = 'FETCH_DATASETS_BY_DOI_PENDING';
/** logbook **/
export const SET_LOGBOOK_CONTEXT = 'SET_LOGBOOK_CONTEXT';
export const FILTER_EVENTS = 'FILTER_EVENTS';
export const SET_CATEGORY_TYPE = 'SET_CATEGORY_TYPE';
export const UNSET_CATEGORY_TYPE = 'UNSET_CATEGORY_TYPE';
/** selection */
export const SELECT_DATASETS = 'SELECT_DATASETS';
......
......@@ -8,9 +8,6 @@ export const EVENT_CATEGORY_INFO = 'info';
export const EVENT_CATEGORY_DEBUG = 'debug';
export const EVENT_CATEGORY_ERROR = 'error';
export const LIST_VIEW = 'list';
export const DOC_VIEW = 'doc';
export const NEW_EVENT_VISIBLE = 'newEventExpanded';
export const NEW_EVENT_INVISIBLE = 'newEventCollapsed';
......
import React, { useEffect } from 'react';
import { Col, Grid, Row } from 'react-bootstrap';
import { useParams } from 'react-router';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import TabContainerMenu from '../components/TabContainerMenu/TabContainerMenu';
import LogbookContainer from './Logbook/LogbookContainer';
import PageNotFound from './PageNotFound';
......@@ -12,13 +12,29 @@ import { useScrollToHash, useQuery } from '../helpers/hooks';
function EventsPage() {
const { investigationId } = useParams();
const categoryTypes = useSelector((state) => state.logbook.categoryTypes);
const investigation = useResource(InvestigationResource.detailShape(), {
id: investigationId,
});
const query = useQuery();
const page = query.get('page') || 1;
useScrollToHash({ milliseconds: 350, attempts: 50 });
useScrollToHash({ milliseconds: 100, attempts: 100 });
/*
const criteria = query.get('criteria');
const search = query.get('search');
if (criteria && search) {
const a = criteria.split(',');
const b = search.split(',');
const c = a.map((e, i) => {
return [e, b[i]];
});
console.log(c);
}
*/
const dispatch = useDispatch();
......@@ -31,13 +47,16 @@ function EventsPage() {
if (!investigation) {
return <PageNotFound />;
}
return (
<Grid fluid>
<Row>
<Col sm={12}>
<TabContainerMenu />
<LogbookContainer investigation={investigation} page={page} />
<LogbookContainer
categoryTypes={categoryTypes}
investigation={investigation}
page={page}
/>
</Col>
</Row>
</Grid>
......
......@@ -5,8 +5,8 @@ import axios from 'axios';
import Loader from '../../components/Loader';
import { getEventsByInvestigationId } from '../../api/icat-plus/logbook';
import { getSearchFilter } from './SelectionFilterHelper';
import { LIST_VIEW, DOC_VIEW } from '../../constants/eventTypes';
import { buildSearchFilter } from './SelectionFilterHelper';
import { ANNOTATION, NOTIFICATION } from '../../constants/eventTypes';
import { store } from '../../store';
import { getFirstIndexToDisplay } from '../../helpers/eventsProviderHelper';
import UserMessage from '../../components/UserMessage';
......@@ -16,7 +16,13 @@ import UserMessage from '../../components/UserMessage';
*/
function EventsProvider(props) {
const { children, investigationId, sortingFilter } = props;
const { searchCriteria, forceReload, view, page } = props;
const {
searchCriteria,
forceReload,
filterType,
filterCategory,
page,
} = props;
const [downloadedEvents, setDownloadedEvents] = useState({}); //{ isFetching: bool, events: [], startIndex: Number, endIndex: Number, isError: bool}
......@@ -28,25 +34,36 @@ function EventsProvider(props) {
useEffect(() => {
setDownloadedEvents({});
}, [investigationId, sortingFilter, searchCriteria, view, forceReload]);
}, [
investigationId,
sortingFilter,
searchCriteria,
filterType,
filterCategory,
forceReload,
]);
useEffect(() => {
if (isFetching || isDownloaded || isError) {
return;
}
const selectionFilter = getSearchFilter(searchCriteria, view);
const selectionFilter = buildSearchFilter(
searchCriteria,
filterType,
filterCategory
);
setDownloadedEvents({ isFetching: true });
getEvents(selectionFilter, sortingFilter, skip, investigationId)
.then(({ data }) => {
// Filter notifications that have been annotated which annotation is empty
if (view === DOC_VIEW) {
if (filterType.length === 1 && filterType[0] === ANNOTATION) {
// debugger;
data = data.filter(
(event) =>
!(
event.type === 'notification' &&
event.type === NOTIFICATION &&
(!event.content[0].text || event.content[0].text.length === 0)
)
);
......@@ -66,7 +83,8 @@ function EventsProvider(props) {
skip,
sortingFilter,
searchCriteria,
view,
filterType,
filterCategory,
isDownloaded,
isFetching,
isError,
......@@ -108,8 +126,9 @@ EventsProvider.propTypes = {
sortingFilter: PropTypes.object,
/** search criteria */
searchCriteria: PropTypes.array,
/** current logbook view */
view: PropTypes.oneOf([LIST_VIEW, DOC_VIEW]),
/** filter criteria, by type and by category */
filterType: PropTypes.array,
filterCategory: PropTypes.array,
/** Whether the events must be forced reloaded. (when no others props changed such as page, sorting filter, searchCriteria, ...) */
forceReload: PropTypes.bool,
};
......
This diff is collapsed.
import escapeStringRegexp from 'escape-string-regexp';
import UI from '../../config/ui';
import { DOC_VIEW } from '../../constants/eventTypes';
import { ANNOTATION, NOTIFICATION } from '../../constants/eventTypes';
import moment from 'moment';
const REGEXP_FILTER_TYPE = 'regexpFilterType';
......@@ -15,7 +15,8 @@ const EQUALITY_FILTER_TYPE = 'equalityFilterType';
export function getSelectionFiltersForMongoQuery(
findCriteria,
sortCriteria,
view
filterType,
filterCategory
) {
if (!findCriteria) {
findCriteria = [];
......@@ -25,7 +26,7 @@ export function getSelectionFiltersForMongoQuery(
}
return {
find: getSearchFilter(findCriteria, view),
find: buildSearchFilter(findCriteria, filterType, filterCategory),
sort: sortCriteria,
};
}
......@@ -42,29 +43,17 @@ export function getSelectionFilterForAllAnnotationsAndNotifications() {
}
/**
* Get search filter to be used in mongoDB query
* @param {array} criteria user specified search criteria
* @param {string} view current view of the logbook
* builds the selection filter based on filter on events type / category and user search criteria
* @param {*} criteria search criteria as provided by the combosearch component.
* @param {*} filterType array of filter on event type
* @param {*} filterCategory array of filter on event category
* @returns {Object} selection filter query to be used
*/
export function getSearchFilter(criteria, view) {
export function buildSearchFilter(criteria, filterType, filterCategory) {
const filter = {};
const andExpressions = [];
// the first part of the AND filter which selects annotation and notification systematically
if (view === DOC_VIEW) {
andExpressions.push({
$or: [
{ type: 'annotation' },
{
$and: [
{ type: 'notification' },
{ previousVersionEvent: { $ne: null } },
],
},
],
});
} else {
andExpressions.push(getNotificationOrAnnotationFilter());
}
andExpressions.push(buildEventFilter(filterType, filterCategory));
const userSpecificFilters = getUserSpecificSelectionFilters(criteria);
if (userSpecificFilters) {
......@@ -74,6 +63,113 @@ export function getSearchFilter(criteria, view) {
return filter;
}
/**
* builds the selection filter based on filter on events type / category
* @param {*} filterType array of filter on event type
* @param {*} filterCategory array of filter on event category
* @returns {Object} selection filter query to be used
*/
export function buildEventFilter(filterType, filterCategory) {
const filter = {};
if (
isArrayUndefinedOrEmpty(filterType) ||
(isFilterOnlyNotification(filterType) &&
isArrayUndefinedOrEmpty(filterCategory))
) {
return buildNoTypeFilter();
}
const orFilter = [];
Array.prototype.forEach.call(filterType, (filter) => {
if (filter === ANNOTATION) {
orFilter.push(buildEventFilterForUserComment());
}
if (filter === NOTIFICATION) {
orFilter.push(buildEventFilterForNotifcation(filterCategory));
}
});
filter.$or = orFilter;
return filter;
}
function isFilterOnlyNotification(filterType) {
return filterType.length === 1 && filterType[0] === NOTIFICATION;
}
function isArrayUndefinedOrEmpty(array) {
return !array || array.length === 0;
}
function buildNoTypeFilter() {
const filter = {};
const andExpressions = [];
const filterItem = popSingleFilter(
{},
{ criteria: 'type', search: 'undefined' },
EQUALITY_FILTER_TYPE
);
andExpressions.push(filterItem);
filter.$and = andExpressions;
return filter;
}
/**
* builds the selection filter for notification events
* @param {*} filterCategory array of filter on event category
* @returns {Object} selection filter query to be used
*/
export function buildEventFilterForNotifcation(filterCategory) {
const filter = {};
if (!filterCategory || filterCategory.length === 0) {
return filter;
}
const andExpressions = [];
const filterItem = popSingleFilter(
{},
{ criteria: 'type', search: NOTIFICATION },
EQUALITY_FILTER_TYPE
);
andExpressions.push(filterItem);
andExpressions.push(buildEventCategoryFilter(filterCategory));
filter.$and = andExpressions;
return filter;
}
/**
* builds the selection filter based on category
* @param {*} filterCategory array of filter on event category
* @returns {Object} selection filter query to be used
*/
export function buildEventCategoryFilter(filterCategory) {
const filter = {};
const orFilter = [];
Array.prototype.forEach.call(filterCategory, (filter) => {
const orFilterItem = popSingleFilter(
{},
{ criteria: 'category', search: filter },
EQUALITY_FILTER_TYPE
);
orFilter.push(orFilterItem);
});
filter.$or = orFilter;
return filter;
}
/**
* builds the selection filter for annotation events + notification with previous events
* @returns {Object} selection filter query to be used
*/
export function buildEventFilterForUserComment() {
// annotation type or notification with a previousVersionEvent
return {
$or: [
{ type: ANNOTATION },
{
$and: [{ type: NOTIFICATION }, { previousVersionEvent: { $ne: null } }],
},
],
};
}
/**
* Generate the user specific part of the selection filters from GUI specified search criteria.
* @param {array} criteria search criteria as provided by the combosearch component. When not provided, this indicates that the user has not set any search criteria.
......
import { SET_LOGBOOK_CONTEXT } from '../constants/actionTypes';
import {
SET_CATEGORY_TYPE,
UNSET_CATEGORY_TYPE,
SET_LOGBOOK_CONTEXT,
} from '../constants/actionTypes';
import {
ANNOTATION,
EVENT_CATEGORY_ERROR,
EVENT_CATEGORY_INFO,
NOTIFICATION,
} from '../constants/eventTypes';
// initialize the logbook redux state
const initialState = {
......@@ -6,12 +16,17 @@ const initialState = {
name: null,
isReleased: null,
},
categoryTypes: [
{ type: ANNOTATION },
{ type: NOTIFICATION, category: EVENT_CATEGORY_INFO },
{ type: NOTIFICATION, category: EVENT_CATEGORY_ERROR },
],
};
/**
* The logbook reducer. THe reducer takes the current state and the action and returns the new state
*/
const logbookReducer = (state = initialState, action) => {
const logbook = (state = initialState, action) => {
switch (action.type) {
case SET_LOGBOOK_CONTEXT: {
state = {
......@@ -23,12 +38,46 @@ const logbookReducer = (state = initialState, action) => {
};
break;
}
case SET_CATEGORY_TYPE: {
action.categoryType.forEach((categoryType) => {
// If categoryType does not exist then it is added to state
if (
state.categoryTypes.find(
(ct) =>
ct.type === categoryType.type &&
ct.category === categoryType.category
) == null
) {
state = {
...state,
categoryTypes: [...state.categoryTypes, categoryType],
};
}
});
break;
}
case UNSET_CATEGORY_TYPE: {
action.categoryType.forEach((categoryType) => {
state = {
...state,
categoryTypes: state.categoryTypes.filter(
(ct) =>
!(
ct.type === categoryType.type &&
ct.category === categoryType.category
)
),
};
});
break;
}
default:
break; // leave the state unchanged when the action should have no impact on current logbook store state
}
return state;
};
export default logbookReducer;
export default logbook;
......@@ -9,7 +9,7 @@ import reducer from './reducers';
const persistConfig = {
key: 'root',
storage: LocalStorage,
whitelist: ['user'],
whitelist: ['user', 'logbook'],
};
const persistedReducer = persistReducer(persistConfig, reducer);
......
......@@ -2,7 +2,6 @@ import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import React from 'react';
import EventListMenu from '../../components/Logbook/Menu/EventListMenu';
import { LIST_VIEW } from '../../constants/eventTypes';
const resources = require('./resources/EventListMenu.resource.js');
......@@ -96,7 +95,7 @@ function getShallowWrapper(params) {
let { getEvents, investigationId, isNewButtonEnabled } = params;
let { isSortingLatestEventsFirst, logbookContext } = params;
let { onPageClicked, onRefreshEventListMenuItemClicked } = params;
let { setView, view, onSortingButtonClicked, searchEvents } = params;
let { filterEvents, onSortingButtonClicked, searchEvents } = params;
let { selectionFilter, sessionId, setNewEventVisibility } = params;
activePage = activePage || 1;
......@@ -120,8 +119,7 @@ function getShallowWrapper(params) {