Commit d62e1c8e authored by Alejandro De Maria Antolinos's avatar Alejandro De Maria Antolinos

Merge branch 'issue_469' into 'master'

Issue 469

See merge request !479
parents 052d6bf9 7151f0e8
Pipeline #37400 passed with stages
in 11 minutes and 34 seconds
......@@ -13,6 +13,7 @@ import OfflinePage from './containers/OfflinePage';
import DOIPage from './containers/DOIPage';
import UserManagementPage from './containers/UserManagementPage';
import CalendarPage from './containers/Calendar/CalendarPage';
import SampleTrackingStatsPage from './containers/Stats/SampleTracking/SampleTrackingStatsPage';
import SearchPage from './containers/SearchPage';
import OpenDataPage from './containers/OpenData/OpenDataPage';
import ClosedDataPage from './containers/ClosedData/ClosedDataPage';
......@@ -119,6 +120,10 @@ function App() {
<CalendarPage component={CalendarPage} />
</Route>
<Route exact path="/manager/stats/sampletracking">
<SampleTrackingStatsPage component={SampleTrackingStatsPage} />
</Route>
<Route exact path="/public" component={OpenDataPage} />
<Route exact path="/public/:prefix/:suffix" component={DOIPage} />
......@@ -176,7 +181,7 @@ function App() {
</Route>
{user.isAdministrator && (
<Route exact path="/manager/stats">
<Route exact path="/manager/stats/data">
<DataStatisticsPage />
</Route>
)}
......
......@@ -43,8 +43,10 @@ function ManagerMenu() {
</MenuItem>
</LinkContainer>
<MenuItem divider />
{isAdministrator && (
<LinkContainer to="/manager/stats">
<LinkContainer to="/manager/stats/data">
<MenuItem eventKey="stats">
<Glyphicon glyph="stats" />
<span style={{ marginLeft: 10 }}>Data Statistics</span>
......@@ -52,6 +54,15 @@ function ManagerMenu() {
</LinkContainer>
)}
{isAdministrator && (
<LinkContainer to="/manager/stats/sampletracking">
<MenuItem eventKey="envelope">
<Glyphicon glyph="envelope" />
<span style={{ marginLeft: 10 }}>Sample Tracking Statistics</span>
</MenuItem>
</LinkContainer>
)}
<MenuItem divider />
{sortedInstruments.map(({ id, name }) => (
......
import React, { lazy, useState } from 'react';
import { useResource } from 'rest-hooks';
import ParcelResource from '../../../resources/parcel';
import ResponsiveTable from '../../../components/Table/ResponsiveTable';
import Moment from 'moment';
import DayPickerInput from 'react-day-picker/DayPickerInput';
import { INVESTIGATION_DATE_FORMAT } from '../../../constants';
import 'react-day-picker/lib/style.css';
import {
getStatisticsByParcels,
getParcelCountByStatus,
} from './ParcelStatsUtils';
import ParameterTableWidget from '../../../components/Instrument/ParameterTableWidget';
import {
Glyphicon,
ControlLabel,
Form,
FormGroup,
PageHeader,
Panel,
Grid,
Row,
Col,
Alert,
} from 'react-bootstrap';
import { STATUS, STATUS_DEFS } from '../../../constants/parcelStatuses';
import ParcelStatsColumns from './ParcelStatsColumns';
import createPlotlyComponent from 'react-plotly.js/factory';
// https://reactjs.org/docs/code-splitting.html
// https://github.com/plotly/react-plotly.js#customizing-the-plotlyjs-bundle
const Plot = lazy(() =>
import('plotly.js-basic-dist').then((Plotly) => ({
default: createPlotlyComponent(Plotly),
}))
);
const beamlineTest = 'ID00';
function ParcelStats() {
const [start, setStart] = useState(Moment(new Date()).subtract(1, 'month'));
const [end, setEnd] = useState(Moment(new Date()));
const parcels = useResource(ParcelResource.listShape(), {}).filter(
(parcel) =>
parcel.investigation &&
parcel.investigation.instrument.name !== { beamlineTest } &&
parcel.investigation.startDate &&
parcel.investigation.endDate &&
Moment(parcel.investigation.startDate) >= Moment(start) &&
Moment(parcel.investigation.endDate) <= Moment(end)
);
const stats = getStatisticsByParcels(parcels);
return (
<>
<Form inline>
<FormGroup controlId="formInlineName">
<ControlLabel>
Search parcels on sessions starting between:{' '}
</ControlLabel>{' '}
<DayPickerInput
format={INVESTIGATION_DATE_FORMAT}
placeholder="Start Date"
value={Moment(start).format(INVESTIGATION_DATE_FORMAT)}
onDayChange={(selectedDay, modifiers, dayPickerInput) =>
setStart(dayPickerInput.getInput().value)
}
dayPickerProps={{
todayButton: 'Today',
}}
/>
</FormGroup>{' '}
<FormGroup controlId="formInlineEmail">
<ControlLabel>and</ControlLabel>{' '}
<DayPickerInput
format={INVESTIGATION_DATE_FORMAT}
placeholder="End Date"
value={Moment(end).format(INVESTIGATION_DATE_FORMAT)}
onDayChange={(selectedDay, modifiers, dayPickerInput) =>
setEnd(dayPickerInput.getInput().value)
}
dayPickerProps={{
todayButton: 'Today',
}}
/>
</FormGroup>{' '}
</Form>
<PageHeader>Parcel Statistics</PageHeader>
<Panel>
<Panel.Heading>Summary</Panel.Heading>
<Panel.Body>
<Alert bsStyle="warning">
<strong>Test parcels are filtered!</strong> A parcel is considered
as test if the beamline associated is{' '}
<strong>{beamlineTest}</strong>
</Alert>
<Grid fluid style={{ margin: 20 }}>
<Row>
<Col xs={12} md={2}>
<ParameterTableWidget
striped
parameters={[
{ name: 'Parcels', value: stats.parcelsCount },
{ name: 'Beamlines', value: stats.beamlinesCount },
]}
></ParameterTableWidget>
<h4>Items</h4>
<ParameterTableWidget
striped
parameters={[
{ name: 'Samples', value: stats.samplesCount },
{ name: 'Tools', value: stats.toolsCount },
{ name: 'Others', value: stats.othersCount },
{ name: 'Total items', value: stats.itemsCount },
]}
></ParameterTableWidget>
</Col>
<Col xs={12} md={2}>
<ParameterTableWidget
striped
parameters={Object.keys(STATUS).map((status) => {
return {
name: (
<span>
<Glyphicon
style={{ marginRight: '10px' }}
glyph={STATUS_DEFS[status].icon}
></Glyphicon>
{status}
</span>
),
value: getParcelCountByStatus(parcels, status),
};
})}
></ParameterTableWidget>
</Col>
<Col xs={0} md={1}></Col>
<Col xs={12} md={7}>
<Plot
data={[
{
name: 'Parcels',
type: 'bar',
x: stats.beamlines,
y: stats.parcelsPerBeamline,
},
{
x: stats.beamlines,
y: stats.itemsPerBeamline,
type: 'scatter',
mode: 'lines+markers',
marker: { color: 'red' },
yaxis: 'y2',
name: 'Items',
},
]}
layout={{
title: 'Sample tracking on Beamlines',
xaxis: { title: 'Instruments' },
yaxis: { title: 'Parcels' },
showlegend: true,
legend: {
y: 80,
orientation: 'h',
},
yaxis2: {
title: 'Items',
overlaying: 'y',
side: 'right',
showgrid: false,
},
}}
/>
</Col>
</Row>
</Grid>
</Panel.Body>
</Panel>
<Panel>
<Panel.Heading>Sessions Summary</Panel.Heading>
<Panel.Body>
{' '}
<ResponsiveTable
data={stats.sessions}
columns={ParcelStatsColumns}
pageOptions={{
sizePerPageList: [
{ text: '25', value: 25 },
{ text: '50', value: 50 },
{ text: '100', value: 100 },
{ text: '500', value: 500 },
],
}}
/>
</Panel.Body>
</Panel>
</>
);
}
export default ParcelStats;
import {
dateFormatter,
nameFormatter,
} from '../../../components/Investigation/utils';
const ParcelStatsColumns = [
{
text: 'id',
dataField: 'id',
hidden: true,
},
{
text: 'investigationName',
dataField: 'investigationName',
hidden: true,
},
{
text: 'Proposal',
dataField: 'investigation',
sort: true,
formatter: (investigation) =>
investigation ? nameFormatter(investigation, true) : '',
},
{
text: 'Beamline',
dataField: 'investigation',
formatter: (investigation) => {
return investigation ? investigation.instrument.name : 'Not available';
},
sort: true,
},
{
text: 'Start',
dataField: 'investigation',
sort: true,
formatter: (investigation) => {
return investigation ? dateFormatter(investigation) : 'Not available';
},
},
{
text: '# Parcels',
dataField: 'parcels',
formatter: (parcels) => {
return parcels.length;
},
sort: true,
},
{
text: '# Items',
dataField: 'itemCount',
sort: true,
},
{
text: '# Samples',
dataField: 'sampleCount',
sort: true,
},
];
export default ParcelStatsColumns;
import _ from 'lodash-es';
export function getItemsCount(parcels) {
const count = parcels.map((p) => {
return p.items.length;
});
return count.length === 0 ? 0 : count.reduce((total, num) => total + num);
}
export function getParcelCountByStatus(parcels, status) {
return parcels.filter((p) => p.status === status).length;
}
export function getCount(parcels, type) {
const count = parcels.map((p) => {
return p.items.filter((item) => item.type === type).length;
});
return count.length === 0 ? 0 : count.reduce((total, num) => total + num);
}
export function getStatisticsByParcels(parcels) {
const groupedByInvestigationId = _.groupBy(parcels, 'investigationId');
const sessions = [];
for (const investigationId in groupedByInvestigationId) {
const parcels = groupedByInvestigationId[investigationId];
const itemCount = parcels
.map((p) => p.items.length)
.reduce((total, num) => total + num);
const sampleCount = parcels
.map((p) => p.items.filter((i) => i.type === 'SAMPLESHEET').length)
.reduce((total, num) => total + num);
sessions.push({
beamline:
groupedByInvestigationId[investigationId][0].investigation.instrument
.name,
investigationName:
groupedByInvestigationId[investigationId][0].investigationName,
parcels,
investigation: groupedByInvestigationId[investigationId][0].investigation,
itemCount,
sampleCount,
});
}
const beamlines = [...new Set(sessions.map((item) => item.beamline))].sort();
const parcelsPerBeamline = beamlines.map((beamline) => {
return parcels.filter((p) => {
return p.investigation.instrument.name === beamline;
}).length;
});
const itemsPerBeamline = beamlines.map((beamline) => {
return parcels
.filter((p) => {
return p.investigation.instrument.name === beamline;
})
.map((p) => p.items.length)
.reduce((total, num) => total + num);
});
return {
sessions,
parcelsCount: parcels.length,
itemsCount: getItemsCount(parcels),
samplesCount: getCount(parcels, 'SAMPLESHEET'),
toolsCount: getCount(parcels, 'TOOL'),
othersCount: getCount(parcels, 'OTHER'),
beamlinesCount: beamlines.length,
beamlines,
parcelsPerBeamline,
itemsPerBeamline,
};
}
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { setBreadCrumbs } from '../../../actions/breadcrumbs';
import ParcelStats from './ParcelStats';
import LoadingBoundary from '../../../components/LoadingBoundary';
function SampleTrackingStatsPage() {
const dispatch = useDispatch();
useEffect(() => {
dispatch(setBreadCrumbs([{ name: 'My Parcels' }]));
}, [dispatch]);
return (
<div className="app__inner">
<LoadingBoundary message="Loading parcels...">
<ParcelStats />
</LoadingBoundary>
</div>
);
}
export default SampleTrackingStatsPage;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment