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

Merge branch 'issue_506' into 'master'

Issue 506

Closes #506

See merge request !526
parents ec1716f1 1fd8db1c
Pipeline #48831 passed with stages
in 13 minutes and 23 seconds
......@@ -23,6 +23,7 @@ import EventsPage from './containers/EventsPage';
import SelectionPage from './containers/Selection/SelectionPage';
import CameraPage from './containers/CameraPage';
import DataStatisticsPage from './containers/DataStatisticsPage';
import LogbookStatisticsPage from './containers/LogbookStatsPage';
import MintSelectionPage from './containers/Selection/MintSelectionPage';
import BeamlineDataPage from './containers/BeamlineData/BeamlineDataPage';
import { getRemainingSessionTime } from './helpers/auth';
......@@ -196,6 +197,12 @@ function App() {
</Route>
)}
{user.isAdministrator && (
<Route exact path="/manager/stats/logbook">
<LogbookStatisticsPage />
</Route>
)}
<Route exact path="/login">
<Redirect
to={
......
......@@ -29,6 +29,30 @@ export function getEventURL(
return `${ICATPLUS.server}/logbook/${sessionId}/event?${params.toString()}`;
}
export function getLogbookStatisticsURL(sessionId, startDate, endDate) {
const params = new URLSearchParams();
params.set('startDate', startDate);
params.set('endDate', endDate);
return `${
ICATPLUS.server
}/logbook/${sessionId}/stats/investigation?${params.toString()}`;
}
export function getCountLogbookStatisticsURL(
sessionId,
startDate,
endDate,
type
) {
const params = new URLSearchParams();
params.set('startDate', startDate);
params.set('endDate', endDate);
params.set('type', type);
return `${
ICATPLUS.server
}/logbook/${sessionId}/stats/count?${params.toString()}`;
}
/** Get the tags associated to a given investigation
* @param {string} sessionId session identifier
* @param {*} investigationId investigation identifier
......@@ -50,3 +74,12 @@ export function updateTagsByInvestigationId(sessionId, investigationId, tagId) {
export function createEventFromBase64(sessionId, investigationId) {
return `${ICATPLUS.server}/logbook/${sessionId}/investigation/id/${investigationId}/event/createfrombase64`;
}
/** Get the statistics of the logbook usage
* @param {string} sessionId session identifier
* @param {string} startDate the startDate of the period
* @param {string} endDate endDate of the period
*/
export function getStats(sessionId, startDate, endDate) {
return `${ICATPLUS.server}/logbook/${sessionId}/stats?startDate=${startDate}&endDate=${endDate}`;
}
......@@ -88,7 +88,7 @@ export function beamlineFormatter(investigation) {
return (investigation?.instrument.name || '').toUpperCase();
}
export function nameFormatter(investigation, showLink) {
export function nameFormatter(investigation, showLink, section = 'datasets') {
const { id, name } = investigation;
if (!showLink) {
......@@ -96,7 +96,7 @@ export function nameFormatter(investigation, showLink) {
}
return (
<Link to={`/investigation/${id}/datasets`}>
<Link to={`/investigation/${id}/${section}`}>
<Button bsSize="xsmall" style={{ width: 120, textAlign: 'left' }}>
<Glyphicon glyph="circle-arrow-right" />
<span style={{ marginLeft: 10 }}>{name}</span>
......
......@@ -125,6 +125,12 @@ function DataHeatmap(props) {
return <Loader message="Loading plots..."></Loader>;
}
if (!data[0]) {
return (
<Alert bsStyle="danger">No data was retrieved from elastic search</Alert>
);
}
return (
<div className="app__inner">
<Suspense fallback={<Loader message="Loading plots..." spacedOut />}>
......
......@@ -168,9 +168,6 @@ class GeneralStatsPanel extends React.Component {
textinfo: 'text',
texttemplate: '%{label}<br>%{value} GB',
text: data.definition.buckets.map((i) => {
console.log(
`${i.key.toUpperCase()} ${i.volume_stats.sum / GB}`
);
return `${i.key.toUpperCase()} ${parseFloat(
i.volume_stats.sum
)}`;
......
import React from 'react';
import { Grid, Row, Col } from 'react-bootstrap';
import ParameterTableWidget from '../../../Instrument/ParameterTableWidget';
function AnnotationsStatisticsPanel(props) {
const { statistics } = props;
const investigations =
statistics !== null
? statistics.filter((stats) => stats.annotations > 0)
: 0;
// Count notifications and annotations
const annotationsListCount = statistics.map((record) =>
record.annotations ? record.annotations : 0
);
// Calculate sum
const annotationsCount =
annotationsListCount.length > 0
? annotationsListCount.reduce((a, b) => a + b)
: 0;
/** Get maximum and minimum */
const maxAnnotations = Math.max(...annotationsListCount);
return (
<Grid fluid style={{ margin: 20 }}>
<Row>
<Col xs={12}>
<h4>Annotations</h4>
<ParameterTableWidget
striped
parameters={[
{
name: 'Investigation with Annotations',
value: investigations.length,
},
{
name: 'Total Annotations',
value: annotationsCount,
},
{
name: 'Max. Annotations/investigation',
value: maxAnnotations,
},
]}
/>
</Col>
</Row>
</Grid>
);
}
export default AnnotationsStatisticsPanel;
import React from 'react';
import { Grid, Row, Col } from 'react-bootstrap';
import ParameterTableWidget from '../../../Instrument/ParameterTableWidget';
function LogbookInvestigationStatisticsPanel(props) {
const { statistics } = props;
const instruments = new Set(statistics.map((i) => i.instrument));
const reducer = (a, b) => {
return {
count: a.count + b.count,
};
};
// Total events
const total = statistics.length > 0 ? statistics.reduce(reducer).count : 0;
// Count notifications and annotations
const annotationsListCount = statistics.map((record) =>
record.annotations ? record.annotations : 0
);
const notificationsListCount = statistics.map((record) =>
record.notifications ? record.notifications : 0
);
// Calculate sum
const annotationsCount =
annotationsListCount.length > 0
? annotationsListCount.reduce((a, b) => a + b)
: 0;
const notificationsCount =
notificationsListCount.length > 0
? notificationsListCount.reduce((a, b) => a + b)
: 0;
/** Get maximum and minimum */
const maxAnnotations = Math.max(...annotationsListCount);
const maxNotifications = Math.max(...notificationsListCount);
return (
<Grid fluid style={{ margin: 20 }}>
<Row>
<Col xs={12}>
<h4>General</h4>
<ParameterTableWidget
striped
parameters={[
{
name: 'Instruments',
value: instruments.size,
},
{
name: 'Investigations',
value: statistics.length,
},
{
name: 'Average Events/Investigations',
value: parseFloat(total / statistics.length).toFixed(0),
},
{
name: 'Max. Annotation/investigation',
value: maxAnnotations,
},
{
name: 'Max. Notifications/investigation',
value: maxNotifications,
},
{
name: 'Total Events',
value: total,
},
]}
/>
<h4>Event types</h4>
<ParameterTableWidget
striped
parameters={[
{
name: 'Annotations',
value: `${annotationsCount} (${parseFloat(
annotationsCount / total
).toFixed(2)}%)`,
},
{
name: 'Notifications',
value: `${notificationsCount} (${parseFloat(
notificationsCount / total
).toFixed(2)}%)`,
},
]}
/>
</Col>
</Row>
</Grid>
);
}
export default LogbookInvestigationStatisticsPanel;
import React from 'react';
import { useResource } from 'rest-hooks';
import { Panel } from 'react-bootstrap';
import CountlogbookStatisticsResource from '../../../../resources/countLogbookStatistics';
import PlotWidget from '../../../../containers/Stats/PlotWidget';
function LogbookStatisticsEvents(props) {
const { startDate, endDate, type, footer } = props;
/** Window with to resize the plots */
const { innerWidth: width } = window;
const statisticsCount = useResource(
CountlogbookStatisticsResource.listShape(),
{
startDate,
endDate,
type,
}
);
const trace = {
x: statisticsCount.map((s) => s._id),
y: statisticsCount.map((s) => s.count),
type: 'bar',
};
return (
<div className="app__inner">
<Panel style={{ margin: 10 }} bsStyle="primary">
<Panel.Heading>
<Panel.Title componentClass="h3">{type} created by date</Panel.Title>
</Panel.Heading>
<Panel.Body>
<PlotWidget
data={[trace]}
layout={{ width: width * 0.8, height: 350 }}
></PlotWidget>
</Panel.Body>
<Panel.Footer>{footer}</Panel.Footer>
</Panel>
</div>
);
}
export default LogbookStatisticsEvents;
import React from 'react';
import { useResource } from 'rest-hooks';
import { Tabs, Tab } from 'react-bootstrap';
import LogbookStatistics from '../../../../resources/logbookStatistics';
import LogbookUsagePanel from './LogbookUsagePanel';
import LogbookStatsResponsiveTable from './LogbookStatsResponsiveTable';
import LogbookStatisticsEvents from './LogbookStatisticsEvents';
function LogbookStatisticsTabs(props) {
const { startDate, endDate } = props;
const statistics = useResource(LogbookStatistics.listShape(), {
startDate,
endDate,
});
return (
<Tabs id="uncontrolled-tab-example" defaultActiveKey={1}>
<Tab eventKey={1} title="Logbook Usage">
<LogbookUsagePanel
startDate={startDate}
endDate={endDate}
statistics={statistics}
></LogbookUsagePanel>
</Tab>
<Tab eventKey={2} title="Raw Statistics">
<LogbookStatsResponsiveTable
statistics={statistics}
></LogbookStatsResponsiveTable>
</Tab>
<Tab eventKey={3} title="Events produced">
<LogbookStatisticsEvents
startDate={startDate}
endDate={endDate}
type="annotation"
footer="Annotations are hand-made entries in the logbook done by users"
></LogbookStatisticsEvents>
<LogbookStatisticsEvents
startDate={startDate}
endDate={endDate}
type="notification"
footer="Notifications are entries produced and sent automatically by beamline software"
></LogbookStatisticsEvents>
</Tab>
</Tabs>
);
}
export default LogbookStatisticsTabs;
import React from 'react';
import { stringifyBytesSize } from '../../../../helpers';
import { dateFormatter, nameFormatter } from '../../../Investigation/utils';
import { INVESTIGATION_DATE_FORMAT } from '../../../../constants';
import ResponsiveTable from '../../../Table/ResponsiveTable';
function LogbookStatsResponsiveTable(props) {
const { statistics } = props;
return (
<>
<br />
<ResponsiveTable
data={statistics}
pageOptions={{
showTotal: true,
sizePerPageList: [
{ text: '25', value: 25 },
{ text: '50', value: 50 },
{ text: '100', value: 100 },
{ text: '500', value: 500 },
],
}}
columns={[
{
text: 'investigationId',
dataField: 'investigationId',
hidden: true,
},
{
text: 'Instrument',
dataField: 'instrument',
hidden: false,
},
{
text: 'Proposal',
dataField: 'name',
hidden: false,
formatter: (_, record) =>
nameFormatter(
{
id: record.investigationId,
name: record.name,
},
true,
'events'
),
},
{
text: 'Title',
dataField: 'title',
hidden: false,
},
{
text: 'Start',
dataField: 'startDate',
hidden: false,
formatter: (_, record) =>
dateFormatter(record.startDate, INVESTIGATION_DATE_FORMAT, false),
},
{
text: 'End',
dataField: 'endDate',
hidden: false,
formatter: (_, record) =>
dateFormatter(record.endDate, INVESTIGATION_DATE_FORMAT, false),
},
{
text: 'Annotations',
dataField: 'annotations',
hidden: false,
sort: true,
},
{
text: 'Notifications',
dataField: 'notifications',
hidden: false,
sort: true,
},
{
text: 'Total events',
dataField: 'count',
hidden: false,
sort: true,
},
{
text: 'Sample',
dataField: '__sampleCount',
hidden: false,
},
{
text: 'Files',
dataField: '__fileCount',
hidden: false,
},
{
text: 'Datasets',
dataField: '__datasetCount',
hidden: false,
},
{
text: 'Volume',
dataField: '__volume',
hidden: false,
formatter: (_, record) =>
record.__volume
? stringifyBytesSize(record.__volume)
: stringifyBytesSize(0),
},
]}
/>
</>
);
}
export default LogbookStatsResponsiveTable;
import React from 'react';
import { Alert, Panel, Grid, Row, Col } from 'react-bootstrap';
import PlotWidget from '../../../../containers/Stats/PlotWidget';
import LogbookInvestigationStatisticsPanel from './LogbookInvestigationStatisticsPanel';
import AnnotationsStatisticsPanel from './AnnotationsStatisticsPanel';
const getPlot = (data, width) => {
return (
<PlotWidget
data={data}
layout={{
titlefont: {
size: 16,
},
width,
height: 300,
barmode: 'group',
font: {
family: 'Raleway, sans-serif',
size: 12,
},
xaxis: {
tickangle: -45,
},
yaxis: {
title: 'Logbook Entries',
tickangle: -45,
},
}}
responsive={true}
></PlotWidget>
);
};
function LogbookUsagePanel(props) {
const { startDate, endDate, statistics } = props;
/** Window with to resize the plots */
const { innerWidth: width } = window;
/** Gets an array with the counts */
const annotationsListCount = statistics.map((record) =>
record.annotations ? record.annotations : 0
);
const notificationsListCount = statistics.map((record) =>
record.notifications ? record.notifications : 0
);
/** labels */
const x = statistics.map((record) => `${record.name} ${record.instrument}`);
const annotationTrace = {
x,
y: annotationsListCount,
text: statistics.map((record) => `${record.instrument}`),
textposition: 'auto',
type: 'bar',
name: 'Annotations',
};
const notificationsTrace = {
x,
y: notificationsListCount,
type: 'bar',
name: 'Notifications',
};
return (
<div className="app__inner">
<>
<Alert bsStyle="info">
Logbook Usage for period <strong>{startDate}</strong> and{' '}
<strong>{endDate}</strong>.<br /> Change url parameters, startDate and
endDate to change manually the range of dates. Example:
/manager/starts/logbook?startDate=2020-08-01&endDate=2020-09-01
</Alert>
<h1> </h1>
<Panel style={{ margin: 10 }} bsStyle="primary">
<Panel.Heading>
<Panel.Title componentClass="h3">
Number of entries in the logbook per investigation
</Panel.Title>
</Panel.Heading>
<Panel.Body>
<Grid fluid>
<Row>
<Col xs={12} md={8}>
{getPlot(
[annotationTrace, notificationsTrace],
width * 0.6,
`Number of entries in the logbook per investigation.`
)}
</Col>