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

Merge branch 'issue_249-dataset_pagination' into 'master'

Resolve "Add remote pagination on dataset list"

Closes #249

See merge request !550
parents fbfa138c a6650784
Pipeline #54947 passed with stages
in 10 minutes and 2 seconds
import axios from 'axios';
import { getDatasetsByInvestigationId } from '../api/icat-plus/catalogue';
import {
FETCH_DATASETS_BY_DOI,
FETCH_DATASETS_BY_INVESTIGATION,
} from '../constants/actionTypes';
import { getDatasetByDOI } from '../api/icat-plus/doi';
import { SET_DATASETS_COUNT } from '../constants/actionTypes';
export function fetchDatasetsByDOI(sessionId, doi) {
return function (dispatch) {
dispatch({
type: FETCH_DATASETS_BY_DOI,
payload: axios.get(getDatasetByDOI(sessionId, doi)),
});
};
}
export function fetchDatasetsByInvestigationId(sessionId, investigationId) {
return function (dispatch) {
dispatch({
type: FETCH_DATASETS_BY_INVESTIGATION,
payload: axios.get(
getDatasetsByInvestigationId(sessionId, investigationId)
),
});
/**
* Set the totol number of datasets
* @param {*} datasetsCount datasetsCount value
*/
export function setDatasetsCount(datasetsCount) {
return {
type: SET_DATASETS_COUNT,
datasetsCount,
};
}
import axios from 'axios';
import ICATPLUS from '../../config/icatPlus';
import { getURLParamsByDictionary } from '../../helpers/url';
export function getFilesByDatasetId(sessionId, datasetIds) {
return `${ICATPLUS.server}/catalogue/${sessionId}/dataset/id/${datasetIds}/datafile`;
......@@ -43,3 +44,25 @@ export function getInstrumentScientistsBySessionId(sessionId) {
export function createInstrumentScientists(sessionId) {
return `${ICATPLUS.server}/catalogue/${sessionId}/instrumentscientist`;
}
export function getDatasetsByInvestigationIdURL(
sessionId,
investigationId,
skip,
limit,
sortOrder,
sortBy,
search
) {
const params = getURLParamsByDictionary({
limit,
sortBy,
sortOrder,
skip,
search,
});
return `${getDatasetsByInvestigationId(
sessionId,
investigationId
)}?${params.toString()}`;
}
import ICATPLUS from '../../config/icatPlus';
import { getURLParamsByDictionary } from '../../helpers/url';
/**
* Get the URL used to mint a DOI
......@@ -9,6 +10,23 @@ export function getMintDOI(sessionId) {
return `${ICATPLUS.server}/doi/${sessionId}/mint`;
}
export function getDatasetByDOI(sessionId, doi) {
return `${ICATPLUS.server}/doi/${doi}/datasets?sessionId=${sessionId}`;
export function getDatasetByDOI(
sessionId,
doi,
skip,
limit,
sortOrder,
sortBy,
search
) {
const params = getURLParamsByDictionary({
limit,
sortBy,
sortOrder,
skip,
search,
});
return `${
ICATPLUS.server
}/doi/${doi}/datasets?sessionId=${sessionId}?${params.toString()}`;
}
import PropTypes from 'prop-types';
import React from 'react';
import React, { useState, useEffect } from 'react';
import { Glyphicon } from 'react-bootstrap';
import { VOLUME } from '../../constants/parameterTypes';
import ResponsiveTable from '../Table/ResponsiveTable';
......@@ -13,14 +13,76 @@ import {
} from './utils';
import { useSelector, useDispatch } from 'react-redux';
import { selectDatasets, deselectDatasets } from '../../actions/selection';
import { useHistory } from 'react-router';
import { useQuery, useQueryParams } from '../../helpers/hooks';
import { useResource } from 'rest-hooks';
import DatasetResource from '../../resources/dataset';
import { setDatasetsCount } from '../../actions/datasets';
function DatasetTable(props) {
const { datasets, expanded = [] } = props;
const {
investigationId,
doi,
defaultSizePerPage,
sizePerPageList,
expanded = [],
} = props;
const history = useHistory();
const query = useQuery();
const sessionId = useSelector((state) => state.user.sessionId);
const selectedDatasets = useSelector((state) => state.selectedDatasets);
const dispatch = useDispatch();
function useQueryParam(key, defaultValue) {
const value = useQueryParams()[key] || defaultValue;
const [queryParam, setQueryParam] = useState(value);
const updateValue = (newVal) => {
setQueryParam(newVal);
if (newVal) {
query.set(key, newVal);
} else {
query.delete(key);
}
history.push({ search: query.toString() });
};
return [queryParam, updateValue];
}
const [search, setSearch] = useQueryParam('search', '');
const [sizePerPage, setSizePerPage] = useState(defaultSizePerPage);
const [page, setPage] = useQueryParam('page', 1);
const [sortField, setSortField] = useQueryParam('sortField', 'startDate');
const [sortOrder, setSortOrder] = useQueryParam('sortOrder', -1);
let fetchingParams = {
investigationId,
doi,
limit: sizePerPage,
skip: sizePerPage * (page - 1),
sortBy: sortField ? sortField : 'startDate',
sortOrder: sortOrder ? sortOrder : -1,
};
if (search) {
fetchingParams = { ...fetchingParams, search };
}
const data = useResource(DatasetResource.listShape(), fetchingParams);
const totalSize = data && data.length > 0 ? data[0].meta?.page?.total : 0;
const totalWithoutFilters =
data && data.length > 0 ? data[0].meta?.page?.totalWithoutFilters : 0;
useEffect(() => {
dispatch(setDatasetsCount(totalWithoutFilters));
return () => {
dispatch(setDatasetsCount(0));
};
}, [dispatch, totalWithoutFilters]);
const columns = [
{ text: 'id', dataField: 'id', hidden: true },
{
......@@ -63,7 +125,7 @@ function DatasetTable(props) {
{
text: 'Definition',
dataField: 'definition',
sort: true,
sort: false,
formatter: (_, dataset) => techniqueFormatter(dataset),
headerStyle: () => ({ width: '50%', textAlign: 'center' }),
responsiveHeaderStyle: {
......@@ -86,7 +148,7 @@ function DatasetTable(props) {
{
text: 'Files',
dataField: '__fileCount',
sort: true,
sort: false,
formatter: (_, dataset) => fileCountFormatter(dataset),
headerStyle: () => ({ width: '50%', textAlign: 'center' }),
responsiveHeaderStyle: {
......@@ -99,7 +161,7 @@ function DatasetTable(props) {
{
text: 'Size',
dataField: VOLUME,
sort: true,
sort: false,
formatter: (_, dataset) => volumeFormatter(dataset),
headerStyle: () => ({ width: '50%', textAlign: 'center' }),
responsiveHeaderStyle: {
......@@ -113,7 +175,7 @@ function DatasetTable(props) {
{
text: 'Download',
dataField: 'download',
sort: true,
sort: false,
formatter: (_, dataset) => downloadFormatter(dataset, sessionId),
headerStyle: () => ({ width: '50%', textAlign: 'center' }),
responsiveHeaderStyle: {
......@@ -157,30 +219,49 @@ function DatasetTable(props) {
return true;
}
function handleTableChange(page, sizePerPage, sortField, sortOrder) {
setPage(parseInt(page || 1));
setSizePerPage(parseInt(sizePerPage || defaultSizePerPage));
setSortField(sortField);
const order = sortOrder ? (sortOrder === 'asc' ? 1 : -1) : undefined;
setSortOrder(order);
}
function onSearch(s) {
setSearch(s);
setPage(1);
}
return (
<div style={{ margin: 20 }}>
<ResponsiveTable
data={datasets}
columns={columns}
expandRow={expandRow}
selectRow={selectRow}
remote={true}
data={data}
defaultSearchText={search}
pageOptions={{
page: parseInt(page),
sizePerPage,
totalSize,
showTotal: true,
sizePerPageList: [
{ text: '25', value: 25 },
{ text: '50', value: 50 },
{ text: '100', value: 100 },
{ text: '1000', value: 1000 },
],
sizePerPageList,
}}
columns={columns}
handleTableChange={handleTableChange}
onSearch={onSearch}
delay={1250}
expandRow={expandRow}
selectRow={selectRow}
/>
</div>
);
}
DatasetTable.propTypes = {
datasets: PropTypes.array.isRequired,
investigationId: PropTypes.string,
doi: PropTypes.string,
expanded: PropTypes.array,
defaultSizePerPage: PropTypes.number,
sizePerPageList: PropTypes.array,
};
export default DatasetTable;
......@@ -9,7 +9,7 @@ import { setInvestigationBreadCrumbs } from '../../containers/investigation-brea
function TabContainerMenu(props) {
const { doi, investigation } = props;
const { isAnonymous } = useSelector((state) => state.user);
const datasetCount = useSelector((state) => state.datasets.data.length);
const datasetCount = useSelector((state) => state.datasets.datasetsCount);
const { investigationId } = useParams();
const breadcrumbsList = useSelector((state) => state.breadcrumbsList);
......@@ -65,16 +65,16 @@ function TabContainerMenu(props) {
</LinkContainer>
</>
)}
</>
)}
{!isAnonymous && !doi && (
<>
<LinkContainer to={`${routePrefix}/proposal`}>
<NavItem eventKey="summary" href="">
<Glyphicon glyph="cog" />
<span style={{ marginLeft: 2 }}> Proposal </span>
</NavItem>
</LinkContainer>
{!isAnonymous && (
<>
<LinkContainer to={`${routePrefix}/proposal`}>
<NavItem eventKey="summary" href="">
<Glyphicon glyph="cog" />
<span style={{ marginLeft: 2 }}> Proposal </span>
</NavItem>
</LinkContainer>
</>
)}
</>
)}
</Nav>
......
......@@ -13,19 +13,7 @@ export const FETCH_DATACOLLECTIONS_PENDING = 'FETCH_DATACOLLECTIONS_PENDING';
/** investigations **/
/** datasets */
export const FETCH_DATASETS_BY_INVESTIGATION =
'FETCH_DATASETS_BY_INVESTIGATION';
export const FETCH_DATASETS_BY_INVESTIGATION_FULFILLED =
'FETCH_DATASETS_BY_INVESTIGATION_FULFILLED';
export const FETCH_DATASETS_BY_INVESTIGATION_PENDING =
'FETCH_DATASETS_BY_INVESTIGATION_PENDING';
export const FETCH_DATASETS_BY_INVESTIGATION_REJECTED =
'FETCH_DATASETS_BY_INVESTIGATION_REJECTED';
export const FETCH_DATASETS_BY_DOI = 'FETCH_DATASETS_BY_DOI';
export const FETCH_DATASETS_BY_DOI_FULFILLED =
'FETCH_DATASETS_BY_DOI_FULFILLED';
export const FETCH_DATASETS_BY_DOI_PENDING = 'FETCH_DATASETS_BY_DOI_PENDING';
export const SET_DATASETS_COUNT = 'SET_DATASETS_COUNT';
/** logbook **/
export const SET_LOGBOOK_CONTEXT = 'SET_LOGBOOK_CONTEXT';
......
......@@ -5,20 +5,23 @@ import { useDispatch, useSelector } from 'react-redux';
import { setBreadCrumbs } from '../actions/breadcrumbs';
import TabContainerMenu from '../components/TabContainerMenu/TabContainerMenu';
import DatasetTable from '../components/Dataset/DatasetTable';
import Loader from '../components/Loader';
import { fetchDatasetsByDOI } from '../actions/datasets';
import { OPEN_DATA_PATH } from '../constants/routePaths';
import LoadingBoundary from '../components/LoadingBoundary';
function DOIPage() {
const { prefix, suffix } = useParams();
const doi = `${prefix}/${suffix}`;
const sessionId = useSelector((state) => state.user.sessionId);
const datasets = useSelector((state) => state.datasets);
const dispatch = useDispatch();
const sizePerPageList = [
{ text: '10', value: 10 },
{ text: '25', value: 25 },
{ text: '30', value: 30 },
{ text: '50', value: 50 },
];
useEffect(() => {
dispatch(fetchDatasetsByDOI(sessionId, doi));
dispatch(
setBreadCrumbs([
{ name: 'Open Data', link: OPEN_DATA_PATH },
......@@ -32,11 +35,13 @@ function DOIPage() {
<Row>
<Col sm={12}>
<TabContainerMenu doi={doi} />
{datasets.fetching ? (
<Loader message="Loading datasets..." spacedOut />
) : (
<DatasetTable datasets={datasets.data} />
)}
<LoadingBoundary inPanel message="Loading datasets...">
<DatasetTable
doi={doi}
defaultSizePerPage={10}
sizePerPageList={sizePerPageList}
/>
</LoadingBoundary>
</Col>
</Row>
</Grid>
......
import React, { useEffect } from 'react';
import React from 'react';
import { Col, Grid, Row, Alert } from 'react-bootstrap';
import { useParams } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import TabContainerMenu from '../components/TabContainerMenu/TabContainerMenu';
import Loader from '../components/Loader';
import DatasetTable from '../components/Dataset/DatasetTable';
import PageNotFound from './PageNotFound';
import { fetchDatasetsByInvestigationId } from '../actions/datasets';
import { useResource } from 'rest-hooks';
import InvestigationResource from '../resources/investigation';
import LoadingBoundary from '../components/LoadingBoundary';
function DatasetsPage() {
const { investigationId } = useParams();
......@@ -16,15 +15,7 @@ function DatasetsPage() {
id: investigationId,
});
const sessionId = useSelector((state) => state.user.sessionId);
const datasets = useSelector((state) => state.datasets);
const dispatch = useDispatch();
/* eslint-disable react-hooks/exhaustive-deps */
useEffect(() => {
dispatch(fetchDatasetsByInvestigationId(sessionId, investigationId));
// `investigation` object can be duplicated in store, so we use `investigation.id` as effect dependency
}, [dispatch, investigationId, sessionId, investigation?.id]);
if (!investigation) {
return <PageNotFound />;
......@@ -32,7 +23,12 @@ function DatasetsPage() {
const expanded =
investigation.visitId === 'publisher' ? datasets.data.map((d) => d.id) : [];
const sizePerPageList = [
{ text: '25', value: 25 },
{ text: '50', value: 50 },
{ text: '100', value: 100 },
{ text: '1000', value: 1000 },
];
return (
<Grid fluid>
<Row>
......@@ -53,11 +49,14 @@ function DatasetsPage() {
</strong>
</Alert>
{datasets.fetching ? (
<Loader message="Loading datasets..." spacedOut />
) : (
<DatasetTable datasets={datasets.data} expanded={expanded} />
)}
<LoadingBoundary inPanel message="Loading datasets...">
<DatasetTable
investigationId={investigationId}
expanded={expanded}
defaultSizePerPage={25}
sizePerPageList={sizePerPageList}
/>
</LoadingBoundary>
</Col>
</Row>
</Grid>
......
import {
FETCH_DATASETS_BY_DOI_FULFILLED,
FETCH_DATASETS_BY_DOI_PENDING,
FETCH_DATASETS_BY_INVESTIGATION_FULFILLED,
FETCH_DATASETS_BY_INVESTIGATION_PENDING,
FETCH_DATASETS_BY_INVESTIGATION_REJECTED,
LOG_OUT,
} from '../constants/actionTypes';
import { LOG_OUT, SET_DATASETS_COUNT } from '../constants/actionTypes';
const initialState = { data: [], fetching: false, fetched: false };
const initialState = {
data: [],
fetching: false,
fetched: false,
datasetsCount: 0,
};
const datasets = (state = initialState, action) => {
switch (action.type) {
case FETCH_DATASETS_BY_INVESTIGATION_PENDING: {
state = { ...state, data: [], fetched: false, fetching: true };
break;
}
case FETCH_DATASETS_BY_INVESTIGATION_FULFILLED: {
case LOG_OUT: {
state = {
...state,
data: action.payload.data,
fetched: true,
data: [],
fetching: false,
fetched: false,
datasetsCount: 0,
};
break;
}
case FETCH_DATASETS_BY_INVESTIGATION_REJECTED: {
state = { ...state, data: [], fetched: false, fetching: false };
break;
}
case FETCH_DATASETS_BY_DOI_PENDING: {
state = { ...state, data: [], fetched: false, fetching: true };
break;
}
case FETCH_DATASETS_BY_DOI_FULFILLED: {
case SET_DATASETS_COUNT: {
state = {
...state,
data: action.payload.data,
fetched: true,
fetching: false,
datasetsCount: action.datasetsCount,
};
break;
}
case LOG_OUT: {
state = { ...state, data: [], fetching: false, fetched: false };
break;
}
default:
break;
}
......
import { Resource } from 'rest-hooks';
import { store } from '../store';
import { getDatasetsByInvestigationIdURL } from '../api/icat-plus/catalogue';
import { getDatasetByDOI } from '../api/icat-plus/doi';
export default class DatasetResource extends Resource {
id = undefined;
pk() {
return this.id?.toString();
}
static get key() {
return 'DatasetResource';
}
static listUrl(params) {
const { sessionId } = store.getState().user;
const {
investigationId,
doi,
skip,
limit,
sortOrder,
sortBy,
search,
} = params;
if (doi) {
return getDatasetByDOI(
sessionId,
doi,
skip,
limit,
sortOrder,
sortBy,
search
);
}
return getDatasetsByInvestigationIdURL(
sessionId,
investigationId,
skip,
limit,
sortOrder,
sortBy,
search
);
}
static url(params) {
return this.listUrl(params);
}
}
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