Commit 79c965f4 authored by Alejandro De Maria Antolinos's avatar Alejandro De Maria Antolinos

Merge branch 'refacto' into 'master'

Refactor fetching lists of investigations

Closes #405

See merge request !443
parents a65168ca ae15fa32
Pipeline #33272 passed with stages
in 7 minutes and 32 seconds
......@@ -5,7 +5,7 @@ import Footer from './components/Footer';
import UI from './config/ui';
import AddressesPage from './containers/AddressesPage';
import LoginPage from './containers/LoginPage';
import MyDataPage from './containers/MyDataPage';
import MyDataPage from './containers/MyData/MyDataPage';
import MyParcelsPage from './containers/MyParcelsPage';
import ParcelPage from './containers/ParcelPage';
import ShippingPage from './containers/ShippingPage';
......@@ -13,8 +13,8 @@ import OfflinePage from './containers/OfflinePage';
import DOIPage from './containers/DOIPage';
import UserManagementPage from './containers/UserManagementPage';
import SearchPage from './containers/SearchPage';
import OpenDataPage from './containers/OpenDataPage';
import ClosedDataPage from './containers/ClosedDataPage';
import OpenDataPage from './containers/OpenData/OpenDataPage';
import ClosedDataPage from './containers/ClosedData/ClosedDataPage';
import DatasetsPage from './containers/DatasetsPage';
import EventTagPage from './containers/EventTagPage';
import EventsPage from './containers/EventsPage';
......@@ -22,12 +22,11 @@ import SelectionPage from './containers/Selection/SelectionPage';
import CameraPage from './containers/CameraPage';
import DataStatisticsPage from './containers/DataStatisticsPage';
import MintSelectionPage from './containers/Selection/MintSelectionPage';
import BeamlineDataPage from './containers/BeamlineDataPage';
import BeamlineDataPage from './containers/BeamlineData/BeamlineDataPage';
import { getRemainingSessionTime } from './helpers/auth';
import { doLogOut, doSilentRefreshFromSSO } from './actions/login';
import keycloak from './keycloak';
import Menu from './components/Menu/Menu';
import { fetchAllInvestigations } from './actions/investigations';
import { useQuery } from './helpers/hooks';
import PageNotFound from './containers/PageNotFound';
import LoadingBoundary from './components/LoadingBoundary';
......@@ -43,12 +42,6 @@ function App() {
const dispatch = useDispatch();
useEffect(() => {
if (user.sessionId) {
dispatch(fetchAllInvestigations(user.sessionId));
}
}, [dispatch, user.sessionId]);
useEffect(() => {
if (!user.expirationTime) {
return;
......@@ -107,24 +100,20 @@ function App() {
<Menu />
<Switch>
<Route
exact
path={['/', '/home', '/investigations']}
component={MyDataPage}
/>
<Route exact path={['/', '/home', '/investigations']}>
<MyDataPage />
</Route>
<Route exact path="/search" component={SearchPage} />
<Route
exact
path="/usermanagement"
component={UserManagementPage}
/>
<Route exact path="/usermanagement">
<UserManagementPage />
</Route>
<Route exact path="/public" component={OpenDataPage} />
<Route exact path="/public/:prefix/:suffix" component={DOIPage} />
<Route exact path="/closed" component={ClosedDataPage} />
<Route exact path="/beamline/:id" component={BeamlineDataPage} />
<Route exact path="/beamline/:name" component={BeamlineDataPage} />
{isSampleTrackingEnabled && (
<Route exact path="/parcels" component={MyParcelsPage} />
......@@ -171,7 +160,11 @@ function App() {
<Route exact path="/selection" component={SelectionPage} />
<Route exact path="/selection/mint" component={MintSelectionPage} />
<Route exact path="/manager/stats" component={DataStatisticsPage} />
{user.isAdministrator && (
<Route exact path="/manager/stats">
<DataStatisticsPage />
</Route>
)}
<Route exact path="/login">
<Redirect
......
import axios from 'axios';
import {
getEmbargoedInvestigations,
getInvestigationsByInstrumentScientist,
getInvestigationsByUser,
getReleasedInvestigations,
} from '../api/icat-plus/catalogue';
import {
FETCH_INSTRUMENT_SCIENTIST_INVESTIGATIONS,
FETCH_INVESTIGATIONS,
FETCH_MY_INVESTIGATIONS,
FETCH_RELEASED_INVESTIGATIONS,
} from '../constants/actionTypes';
export function fetchMyInvestigations(sessionId) {
return {
type: FETCH_MY_INVESTIGATIONS,
payload: axios.get(getInvestigationsByUser(sessionId)),
};
}
export function fetchEmbargoedInvestigations(sessionId) {
return {
type: FETCH_INVESTIGATIONS,
payload: axios.get(getEmbargoedInvestigations(sessionId)),
};
}
export function fetchReleasedInvestigations(sessionId) {
return {
type: FETCH_RELEASED_INVESTIGATIONS,
payload: axios.get(getReleasedInvestigations(sessionId)),
};
}
export function fetchInvestigationsAsInstrumentScientist(sessionId) {
return {
type: FETCH_INSTRUMENT_SCIENTIST_INVESTIGATIONS,
payload: axios.get(getInvestigationsByInstrumentScientist(sessionId)),
};
}
export function fetchAllInvestigations(sessionId) {
return (dispatch) => {
dispatch(fetchMyInvestigations(sessionId));
dispatch(fetchReleasedInvestigations(sessionId));
dispatch(fetchEmbargoedInvestigations(sessionId));
dispatch(fetchInvestigationsAsInstrumentScientist(sessionId));
};
}
......@@ -13,26 +13,6 @@ export function getUsersByInvestigationIds(sessionId, investigationIds) {
return `${ICATPLUS.server}/catalogue/${sessionId}/investigation/id/${investigationIds}/investigationusers`;
}
export function getInvestigationById(sessionId, investigationId) {
return `${ICATPLUS.server}/catalogue/${sessionId}/investigation/id/${investigationId}/investigation`;
}
export function getEmbargoedInvestigations(sessionId) {
return `${ICATPLUS.server}/catalogue/${sessionId}/investigation/status/embargoed/investigation`;
}
export function getReleasedInvestigations(sessionId) {
return `${ICATPLUS.server}/catalogue/${sessionId}/investigation/status/released/investigation`;
}
export function getInvestigationsByUser(sessionId) {
return `${ICATPLUS.server}/catalogue/${sessionId}/investigation?useris=participant`;
}
export function getInvestigationsByInstrumentScientist(sessionId) {
return `${ICATPLUS.server}/catalogue/${sessionId}/investigation/useris/instrumentscientist/investigation`;
}
export function getDatasetsById(sessionId, datasetIds) {
return `${ICATPLUS.server}/catalogue/${sessionId}/dataset/id/${datasetIds}/dataset`;
}
......
......@@ -14,16 +14,17 @@ import {
Row,
} from 'react-bootstrap';
import { useForm, FormProvider } from 'react-hook-form';
import { useSelector } from 'react-redux';
import FieldAlert from '../Form/FieldAlert';
import TextFieldGroup from '../Form/TextFieldGroup';
import styles from './AddressFormModal.module.css';
import { useResource } from 'rest-hooks';
import InvestigationResource from '../../resources/investigation';
function AddressFormModal(props) {
const { investigation, address, onSubmitAsync, onCloseModal } = props;
const userInvestigations = useSelector(
(state) => state.myInvestigations.data
);
const myInvestigations = useResource(InvestigationResource.listShape(), {
filter: 'participant',
});
const methods = useForm({ defaultValues: address });
const { handleSubmit, errors, register, formState } = methods;
......@@ -75,18 +76,18 @@ function AddressFormModal(props) {
) : (
<FormGroup controlId="formInvestigation">
<div className={styles.row}>
<ControlLabel className={styles.proposalLabel}>
Proposal
<ControlLabel className={styles.investigationLabel}>
Investigation
</ControlLabel>
<div>
<FormControl
name="investigationId"
componentClass="select"
placeholder="Select a proposal"
placeholder="Select an investigation"
inputRef={register({ required: true })}
>
{userInvestigations &&
userInvestigations.map((investigation) => (
{myInvestigations &&
myInvestigations.map((investigation) => (
<option
key={investigation.id}
value={investigation.id}
......@@ -98,7 +99,7 @@ function AddressFormModal(props) {
</div>
{errors.investigationId && (
<FieldAlert
fieldLabel="Proposal"
fieldLabel="Investigation"
error={errors.investigationId}
/>
)}
......
......@@ -4,7 +4,7 @@
justify-content: flex-start;
}
.proposalLabel {
.investigationLabel {
margin-left: 1rem;
margin-right: 1rem;
}
......@@ -16,6 +16,7 @@ import { useQuery } from '../../helpers/hooks';
import { useHistory } from 'react-router';
import AddressPanel from './AddressPanel';
import AddressFormModal from './AddressFormModal';
import ModalLoadingBoundary from '../ModalLoadingBoundary';
function MyAddressesSummary() {
const [alert, setAlert] = useState();
......@@ -100,11 +101,16 @@ function MyAddressesSummary() {
return (
<>
{(isCreating || editingId) && (
<AddressFormModal
address={editedAddress}
onSubmitAsync={handleSubmit}
<ModalLoadingBoundary
message="Loading investigations..."
onCloseModal={handleCloseModal}
/>
>
<AddressFormModal
address={editedAddress}
onSubmitAsync={handleSubmit}
onCloseModal={handleCloseModal}
/>
</ModalLoadingBoundary>
)}
{alert && <Alert bsStyle={alert.style}>{alert.message}</Alert>}
<Grid fluid>
......
......@@ -67,9 +67,9 @@ function getColumns({ showProposalLinks, showInvestigationStats, showFiles }) {
{
text: 'Beamline',
dataField: 'visitId',
formatter: (visitId, investigation) => (
formatter: (_, investigation) => (
<span style={{ fontWeight: 'bold' }}>
{beamlineFormatter(investigation, visitId)}
{beamlineFormatter(investigation)}
</span>
),
sort: true,
......@@ -99,7 +99,7 @@ function getColumns({ showProposalLinks, showInvestigationStats, showFiles }) {
{
text: 'Datasets',
dataField: 'datasets',
formatter: datasetCountFormatter,
formatter: (_, { parameters }) => datasetCountFormatter(parameters),
responsiveHeaderStyle: getLgHeaderStyle(130, !showInvestigationStats),
},
{
......@@ -107,7 +107,7 @@ function getColumns({ showProposalLinks, showInvestigationStats, showFiles }) {
hidden: !showFiles,
dataField: 'dummy-1',
isDummyField: true,
formatter: fileCountFormatter,
formatter: (_, { parameters }) => fileCountFormatter(parameters),
responsiveHeaderStyle: getLgHeaderStyle(80, !showInvestigationStats),
},
{
......@@ -130,11 +130,11 @@ function getColumns({ showProposalLinks, showInvestigationStats, showFiles }) {
function InvestigationTable(props) {
const {
investigations,
showProposalLinks = false,
showInvestigationStats = false,
withProposalLinks = false,
withInvestigationStats = false,
} = props;
const user = useSelector((state) => state.user);
const { sessionId, isAdministrator } = useSelector((state) => state.user);
const history = useHistory();
const query = useQuery();
......@@ -188,7 +188,7 @@ function InvestigationTable(props) {
renderer: (investigation) => (
<InvestigationWidget
investigation={investigation}
sessionId={user.sessionId}
sessionId={sessionId}
/>
),
};
......@@ -221,9 +221,9 @@ function InvestigationTable(props) {
<ResponsiveTable
data={investigations.filter(isInvestigationMatching)}
columns={getColumns({
showProposalLinks: showProposalLinks || user.isAdministrator,
showInvestigationStats,
showFiles: user.isAdministrator,
showProposalLinks: withProposalLinks || isAdministrator,
showInvestigationStats: withInvestigationStats || isAdministrator,
showFiles: isAdministrator,
})}
expandRow={expandRow}
/>
......
import React, { Suspense } from 'react';
import React from 'react';
import { Panel, Tab, Tabs } from 'react-bootstrap';
import { NetworkErrorBoundary } from 'rest-hooks';
import Loader from '../Loader';
import SamplesTable from './SamplesTable';
import ParticipantsPanel from './ParticipantsPanel';
import LoadingBoundary from '../LoadingBoundary';
function InvestigationWidget(props) {
const { investigation } = props;
......@@ -16,11 +15,9 @@ function InvestigationWidget(props) {
<ParticipantsPanel investigationId={investigation.id} />
</Tab>
<Tab style={{ margin: 30 }} eventKey={2} title="Samples">
<Suspense fallback={<Loader message="Loading samples" />}>
<NetworkErrorBoundary>
<SamplesTable investigationId={investigation.id} />
</NetworkErrorBoundary>
</Suspense>
<LoadingBoundary message="Loading samples">
<SamplesTable investigationId={investigation.id} />
</LoadingBoundary>
</Tab>
</Tabs>
</Panel.Body>
......
......@@ -11,49 +11,34 @@ import { INVESTIGATION_DATE_FORMAT } from '../../constants';
import DOIBadge from '../doi/DOIBadge';
import { stringifyBytesSize } from '../../helpers';
export function getInstrumentName(investigation) {
if (!investigation) {
return undefined;
}
const { investigationInstruments } = investigation;
return (
investigationInstruments && investigationInstruments[0]?.instrument?.name
);
}
export function dateFormatter(date) {
return date ? moment(date).format(INVESTIGATION_DATE_FORMAT) : '';
}
export function beamlineFormatter(investigation, placeHolder = '') {
const instrumentName = getInstrumentName(investigation);
return instrumentName ? instrumentName.toUpperCase() : placeHolder;
export function beamlineFormatter(investigation) {
return (investigation?.instrument.name || '').toUpperCase();
}
export function nameFormatter(investigation, showLink) {
const { id, name } = investigation;
if (!showLink) {
return <span style={{ fontWeight: 'bold' }}>{investigation.name}</span>;
return <span style={{ fontWeight: 'bold' }}>{name}</span>;
}
return (
<Link to={`/investigation/${investigation.id}/datasets`}>
<Link to={`/investigation/${id}/datasets`}>
<Button bsSize="xsmall" style={{ width: 120, textAlign: 'left' }}>
<Glyphicon glyph="circle-arrow-right" />
<span style={{ marginLeft: 10 }}>{investigation.name} </span>
<span style={{ marginLeft: 10 }}>{name}</span>
</Button>
</Link>
);
}
export function volumeFormatter(_, investigation) {
const volume = investigation.parameters.find((o) => o.name === VOLUME);
if (volume && volume.value && volume.value !== 0) {
return stringifyBytesSize(volume.value);
}
}
export function experimentFormatter(investigation, showLink) {
const { summary, doi } = investigation;
return (
<Grid style={{ textAlign: 'center' }}>
<Row className="show-grid">
......@@ -64,43 +49,29 @@ export function experimentFormatter(investigation, showLink) {
<Row className="show-grid">
<Col xs={12} md={12}>
<span style={{ fontWeight: 'bold' }}>
{beamlineFormatter(investigation, investigation.visitId)}
{beamlineFormatter(investigation)}
</span>
</Col>
</Row>
<Row className="show-grid">
<Col xs={12}>
<div style={{ color: 'gray', fontStyle: 'italic' }}>
{investigation.summary}
</div>
<div style={{ color: 'gray', fontStyle: 'italic' }}>{summary}</div>
</Col>
</Row>
<Row className="show-grid" style={{ fontSize: 10 }}>
<Col xs={12}>
<DOIBadge doi={investigation.doi} />
<DOIBadge doi={doi} />
</Col>
</Row>
</Grid>
);
}
/**
* This looks into the parameters of the investigation
*/
export function getParameter(investigation, parameterName) {
const parameter = investigation.parameters.find(
(o) => o.name === parameterName
);
if (parameter && parameter.value) {
return parameter.value;
}
}
export function fileCountFormatter(_, investigation) {
const fileCount = getParameter(investigation, FILE_COUNT);
export function fileCountFormatter(parameters) {
const fileCount = parameters[FILE_COUNT];
if (!fileCount) {
return undefined;
if (fileCount === undefined) {
return '';
}
return (
......@@ -110,29 +81,31 @@ export function fileCountFormatter(_, investigation) {
);
}
export function datasetCountFormatter(cell, investigation) {
const datasetCount = investigation.parameters.find(
(o) => o.name === DATASET_COUNT
);
if (datasetCount && datasetCount.value) {
return (
<>
<span style={{ width: 40, textAlign: 'right', float: 'left' }}>
{datasetCount.value}
</span>
<span
style={{
width: 70,
marginLeft: 5,
float: 'left',
fontStyle: 'italic',
color: '#999',
fontSize: 12,
}}
>
({volumeFormatter(cell, investigation)})
</span>
</>
);
export function datasetCountFormatter(parameters) {
const datasetCount = parameters[DATASET_COUNT];
const volume = parameters[VOLUME];
if (datasetCount === undefined) {
return '';
}
return (
<>
<span style={{ width: 40, textAlign: 'right', float: 'left' }}>
{parameters[DATASET_COUNT]}
</span>
<span
style={{
width: 70,
marginLeft: 5,
float: 'left',
fontStyle: 'italic',
color: '#999',
fontSize: 12,
}}
>
{volume !== undefined && volume !== 0 && stringifyBytesSize(volume)}
</span>
</>
);
}
......@@ -4,18 +4,27 @@ import Loader from './Loader';
import { NetworkErrorBoundary } from 'rest-hooks';
function LoadingBoundary(props) {
const { authError, children, ...loaderProps } = props;
const {
customLoader,
withSilentError = false,
children,
...loaderProps
} = props;
const loader =
customLoader !== undefined ? customLoader : <Loader {...loaderProps} />;
const errorFallback = withSilentError
? () => null
: ({ error }) => (
<Alert bsStyle="warning" style={{ marginTop: 16 }}>
An error occured: {error.message}
</Alert>
);
return (
<Suspense fallback={<Loader {...loaderProps} />}>
<NetworkErrorBoundary
fallbackComponent={({ error }) => (
<Alert bsStyle="warning" style={{ marginTop: 16 }}>
{error.status === 403
? authError || 'Not allowed'
: `An error occured: ${error.message}`}
</Alert>
)}
>
<Suspense fallback={loader}>
<NetworkErrorBoundary fallbackComponent={errorFallback}>
{children}
</NetworkErrorBoundary>
</Suspense>
......
......@@ -303,7 +303,7 @@ EventListMenu.propTypes = {
/** Callback function used to trigger event list update */
getEvents: PropTypes.func,
/** Identifier of the current investigation */
investigationId: PropTypes.string,
investigationId: PropTypes.number,
/** whether the event list refreshes automatically when a new event has arrived */
autorefreshEventList: PropTypes.bool,
/** Whether the New button is enabled or not */
......
import { uniq } from 'lodash-es';
import React from 'react';
import { Button, Glyphicon, MenuItem, NavDropdown } from 'react-bootstrap';
import { Glyphicon, MenuItem, NavDropdown } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
import styles from './Menu.module.css';
import { useSelector } from 'react-redux';
import { useResource } from 'rest-hooks';
import InstrumentResource from '../../resources/instrument';
class ManagerMenu extends React.Component {
getBeamlineList(instruments) {
if (this.props.scientistInstrumentInvestigations.fetching) {
return (
<LinkContainer to="/usermanagement">
<MenuItem eventKey={3.3}>
Fetching beamlines
<Glyphicon
style={{ marginLeft: 10 }}
className="spin"
glyph="repeat"
/>
</MenuItem>
</LinkContainer>
);
}
function ManagerMenu() {
const { isAdministrator } = useSelector((state) => state.user);
if (instruments && instruments.length > 0) {
const items = instruments
.slice(0)
.filter((value) => !!value)
.sort()
.map((value, index) => (
<LinkContainer
key={value + index}
to={`/beamline/${value.toLowerCase()}`}