ES6 Plato on Github
Report Home
Summary Display
containers/Logbook/LogbookContainer.js
Maintainability
71.12
Lines of code
567
Difficulty
83.09
Estimated Errors
6.65
Function weight
By Complexity
By SLOC
import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import _ from 'lodash'; import EventList from '../../components/Logbook/List/EventList'; import EventListMenu from '../../components/Logbook/Menu/EventListMenu.js'; import { NEW_EVENT_INVISIBLE, NEW_EVENT_VISIBLE, LIST_VIEW, ERROR_MESSAGE_TYPE, INFO_MESSAGE_TYPE, LOGBOOK_CONTEXT_NAME_PROPOSAL, PUBLIC_EVENT_DETAILS_VISIBLE, PUBLIC_EVENT_DETAILS_INVISIBLE } from '../../constants/EventTypes.js'; import { convertPageToPageEventIndexes, isRangeAvailableOnTheClient } from '../../helpers/PaginationHelper.js'; import UI from '../../config/ui/config'; import Loading from '../../components/Loading.js'; import UserMessage from '../../components/UserMessage.js'; import { getSelectionFiltersBySearchCriteria, getSelectionFiltersForMongoQuery } from './SelectionFilterHelper.js'; import { setLogbookContextAction } from '../../actions/logbook.js'; import OverlayBox from '../../components/Logbook/OverlayBox.js'; import NewOrEditEventPanel from '../../components/Logbook/NewOrEditEventPanel.js'; import "../../components/Logbook/event.css"; import PeriodicRefresher from './PeriodicRefresher.js'; import PublicEventPanel from '../../components/Logbook/PublicEventPanel'; import { parseHTTPError, getSortingOrder } from '../../helpers/EventHelpers'; import axios from 'axios'; import { getEventsByInvestigationId, getEventCountByInvestigationId, updateEvent as updateEventURL, createEvent as createEventURL } from '../../api/icat/icatPlus'; /** * This class represents the event container component. It's role is to retrieve events from the server asynchronuously. * Also, it is connected to redux and retrieves user data and sessionId. */ export class LogbookContainer extends React.Component { constructor(props) { super(props); this.state = { activePage: 1, eventCountBySelectionFilter: 0, // count of events for the current selection filter found in the logbook since last refresh allEventsCountAtLastRefresh: 0, // count of all events found in the logbook since last refresh downloadedEvents: [], //events already downloaded errorMessage: '', fetchingEventList: true, fetchedEventList: false, fetchingEventCount: true, fetchedEventCount: false, event: null, // event being edited isNewEventPanelVisible: false, isShowingEditEventUI: false, isShowingPublicEventUI: false, autorefresh: { isEnabled: UI.logbook.AUTOREFRESH_EVENTLIST, isFetchingEvents: false, isLockingRefresh: false // prevent EventList refresh even if isEnabled is true }, pageEvents: [], // events currently displayed on the page sortingFilter: { [UI.logbook.SORT_EVENTS_BY]: UI.logbook.SORTING_ORDER }, // current sorting flter searchCriteria: [], view: LIST_VIEW, }; this.createEvent = this.createEvent.bind(this); this.getAllEventsAndRefresh = this.getAllEventsAndRefresh.bind(this); this.getEventsBySelectionFilter = this.getEventsBySelectionFilter.bind(this); this.getEventCountBySelectionFilter = this.getEventCountBySelectionFilter.bind(this); this.isAppProperlyConfigured = this.isAppProperlyConfigured.bind(this); this.onCancelButtonClicked = this.onCancelButtonClicked.bind(this); this.onCloseButtonClicked = this.onCloseButtonClicked.bind(this); this.onEventCountReceptionFailure = this.onEventCountReceptionFailure.bind(this); this.onEventClicked = this.onEventClicked.bind(this); this.onNewEventCountReceived = this.onNewEventCountReceived.bind(this); this.onPageClicked = this.onPageClicked.bind(this); this.onSaveButtonClicked = this.onSaveButtonClicked.bind(this); this.flipEventsSortingOrder = this.flipEventsSortingOrder.bind(this); this.searchEvents = this.searchEvents.bind(this); this.setNewEventVisibility = this.setNewEventVisibility.bind(this); this.setView = this.setView.bind(this); } render() { const { investigationId, user } = this.props; if (this.isAppProperlyConfigured()) { return ( <div style={{ marginBottom: '40px' }}> <OverlayBox open={this.state.errorMessage ? true : false} onClose={() => { this.setState({ errorMessage: null }) }} classNames={{ modal: 'userMessageModalClass', closeButton: 'userMessageCloseButton' }}> {this.state.errorMessage ? <UserMessage type={ERROR_MESSAGE_TYPE} message={this.state.errorMessage} /> : null} </OverlayBox> <PeriodicRefresher initialValue={0} intervalInMilliSeconds={UI.logbook.AUTOREFRESH_DELAY} isEnabled={true} onCallbackSuccess={this.onNewEventCountReceived} periodicCallback={(onSuccess, onFailure) => this.getEventCountBySelectionFilter(getSelectionFiltersBySearchCriteria(this.state.searchCriteria, LIST_VIEW)).then(eventCount => onSuccess(eventCount), error => onFailure(error))} > <EventListMenu activePage={this.state.activePage} autorefreshEventList={this.state.autorefresh.isEnabled} //availableTags={this.props.availableTags} eventCountBySelectionFilter={this.state.eventCountBySelectionFilter} eventCountSinceLastRefresh={this.state.allEventsCountAtLastRefresh} getEvents={this.getAllEventsAndRefresh} investigationId={investigationId} isNewButtonEnabled={!this.state.isNewEventPanelVisible} isSortingLatestEventsFirst={getSortingOrder(this.state.sortingFilter) === -1 ? true : false} logbookContext={this.props.logbookContext} onPageClicked={this.onPageClicked} onRefreshEventListMenuItemClicked={() => this.setState({ autorefresh: { ...this.state.autorefresh, isEnabled: !this.state.autorefresh.isEnabled } })} onSortingButtonClicked={this.flipEventsSortingOrder} searchEvents={this.searchEvents} selectionFilter={getSelectionFiltersForMongoQuery(this.state.searchCriteria, this.state.sortingFilter, this.state.view)} sessionId={user.sessionId} setNewEventVisibility={this.setNewEventVisibility} setView={this.setView} view={this.state.view} /> </PeriodicRefresher> <OverlayBox open={this.state.isNewEventPanelVisible === true || this.state.isShowingEditEventUI === true || this.state.isShowingPublicEventUI === true} classNames={{ overlay: 'newOrEditOverlayClass', modal: 'newOrEditModalClass' }}> {this.state.isNewEventPanelVisible === true ? <div style={{ height: this.getPanelHeightInPercent(), paddingBottom: this.state.isShowingEditEventUI ? '5px' : '0px' }}> <NewOrEditEventPanel investigationId={investigationId} onCancelButtonClicked={this.onCancelButtonClicked} onSaveButtonClicked={this.onSaveButtonClicked} user={user} /> </div> : null} {this.props.logbookContext.isReleased === true ? <div style={{ height: '100%', paddingBottom: '0px' }}> <PublicEventPanel event={this.state.event} onCloseButtonClicked={this.onCloseButtonClicked} /> </div> : this.state.isShowingEditEventUI === true ? <div style={{ height: this.getPanelHeightInPercent(), paddingBottom: this.state.isNewEventPanelVisible === true ? '5px' : '0px' }}> <NewOrEditEventPanel event={this.state.event} investigationId={investigationId} onCancelButtonClicked={this.onCancelButtonClicked} onSaveButtonClicked={this.onSaveButtonClicked} user={user} /> </div> : null} </OverlayBox> {this.state.fetchedEventList === false && this.state.fetchingEventList === true ? <Loading message='Loading logbook'></Loading> : null} {this.state.fetchedEventList === true ? <div> {(this.state.pageEvents.length === 0) ? <UserMessage message='The logbook is empty.' type={INFO_MESSAGE_TYPE} isTextCentered={true} /> : <EventList activePage={this.state.activePage} events={this.state.pageEvents} eventCountBySelectionFilter={this.state.eventCountBySelectionFilter} logbookContext={this.props.logbookContext} onEventClicked={this.onEventClicked} onPageClicked={this.onPageClicked} /> } </div> : null} </div > ); } else { return (<div style={{ marginBottom: '40px' }}> <UserMessage type={ERROR_MESSAGE_TYPE} message={'Sorry, the application is not properly configured for wall-eLog. Can not start. Check the console.'} /> </div> ); } } /** * Executed the first time the component is mounted */ componentDidMount() { this.getAllEventsAndRefresh(this.state.view); const isCurrentProposalReleased = () => { let _this = this; return _.find(this.props.releasedInvestigations, releasedInvestigation => String(releasedInvestigation.id) === _this.props.investigationId) != undefined ? true : false; } this.props.setLogbookContext({ name: LOGBOOK_CONTEXT_NAME_PROPOSAL, isReleased: isCurrentProposalReleased() }); } /** Get the height percentage value for the panels for new and edition of an event */ getPanelHeightInPercent() { return (this.state.isNewEventPanelVisible === true && this.state.isShowingEditEventUI === true) ? '50%' : '100%'; } /** * Callback function triggered when event count retreval failed * @param {*} error * @param {String} event counter to reset */ onEventCountReceptionFailure(error, eventCounterToReset) { this.setState({ errorMessage: parseHTTPError(error), [eventCounterToReset]: 0, fetchingEventCount: false, }); } onEventClicked(event) { if (this.state.isShowingEditEventUI === true && this.state.event != event) { this.setState({ errorMessage: 'Action refused. Please save or cancel the log which is currently being edited.' }); } if (this.props.logbookContext.isReleased === true) { this.setState({ event: event, isShowingPublicEventUI: true }); } if (this.props.logbookContext.isReleased === false) { this.setState({ event: event, isShowingEditEventUI: true }); } } /** * Function triggered periodically by the periodicRefresher * @param {object} eventCount event count received from the periodic refresher */ onNewEventCountReceived(eventCount) { if (this.state.autorefresh.isEnabled === true) { this.setState({ autorefresh: { ...this.state.autorefresh, isFetchingEvents: true } }); this.getEventsBySelectionFilter(this.state.downloadedEventIndexes.start, getSelectionFiltersBySearchCriteria(this.state.searchCriteria, this.state.view), this.state.sortingFilter) .then(events => { this.setState({ autorefresh: { ...this.state.autorefresh, isFetchingEvents: false } }); if (this.state.autorefresh.isLockingRefresh === true) { console.log('Event list not refreshed automatically because a user triggered request occured in the mean time'); } else { this.refreshEventList(events, this.state.downloadedEventIndexes.start, getSelectionFiltersBySearchCriteria(this.state.searchCriteria, this.state.view), this.state.sortingFilter) } this.setState({ autorefresh: { ...this.state.autorefresh, isLockingRefresh: false } }); }); this.getEventCountBySelectionFilter(getSelectionFiltersBySearchCriteria(this.state.searchCriteria, this.state.view)) .then(eventCount => this.setState({ eventCountBySelectionFilter: parseInt(eventCount, 10) })); } } /** * Refresh the event list * @param {*} events * @param {*} offset * @param {*} selectionFilter * @param {*} sortingFilter */ refreshEventList(events, offset, selectionFilter, sortingFilter) { this.setState({ activePage: offset === 0 ? 1 : this.state.activePage, downloadedEvents: events, downloadedEventIndexes: { start: offset, end: offset + events.length - 1 }, fetchedEventList: true, pageEvents: _.slice(events, 0, UI.logbook.EVENTS_PER_PAGE), selectionFilter: selectionFilter, sortingFilter: sortingFilter, }); } /** * Function triggered when the user clicks on a page * @param {integer} page the page the user has clicked */ onPageClicked(page) { if (page) { page = page.selected + 1; let pageEventIndexes = convertPageToPageEventIndexes(page, UI.logbook.EVENTS_PER_PAGE, this.state.eventCountBySelectionFilter); let areEventsAlreadyDownloaded = isRangeAvailableOnTheClient(pageEventIndexes, this.state.downloadedEventIndexes); if (areEventsAlreadyDownloaded) { this.setState({ pageEvents: _.slice(this.state.downloadedEvents, pageEventIndexes.start - this.state.downloadedEventIndexes.start, pageEventIndexes.end - this.state.downloadedEventIndexes.start + 1) }); } else { this.getEventsBySelectionFilter(pageEventIndexes.start, getSelectionFiltersBySearchCriteria(this.state.searchCriteria, this.state.view), this.state.sortingFilter) .then(events => { this.refreshEventList(events, pageEventIndexes.start, getSelectionFiltersBySearchCriteria(this.state.searchCriteria, this.state.view), this.state.sortingFilter) }); } this.setState({ activePage: page }); } } /** Callback function triggered when the user starts a new search * @param {Array} searchCriteria the search criteria as provided by the search component */ searchEvents(searchCriteria) { this.setState({ searchCriteria: searchCriteria }); this.getEventsBySelectionFilter(0, getSelectionFiltersBySearchCriteria(searchCriteria, this.state.view), this.state.sortingFilter) .then(events => { if (this.state.autorefresh.isFetchingEvents) { this.setState({ autorefresh: { ...this.state.autorefresh, isLockingRefresh: true } }) } this.refreshEventList(events, 0, getSelectionFiltersBySearchCriteria(searchCriteria, this.state.view), this.state.sortingFilter) }); this.getEventCountBySelectionFilter(getSelectionFiltersBySearchCriteria(searchCriteria, this.state.view)) .then(eventCount => { this.setState({ eventCountBySelectionFilter: parseInt(eventCount, 10) }) }); } /** * Function triggered when the user hits the cancel button during event creation or edition */ onCancelButtonClicked() { if (this.state.event) { // editing an event this.setState({ isShowingEditEventUI: false }); } else { // creating an event this.setState({ isNewEventPanelVisible: false }); } } /** * Set the visibility of the new event panel * @param {string} visibility the requested visibility */ setNewEventVisibility(visibility) { if (visibility) { if (visibility === NEW_EVENT_VISIBLE) { this.setState({ isNewEventPanelVisible: true }); } else if (visibility === NEW_EVENT_INVISIBLE) { this.setState({ isNewEventPanelVisible: false }); } }; } /** * Function triggered when the user clicks on the close button while view a public event */ onCloseButtonClicked() { this.setState({ isShowingPublicEventUI: false }); } /** * Change the current view to the requested view * @param {string} view the requested view */ setView(view) { if (view && view !== this.state.view) { this.getAllEventsAndRefresh(view); } this.setState({ view: view }); } /** * Callback function which reverses the sorting order of events */ flipEventsSortingOrder() { let newSortingFilter = { ...this.state.sortingFilter }; newSortingFilter[Object.keys(this.state.sortingFilter)[0]] = getSortingOrder(this.state.sortingFilter) * (-1); this.getEventsBySelectionFilter(0, getSelectionFiltersBySearchCriteria(this.state.searchCriteria, this.state.view), newSortingFilter) .then(events => { if (this.state.autorefresh.isFetchingEvents) { this.setState({ autorefresh: { ...this.state.autorefresh, isLockingRefresh: true } }) } this.refreshEventList(events, 0, getSelectionFiltersBySearchCriteria(this.state.searchCriteria, this.state.view), newSortingFilter) } ); this.getEventCountBySelectionFilter(getSelectionFiltersBySearchCriteria(this.state.searchCriteria, this.state.view)) .then(eventCount => { this.setState({ eventCountBySelectionFilter: parseInt(eventCount, 10) }) }); } /** * Get events * @param {integer} offset the offset of events * @param {Object} selectionFilter selection filter in mongodb format * @param {object} sortingFilter the sorting filter to use , optional * @param {bool} useLimit if false, the limit property is not used in the request. True by default. */ getEventsBySelectionFilter(offset, selectionFilter, sortingFilter, useLimit = true) { const { investigationId, user } = this.props; sortingFilter = sortingFilter || { [UI.logbook.SORT_EVENTS_BY]: UI.logbook.SORTING_ORDER } if (!selectionFilter) { // there is not selectionfilter as parameter when the user has clicked on a page selectionFilter = getSelectionFiltersBySearchCriteria(this.state.searchCriteria, this.state.view); }; if (investigationId && user.sessionId) { return new Promise((resolve, reject) => axios({ method: 'post', url: getEventsByInvestigationId(user.sessionId, investigationId), data: { find: selectionFilter, sort: sortingFilter, limit: useLimit === true ? UI.logbook.EVENTS_PER_DOWNLOAD : null, skip: offset } }) .then(data => { resolve(data.data); }, error => { console.log('[ERROR] An error occured while retrieving the event list.'); })) } else { //onError('[ERROR] Missing query parameters in the request handled by getEventsByInvestigationId()'); } } getAllEventsAndRefresh(view) { this.getEventsBySelectionFilter(0, getSelectionFiltersBySearchCriteria([], view), this.state.sortingFilter) .then(events => this.refreshEventList(events, 0, getSelectionFiltersBySearchCriteria([], view), this.state.sortingFilter)); this.getEventCountBySelectionFilter(getSelectionFiltersBySearchCriteria([], view)) .then(eventCount => { this.setState({ allEventsCountAtLastRefresh: parseInt(eventCount, 10), eventCountBySelectionFilter: parseInt(eventCount, 10) }) }); } /** * Get event count by selection filter * @param {object} selectionFilter the selection filter used to know which events of the logbook need to be counted */ getEventCountBySelectionFilter(selectionFilter) { const { investigationId, user } = this.props; this.setState({ fetchingEventCount: true }); if (investigationId && user.sessionId) { return new Promise((resolve, reject) => axios({ method: 'post', url: getEventCountByInvestigationId(user.sessionId, investigationId), data: { find: selectionFilter } }) .then(data => { this.setState({ fetchedEventCount: true }); resolve(data.data); }, error => { console.log('[ERROR] An error occured while retrieving the event count.'); }) ) } else { //onError('[ERROR] Missing query parameters in the request handled by getEventCountByInvestigationId()'); } } /** * Update the event on ICAT+ server * @param {*} event event object to be updated */ updateEvent(event) { return new Promise((resolve, reject) => axios({ method: 'put', url: updateEventURL(this.props.user.sessionId, this.props.investigationId), data: { event: event }, }) .then(response => { console.log('the event was modified successfully on ICAT+ server.'); resolve(response.data); }) .catch(error => { this.setState({ errorMessage: parseHTTPError(error) }); })); } /** * Create the event * @param {*} newEvent event object to be created */ createEvent(newEvent) { return new Promise((resolve, reject) => axios({ method: 'post', url: createEventURL(this.props.user.sessionId, this.props.investigationId), data: newEvent, }) .then(response => { console.log('The new event was created successfully on ICAT+ server.'); resolve(); }) .catch(error => { this.setState({ errorMessage: parseHTTPError(error) }); }) ); } /** * Function triggered when the user hits the save button to create or edit an event * @param {*} event event to be created or updated */ onSaveButtonClicked(event) { if (this.state.event) { //editing an event this.updateEvent(event) .then(updatedEvent => { let copyPageEvents = this.state.pageEvents.map(event => event); let foundIndex1 = _.findIndex(copyPageEvents, (event) => event._id === updatedEvent._id); copyPageEvents.splice(foundIndex1, 1, updatedEvent); let copyDownloadedEvents = this.state.downloadedEvents.map(event => event); let foundIndex2 = _.findIndex(copyDownloadedEvents, (event) => event._id === updatedEvent._id); copyDownloadedEvents.splice(foundIndex2, 1, updatedEvent); this.setState({ downloadedEvents: copyDownloadedEvents, pageEvents: copyPageEvents, event: null, isShowingEditEventUI: false }); }) } else { // creating an event this.createEvent(event) .then(() => { this.setState({ isNewEventPanelVisible: false, activePage: 1, }); this.getEventsBySelectionFilter(0, getSelectionFiltersBySearchCriteria(this.state.searchCriteria, this.state.view), this.state.sortingFilter) .then(events => this.refreshEventList(events, 0, getSelectionFiltersBySearchCriteria(this.state.searchCriteria, this.state.sortingFilter))); this.getEventCountBySelectionFilter(getSelectionFiltersBySearchCriteria(this.state.searchCriteria, this.state.view)) .then(eventCount => this.setState({ eventCountBySelectionFilter: parseInt(eventCount, 10) })); this.getEventCountBySelectionFilter(getSelectionFiltersBySearchCriteria([], this.state.view)) .then(eventCount => this.setState({ allEventsCountAtLastRefresh: parseInt(eventCount, 10) })); }) } } /** * Checks whether the app is properly configured for wall-eLog * @returns {boolean} true when the app is properly configured */ isAppProperlyConfigured() { if (!UI.logbook) { console.log('[ERROR] [CONFIG] logbook section is missing in File src/config/ui/config'); return false; } if (!UI.logbook.SORT_EVENTS_BY) { console.log('[ERROR] [CONFIG] SORT_EVENTS_BY object is missing'); return false; } else if (UI.logbook.SORT_EVENTS_BY !== 'creationDate' && UI.logbook.SORT_EVENTS_BY !== '_id' && UI.logbook.SORT_EVENTS_BY !== 'createdAt' && UI.logbook.SORT_EVENTS_BY !== 'updatedAt') { console.log("[ERROR] [CONFIG] SORT_EVENTS_BY value is not supported. Possible values are 'creationDate', '_id', 'createdAt' and 'updatedAt' "); return false; } if (!UI.logbook.SORTING_ORDER) { console.log('[ERROR] [CONFIG] SORTING_ORDER object is missing'); return false; } if (!UI.logbook.EVENTS_PER_PAGE) { console.log('[ERROR] [CONFIG] EVENTS_PER_PAGE value is missing'); return false; } return true; } } function mapStateToProps(state) { return { releasedInvestigations: state.releasedInvestigations.data, logbookContext: state.logbook.context, user: state.user }; } /** * Redux related function which is used to map local function with redux dispatch() * @param {*} dispatch redux dispatch function */ function mapDispatchToProps(dispatch) { return { setLogbookContext: (context) => dispatch(setLogbookContextAction(context)) }; } LogbookContainer.propTypes = { /** the investigationId */ investigationId: PropTypes.string.isRequired }; export default connect(mapStateToProps, mapDispatchToProps)(LogbookContainer);