# containers/Event/EventContainer.js

## Maintainability

68.97

## Lines of code

475

## Difficulty

61.29

## Estimated Errors

3.85

## Function weight

### By Complexity

### By SLOC

import React from 'react'; import axios from 'axios'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { getEventsByInvestigationId, getEventCountByInvestigationId, createEvent, updateEvent } from '../../api/icat/icatPlus.js'; import _ from 'lodash'; import EventList from '../../components/Event/EventList'; import EventActionBar from '../../components/Event/EventActionBar.js'; import NewEvent from '../../components/Event/NewEvent.js'; import { NEW_EVENT_INVISIBLE, NEW_EVENT_VISIBLE, DOC_VIEW, LIST_VIEW, ERROR_MESSAGE_TYPE } from '../../constants/EventTypes.js'; import EventPager from '../../components/Event/EventPager.js'; import { convertPageToPageEventIndexes, isRangeAvailableOnTheClient } from '../../helpers/PaginationHelper.js'; import { GUI_CONFIG } from '../../config/gui.config.js'; import Loading from '../../components/Loading.js'; import UserMessage from '../../components/UserMessage.js'; import { getSelectionFiltersBySearchCriteria, getSelectionFiltersForMongoQuery } from './SelectionFilterHelper.js'; import { clearAvailableTagAction } from '../../actions/logbook.js'; /** * 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. */ class EventContainer extends React.Component { constructor(props) { super(props); this.state = { activePage: 1, foundEventCount: 0, // count of events found by the server downloadedEvents: [], //events already downloaded errorMessage: '', eventReceptionStatus: 'idle', //idle, fetched, countFetched, allFetched; Used to handle asynchronous data retrieval from the server eventCountReceptionStatus: 'idle', //idle, fetched, countFetched, allFetched; Used to handle asynchronous data retrieval from the server isNewEventVisible: false, pageEvents: [], // events currently displayed on the page selectedEventId: 0, sortingFilter: GUI_CONFIG().DEFAULT_SORTING_FILTER, userSearchCriteria: [], view: LIST_VIEW }; this.createEvent = this.createEvent.bind(this); this.downloadEvents = this.downloadEvents.bind(this); this.isAppProperlyConfigured = this.isAppProperlyConfigured.bind(this); this.onEventsReceptionFailure = this.onEventsReceptionFailure.bind(this); this.onEventUpdated = this.onEventUpdated.bind(this); this.onNewEventUploaded = this.onNewEventUploaded.bind(this); this.onPageClicked = this.onPageClicked.bind(this); this.reverseEventsSortingByCreationDate = this.reverseEventsSortingByCreationDate.bind(this); this.searchEvents = this.searchEvents.bind(this); this.setNewEventVisibility = this.setNewEventVisibility.bind(this); this.setView = this.setView.bind(this); this.updateEvent = this.updateEvent.bind(this); } render() { const { investigationId, user } = this.props; const selectionFilter = getSelectionFiltersForMongoQuery(this.state.userSearchCriteria, this.state.sortingFilter, this.state.view); function isDataFetched(parentThis) { let _this = parentThis; if (_this.state.eventReceptionStatus === 'fetched' && _this.state.eventCountReceptionStatus === 'fetched') { return true; } return false; } if (this.isAppProperlyConfigured()) { return ( <div style={{ marginBottom: '40px' }}> <UserMessage type={ERROR_MESSAGE_TYPE} message={this.state.errorMessage} /> <EventActionBar availableTags = {this.props.availableTags} investigationId={investigationId} isNewEventVisible={this.state.isNewEventVisible} numberOfMatchingEventsFound={this.state.foundEventCount} reverseEventsSortingByCreationDate={this.reverseEventsSortingByCreationDate} searchEvents={this.searchEvents} sessionId={user.sessionId} setNewEventVisibility={this.setNewEventVisibility} setView={this.setView} sortingFilter={this.state.sortingFilter} selectionFilter={selectionFilter} view={this.state.view} /> <NewEvent createEvent={this.createEvent} isVisible={this.state.isNewEventVisible} investigationId={investigationId} onNewEventUploaded={this.onNewEventUploaded} setNewEventVisibility={this.setNewEventVisibility} user={user} /> {(!isDataFetched(this)) ? <Loading message='Loading logbook'></Loading> : <div> <EventPager activePage={this.state.activePage} eventCount={this.state.foundEventCount} isFloatingRight={true} onPageClicked={this.onPageClicked} /> <EventList events={this.state.pageEvents} investigationId={investigationId} onEventUpdated={this.onEventUpdated} reloadEvents={this.downloadEvents} reverseEventsSortingByCreationDate={this.reverseEventsSortingByCreationDate} selectedEventId={this.state.selectedEventId} updateEvent={this.updateEvent} user={user} view={this.state.view} /> <EventPager activePage={this.state.activePage} eventCount={this.state.foundEventCount} onPageClicked={this.onPageClicked} /> </div>} </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 */ componentWillMount() { this.downloadEvents(0, getSelectionFiltersBySearchCriteria(this.state.userSearchCriteria, this.state.view), true); } componentWillUnmount() { this.props.clearAvailableTags(); } /** 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.downloadEvents(0, getSelectionFiltersBySearchCriteria(searchCriteria, this.state.view), true); this.setState({ userSearchCriteria: searchCriteria }); } /** * Callback function which refreshes the event list because the events the user want to see have not been downloaded yet * @param {integer} offset the offset * @param {object} sortingFilter the sorting filter to use , optional * @param {Object} selectionFilter optional; selection filter in mongodb format * @param {bool} isEventCountRequestNeeded whether the event count request is needed or not */ downloadEvents(offset, selectionFilter, isEventCountRequestNeeded, sortingFilter) { const { investigationId, user } = this.props; sortingFilter = sortingFilter || GUI_CONFIG().DEFAULT_SORTING_FILTER; if (!selectionFilter) { // there is not selectionfilter as parameter when the user has clicked on a page selectionFilter = getSelectionFiltersBySearchCriteria(this.state.userSearchCriteria, this.state.view); }; this.getEventsByInvestigationId(investigationId, user.sessionId, GUI_CONFIG().EVENTS_PER_DOWNLOAD, offset, selectionFilter, sortingFilter, this.onEventsReceptionFailure); this.setState({ eventReceptionStatus: 'fetching', isNewEventVisible: false }); if (isEventCountRequestNeeded) { this.getEventCountByInvestigationId(investigationId, user.sessionId, selectionFilter, this.onEventsReceptionFailure); this.setState({ eventCountReceptionStatus: 'fetching' }); } } /** * Callback function triggered when event data retreval failed * @param {*} error */ onEventsReceptionFailure(error) { this.setState({ errorMessage: 'An error occured while retrieving the events', events: [], eventCount: 0, eventReceptionStatus: 'idle', eventCountReceptionStatus: 'idle', }); } /** * Change the current view to the requested view * @param {string} view the requested view */ setView(view) { if (view && view !== this.state.view) { this.downloadEvents(0, getSelectionFiltersBySearchCriteria(this.state.userSearchCriteria, view), true); } this.setState({ view: view }); } /** * 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({ isNewEventVisible: true }); } else if (visibility === NEW_EVENT_INVISIBLE) { this.setState({ isNewEventVisible: false }); } }; } /** * Request events associated to the given investigationId and selectionFilter * @param {integer} investigationId the investigation indentifier * @param {String} sessionId the session identifier * @param {integer} limit the total number of events requested * @param {integer} offset the offset value of events from all events found by the DB (used for pagination) * @param {Object} selectionFilter the selection filter (DB syntax). Example: {"$and":[{"$or":[{"type":"annotation"},{"type":"notification"}]}]} * @param {Object} sortingFilter the sorting filter (DB synthax). Example: {"createdAt":-1} * @param {requestCallback} onError the callback function executed when an error occured during the request. */ getEventsByInvestigationId(investigationId, sessionId, limit, offset, selectionFilter, sortingFilter, onError) { if (investigationId && sessionId) { axios({ method: 'post', url: getEventsByInvestigationId(sessionId, investigationId), data: { find: selectionFilter, sort: sortingFilter, limit: limit, skip: offset } }) .then(data => { this.setState({ downloadedEvents: data.data, pageEvents: _.slice(data.data, 0, GUI_CONFIG().EVENTS_PER_PAGE), downloadedEventIndexes: { start: offset, end: offset + data.data.length - 1 }, selectionFilter: selectionFilter, sortingFilter: sortingFilter, eventReceptionStatus: 'fetched', }); }) .catch((error) => { console.log('[ERROR] An error occured while retrieving the event list.'); onError(error); }); } else { onError('[ERROR] Missing query parameters in the request handled by getEventsByInvestigationId()'); } } /** * Request total number of events associated to the given investigationId * @param {string} investigationId the investigation id * @param {string} sessionId the session id * @param {object} selectionFilter the selection filter * @param {func} onError the callback function executed when an error occured during the request. */ getEventCountByInvestigationId(investigationId, sessionId, selectionFilter, onError) { if (investigationId && sessionId) { axios({ method: 'post', url: getEventCountByInvestigationId(sessionId, investigationId), data: { find: selectionFilter } }) .then(data => { this.setState({ foundEventCount: parseInt(data.data, 10), eventCountReceptionStatus: 'fetched' }); }) .catch((error) => { console.log('[ERROR] An error occured while retrieving the event count.'); onError(error); }); } else { onError('[ERROR] Missing query parameters in the request handled by getEventCountByInvestigationId()'); } } /** * Callback function triggered when a new event has been successfully uploaded */ onNewEventUploaded() { this.setState({ isNewEventVisible: false, activePage: 1, }); this.downloadEvents(0, getSelectionFiltersBySearchCriteria(this.state.userSearchCriteria, this.state.view), true); } /** * Callback function triggered when the user clicks on a page link * @param {integer} page the page the user has clicked */ onPageClicked(page) { if (page) { let pageEventIndexes = convertPageToPageEventIndexes(page, GUI_CONFIG().EVENTS_PER_PAGE, this.state.foundEventCount); 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.downloadEvents(pageEventIndexes.start, false, false, this.state.sortingFilter); } this.setState({ activePage: page, selectedEventId: 0 }); } } /** * Callback function which reverses the sorting of events by creation date * @param {string} selectedEventId the id of the selected event */ reverseEventsSortingByCreationDate(selectedEventId) { let newSortingFilter = {}; let selectionFilter = getSelectionFiltersBySearchCriteria(this.state.userSearchCriteria, this.state.view); /* temporary fix , in the future we will use creationDate instead of createdAt*/ newSortingFilter.createdAt = this.state.sortingFilter.createdAt * (-1); if (selectedEventId !== undefined) { // An event is selected, sort and show the page where the selected event is let indexOfEventOnThePage = _.findIndex(this.state.pageEvents, (event) => { return event._id === selectedEventId; }); if (indexOfEventOnThePage !== -1) { let indexOfEventOnAllEventsFound = indexOfEventOnThePage + (this.state.activePage - 1) * GUI_CONFIG().EVENTS_PER_PAGE; let rankInFutureList = this.state.foundEventCount - indexOfEventOnAllEventsFound; let newPage = Math.ceil(rankInFutureList / GUI_CONFIG().EVENTS_PER_PAGE); let offset = (newPage - 1) * GUI_CONFIG().EVENTS_PER_PAGE; this.downloadEvents(offset, selectionFilter, false, newSortingFilter); this.setState({ activePage: newPage, selectedEventId: selectedEventId }); return; } } this.downloadEvents(0, selectionFilter, false, newSortingFilter); this.setState({ activePage: 1, selectedEventId: 0 }); } /** * Checks whether the app is properly configured for wall-eLog * @returns {boolean} true when the app is properly configured */ isAppProperlyConfigured() { if (!GUI_CONFIG()) { console.log('[ERROR] [CONFIG] File gui.config.js is missing'); return false; } if (!GUI_CONFIG().DEFAULT_SORTING_FILTER) { console.log('[ERROR] [CONFIG] DEFAULT_SORTING_FILTER object is missing'); return false; } /* temporary workaround. We will need to use creationDate in fact in the future */ if (!GUI_CONFIG().DEFAULT_SORTING_FILTER.createdAt) { console.log('[ERROR] [CONFIG] DEFAULT_SORTING_FILTER.creationDate value is missing'); return false; } if (!GUI_CONFIG().EVENTS_PER_PAGE) { console.log('[ERROR] [CONFIG] EVENTS_PER_PAGE value is missing'); return false; } return true; } /** Callback function triggered when an event as been updated. * @param {Object} event the updated event as provided by the server */ onEventUpdated(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 }); } /** * Create the event on ICAT+ server * @param {*} newEvent event object to be created * @param {String} sessionId session Id required for HTTP request authentication * @param {*} investigationId investigation Id to which the event will be added to. * @param {*} onSuccess callback function executed after the asymchronous event creation process succeeded. */ createEvent(newEvent, sessionId, investigationId, onSuccess) { axios({ method: 'post', url: createEvent(sessionId, investigationId), data: newEvent, }) .then((response) => { console.log('The new event was created successfully on ICAT+ server.'); localStorage.removeItem('plainText'); localStorage.removeItem('HTMLText'); onSuccess(); }) .catch((error) => { console.log(error); }); } /** * Update the event on ICAT+ server * @param {*} newEvent event object to be created * @param {String} sessionId session Id required for HTTP request authentication * @param {*} investigationId investigation Id to which the event will be added to. * @param {*} onSuccess callback function executed after the asymchronous event creation process succeeded. */ updateEvent(newEvent, sessionId, investigationId, onSuccess) { axios({ method: 'put', url: updateEvent(sessionId, investigationId), data: { event: newEvent }, }) .then((response) => { console.log('the event was modified successfully on ICAT+ server.'); localStorage.removeItem('plainText'); localStorage.removeItem('HTMLText'); onSuccess(response.data); }) .catch((error) => { console.log(error); }); } } function mapStateToProps(state) { return { user: state.user, availableTags: state.logbook.availableTags }; } /** * Redux related function which is used to map local function with redux dispatch() * @param {*} dispatch redux dispatch function */ function mapDispatchToProps(dispatch) { return { clearAvailableTags: () => dispatch(clearAvailableTagAction()) }; } EventContainer.propTypes = { /** the investigationId */ investigationId: PropTypes.string.isRequired }; export default connect(mapStateToProps, mapDispatchToProps)(EventContainer);