Commit 3a9cffb8 authored by Alejandro De Maria Antolinos's avatar Alejandro De Maria Antolinos
Browse files

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

Milestone logbook search settings

Closes #130, #132, #137, #138, #139, #140, #147, #155, #177, #179, #186, #192, #197, #198, #210, #215, #218, #221, #222, #225, #229, #232, #236, #242, #243, #244, #246, #254, #256, #265, #266, #268, #272, #274, #275, #276, #279, #339, #347, #350, #351, #377, #387, #390, #400, #401, #413, #414, #415, #417, #418, #421, #428, #430, #463, #469, #481, #493, #492, and #489

See merge request !524
parents 22f8ec62 5bf42452
Pipeline #48308 passed with stages
in 13 minutes and 16 seconds
This diff is collapsed.
......@@ -37,6 +37,7 @@ import {
CLOSED_DATA_PATH,
MY_DATA_PATH,
OPEN_DATA_PATH,
BEAMLINE_PATH,
} from './constants/routePaths';
function App() {
......@@ -133,7 +134,11 @@ function App() {
<Route exact path="/public/:prefix/:suffix" component={DOIPage} />
<Route exact path={CLOSED_DATA_PATH} component={ClosedDataPage} />
<Route exact path="/beamline/:name" component={BeamlineDataPage} />
<Route
exact
path={`${BEAMLINE_PATH}:name`}
component={BeamlineDataPage}
/>
{isSampleTrackingEnabled && (
<Route exact path="/parcels" component={MyParcelsPage} />
......
import { SET_LOGBOOK_CONTEXT } from '../constants/actionTypes';
import {
SET_CATEGORY_TYPE,
UNSET_CATEGORY_TYPE,
SET_LOGBOOK_CONTEXT,
SET_AUTOMATIC_COLLAPSING,
SET_AUTOMATIC_REFRESH,
SET_IS_SORTING_LATESTS_EVENTS_FIRST,
} from '../constants/actionTypes';
/**
* Set the logbook context ie the context in which the logbook is used. Here the context corresponds to :
......@@ -13,3 +20,58 @@ 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,
};
}
/**
* Unset 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,
};
}
/**
* Set the automatic collapsing events
* @param {*} automaticCollapsing automaticCollapsing value
*/
export function setAutomaticCollapsing(automaticCollapsing) {
return {
type: SET_AUTOMATIC_COLLAPSING,
automaticCollapsing,
};
}
/**
* Set the automatic refresh of events
* @param {*} automaticRefresh automaticRefresh value
*/
export function setAutomaticRefresh(automaticRefresh) {
return {
type: SET_AUTOMATIC_REFRESH,
automaticRefresh,
};
}
/**
* Set the sorting order
* @param {*} isSortingLatestEventsFirst isSortingLatestEventsFirst value
*/
export function setIsSortingLatestEventsFirst(isSortingLatestEventsFirst) {
return {
type: SET_IS_SORTING_LATESTS_EVENTS_FIRST,
isSortingLatestEventsFirst,
};
}
......@@ -29,6 +29,7 @@ export function doSignIn(plugin, username, password) {
name,
fullName,
isAdministrator,
isInstrumentScientist,
lifeTimeMinutes,
} = data;
dispatch({
......@@ -37,6 +38,7 @@ export function doSignIn(plugin, username, password) {
name,
fullName,
isAdministrator,
isInstrumentScientist,
lifeTimeMinutes,
});
})
......@@ -65,6 +67,7 @@ export function doSilentRefreshFromSSO() {
name,
fullName,
isAdministrator,
isInstrumentScientist,
lifeTimeMinutes,
} = data;
dispatch({
......@@ -73,6 +76,7 @@ export function doSilentRefreshFromSSO() {
name,
fullName,
isAdministrator,
isInstrumentScientist,
lifeTimeMinutes,
});
})
......
import ICATPLUS from '../../config/icatPlus';
/**
* Get URL needed to retrieve events for a given investigation
* @param {String} sessionId the session identifier
* @param {String} investigationId the session identifier
* @return {String} the URL to get the requested events. Null if investigationId or sessionId is missing.
*/
export function getEventsByInvestigationId(sessionId, investigationId) {
return `${ICATPLUS.server}/logbook/${sessionId}/investigation/id/${investigationId}/event/query`;
}
/**
* Get URL needed to retrieve the event count for a given investigation
* @param {string} sessionId the session identifier
* @param {String} investigationId the investigation identifier
* @return {String} the URL to get the requested event count
*/
export function getEventCountByInvestigationId(sessionId, investigationId) {
return `${ICATPLUS.server}/logbook/${sessionId}/investigation/id/${investigationId}/event/count`;
}
/**
* Get URL used to create a new event for a given investigation on ICAT+
* @param {*} investigationId investigation indentifier
* @param {String} sessionId session identifier
* @return {String} URL to get the requested events
*/
export function createEvent(sessionId, investigationId) {
return `${ICATPLUS.server}/logbook/${sessionId}/investigation/id/${investigationId}/event/create`;
}
/**
* Get URL used to update an event on a given investigation on ICAT+
* @param {String} sessionId the session identifier
* @param {String} investigationId the investigation indentifier
* @return {String} the URL to get the requested events
*/
export function updateEvent(sessionId, investigationId) {
return `${ICATPLUS.server}/logbook/${sessionId}/investigation/id/${investigationId}/event/update`;
}
/**
* Get URL used to download a PDF file for a given investigation from the logbook
* @param {string} sessionId session identifier
* @param {*} investigationId investigation identifier
* @param {object} selectionFilter selection filter used to retrieve part of the logbook. This is URI encoded and passed as query string
*/
export function getPDF(sessionId, investigationId, selectionFilter) {
return `${ICATPLUS.server}/logbook/${sessionId}/investigation/id/${investigationId}/event/pdf?find=&sort=&skip=&limit=`
.replace('find=', () => {
return `find=${
selectionFilter && selectionFilter.find
? JSON.stringify(selectionFilter.find)
: ''
}`;
})
.replace('&sort=', () => {
return `&sort=${
selectionFilter && selectionFilter.sort
? JSON.stringify(selectionFilter.sort)
: ''
}`;
})
.replace('&skip=', () => {
return `&skip=${
selectionFilter && selectionFilter.skip !== undefined
? JSON.stringify(selectionFilter.skip)
: ''
}`;
})
.replace('&limit=', () => {
return `&limit=${
selectionFilter && selectionFilter.limit !== undefined
? JSON.stringify(selectionFilter.limit)
: ''
}`;
});
export function getEventURL(
sessionId,
investigationId,
skip,
limit,
sortOrder,
sortBy,
types,
format,
search
) {
const params = new URLSearchParams();
params.set('investigationId', investigationId);
if (limit) params.set('limit', limit);
if (sortBy) params.set('sortBy', sortBy);
if (sortOrder) params.set('sortOrder', sortOrder);
if (types) params.set('types', types);
if (skip) params.set('skip', skip);
if (format) params.set('format', format);
if (search) params.set('search', search);
return `${ICATPLUS.server}/logbook/${sessionId}/event?${params.toString()}`;
}
/** Get the tags associated to a given investigation
......
......@@ -27,7 +27,9 @@ function BreadCrumbs(props) {
let badges = '';
if (item.badges) {
badges = item.badges.map((badge) => (
<>{badge && <Label className={styles.badge}>{badge}</Label>}</>
<span key={badge}>
{badge && <Label className={styles.badge}>{badge}</Label>}
</span>
));
}
return (
......
......@@ -107,7 +107,7 @@ class EventVersionItem extends React.Component {
function buildTheDisplay(event, type, mostRecent) {
return (
<Well bsSize="small" style={{ marginBottom: 5, cursor: 'pointer' }}>
<b> {event.username} </b>
<b> {event.fullName} </b>
{type === 'creation' ? 'created on ' : ''}
{type === 'edition' ? 'edited on ' : ''}
{moment(event.creationDate).format('MMMM DD HH:mm')}
......
import React from 'react';
import React, { useState } from 'react';
import moment from 'moment';
import PropTypes from 'prop-types';
import { useHistory } from 'react-router';
import { useQuery } from '../../../helpers/hooks';
import { Button, Glyphicon, Label } from 'react-bootstrap';
import {
getOriginalEvent,
getPreviousVersionNumber,
} from '../../../helpers/eventHelpers';
import { getOriginalEvent } from '../../../helpers/eventHelpers';
import TagListInLine from '../Tag/TagListInLine';
import styles from './EventList.module.css';
import EventTextBox from './EventTextBox';
......@@ -13,47 +11,51 @@ import EventTextBox from './EventTextBox';
/** React component which renders an event. Here 'event can be the classical event as found in the logbook but
* could also be a list of event corresponding to a collapsed line containing several events
*/
class Event extends React.Component {
constructor(props) {
super(props);
this.state = {
collapsed: true,
};
this.handleClick = this.handleClick.bind(this);
}
function Event(props) {
let events = [props.event];
const { isReleased, search } = props;
const history = useHistory();
const query = useQuery();
render() {
let events = [this.props.event];
const [collapsed, setCollapsed] = useState(true);
if (this.props.event.events && !this.state.collapsed) {
events = events.concat(this.props.event.events);
if (props.event.events && !collapsed) {
events = events.concat(props.event.events);
}
const getButtonIcon = (event) => {
if (this.props.logbookContext.isReleased) {
const getButtonIcon = () => {
if (isReleased) {
return <Glyphicon glyph="eye-open" />;
}
if (getPreviousVersionNumber(event) === 0) {
return <Glyphicon glyph="pencil" style={{ width: 10 }} />;
}
return <Glyphicon glyph="pencil" style={{ width: 10 }} />;
};
const getTimeComponent = (event) => {
const page = event.meta.search
? event.meta.search.page
: event.meta.page.currentPage;
const eventCreationDate = moment(getOriginalEvent(event).creationDate);
return (
<a
id={event._id}
href={`events?page=${event.meta.page.currentPage}#${event._id}`}
href={`events?page=${page}#${event._id}`}
style={{ fontWeight: 'bold' }}
>
{moment(getOriginalEvent(event).creationDate).format(
moment.HTML5_FMT.TIME_SECONDS
className="text-muted"
title={eventCreationDate.format(
moment.HTML5_FMT.DATETIME_LOCAL_SECONDS
)}
>
{eventCreationDate.format(moment.HTML5_FMT.TIME_SECONDS)}
</a>
);
};
const onHandleClick = () => {
setCollapsed(!collapsed);
};
return events.map((event, index) => (
<tr key={index} style={{ backgroundColor: '#f0f0f6' }}>
<tr id={event._id} key={index} style={{ backgroundColor: '#f0f0f6' }}>
<td
style={{ width: 16 }}
className={styles.borderTopSeparatorBetweenEvents}
......@@ -62,7 +64,10 @@ class Event extends React.Component {
bsStyle="default"
bsSize="small"
style={{ width: 25, position: 'static', padding: 0 }}
onClick={() => this.props.onEventClicked(event)}
onClick={() => {
query.set('edit', event._id);
history.push({ search: query.toString() });
}}
>
{getButtonIcon(event)}
</Button>
......@@ -83,16 +88,16 @@ class Event extends React.Component {
}}
>
<div style={{ marginLeft: 5, backgroundColor: 'white' }}>
<EventTextBox event={event} />
<EventTextBox event={event} search={search} />
{event.events && this.state.collapsed && (
{event.events && collapsed && (
<Label
style={{
color: 'blue',
backgroundColor: '#f8f8f8',
cursor: 'pointer',
}}
onClick={this.handleClick}
onClick={onHandleClick}
>
.... {event.events.length} command lines more
</Label>
......@@ -107,30 +112,6 @@ class Event extends React.Component {
</td>
</tr>
));
}
handleClick() {
this.setState({ collapsed: !this.state.collapsed });
}
getUncollapsedEvents() {
return (
<tbody>
{this.props.event.events.map((event) =>
this.getEventContentBody(event)
)}
</tbody>
);
}
}
Event.protypes = {
/** A classical event or a structure representing a collapsed line containing several similar events */
event: PropTypes.object,
/** Context in which the logbook is run */
logbookContext: PropTypes.object,
/** Callback function triggered which the user clicks a link to edit/consult the detailed event */
onEventClicked: PropTypes.func,
};
export default Event;
......@@ -16,14 +16,18 @@ function collapse(items) {
const collapsed = [];
for (let i = 0; i < items.length; i++) {
const event = items[i];
if (event.category) {
if (event.category && !isEventSearchedResult(event)) {
if (
event.category.toLowerCase() === EVENT_CATEGORY_COMMANDLINE &&
!event.previousVersionEvent
) {
const lastEvent = collapsed[collapsed.length - 1];
if (lastEvent && lastEvent.category) {
if (lastEvent.category.toLowerCase() === EVENT_CATEGORY_COMMANDLINE) {
if (
lastEvent.category.toLowerCase() === EVENT_CATEGORY_COMMANDLINE &&
!isEventSearchedResult(lastEvent)
) {
if (!lastEvent.events) {
lastEvent.events = [event];
} else {
......@@ -41,8 +45,14 @@ function collapse(items) {
return collapsed;
}
function isEventSearchedResult(event) {
const { hash } = window.location;
const id = hash ? hash.replace('#', '') : undefined;
return id && id === event._id;
}
/** Returns the list of items to be displayed in the table: events + days */
function getItems(events) {
function getItems(events, automaticCollapsing) {
const eventsCopy = cloneDeep(events);
const items = [];
let lastDate = null; // format DDMMYYYY
......@@ -69,14 +79,14 @@ function getItems(events) {
if (items.length > 0) {
items[items.length - 1].shadowBottomBorder = true;
}
return collapse(items);
return automaticCollapsing ? collapse(items) : items;
}
/**
* The list of the all events
*/
function EventList(props) {
const { events } = props;
const { events, isReleased, search, automaticCollapsing } = props;
if (!events) {
return null;
......@@ -86,11 +96,14 @@ function EventList(props) {
return <UserMessage type="info" message="No log found." />;
}
const collapseEvents =
search && search.length > 0 ? false : automaticCollapsing;
return (
<>
<Table responsive style={{ border: 0 }}>
<tbody>
{getItems(events).map((event, index) => {
{getItems(events, collapseEvents).map((event, index) => {
if (event.type === 'date') {
return (
<tr
......@@ -119,9 +132,10 @@ function EventList(props) {
return (
<Event
search={search}
key={index}
event={event}
logbookContext={props.logbookContext}
isReleased={isReleased}
onEventClicked={props.onEventClicked}
/>
);
......
......@@ -22,7 +22,7 @@ import LazyLoadedText from './LazyLoadedText';
* For notifications, the box could render 2 texts: the original text and the last comment if it exists
* */
function EventTextBox(props) {
const { event } = props;
const { event, search } = props;
const htmlText = getText(event.content, 'html');
let text = convertImagesToThumbnails(
......@@ -42,7 +42,7 @@ function EventTextBox(props) {
if (event.type === ANNOTATION) {
return (
<div className={getTextColor(EVENT_CATEGORY_COMMENT)}>
<LazyLoadedText text={text} />
<LazyLoadedText text={text} search={search} />
</div>
);
}
......@@ -51,14 +51,8 @@ function EventTextBox(props) {
if (!event.previousVersionEvent) {
return (
<div className={getTextColor(event.category)}>
<pre
className={
event.category.toLowerCase() === EVENT_CATEGORY_COMMANDLINE
? undefined
: 'whitePre'
}
>
<LazyLoadedText text={text} />
<pre className="whitePre">
<LazyLoadedText text={text} search={search} />
</pre>
</div>
);
......@@ -79,11 +73,11 @@ function EventTextBox(props) {
: 'whitePre'
}
>
<LazyLoadedText text={originalText} />
<LazyLoadedText text={originalText} search={search} />
</pre>
</div>
<div className={getTextColor(EVENT_CATEGORY_COMMENT)}>
<LazyLoadedText text={text} />
<LazyLoadedText text={text} search={search} />
</div>
</>
);
......@@ -105,13 +99,13 @@ function getTextColor(category) {
}
if (category.toLowerCase() === EVENT_CATEGORY_INFO) {
return '';
return 'primary';
}
if (category.toLowerCase() === EVENT_CATEGORY_DEBUG) {
return 'text-muted';
}
return 'text-primary';
return '';
}
EventTextBox.propTypes = {
......
......@@ -6,20 +6,41 @@ import LazyLoad from 'react-lazyload';
* least an `img` tag. When there are no images, the text is rendered as such.
*/
function LazyLoadedText(props) {
const { text } = props;
const { search } = props;
let { text } = props;
const withImages = text.indexOf('img') !== -1;
/* If search is enabled then the search is highlighted but not in the images as the base64 code can get corrupted */
if (search && !withImages) {
text = text.replace(
new RegExp(search, 'g'),
`<span style="background-color:yellow;">${search}</span>`
);
}
if (!text) {
return <div />;
}