Commit f814dc88 authored by Viacheslav Pavlov's avatar Viacheslav Pavlov
Browse files

Added ui part for Datasets overview

Fixed displaying of partner overview

fixed SSR

Fixed Map deserializing after SSR

Changed order of Overview and Browse pages
parent c469a6b5
......@@ -959,6 +959,11 @@
"DIGITIZER": "Digitizes data.",
"CURATOR": "Organizes and validates data and metadata in correct format, ensures quality of both."
}
},
"overview": {
"owner": "Data provider",
"crops": "Crop",
"rights": "Licence"
}
},
"dashboard": {
......@@ -1171,6 +1176,10 @@
"descriptorCountDesc": "Descriptor count (high to low)",
"startDate": "Experiment start date",
"endDate": "Experiment end date"
},
"tab": {
"data": "Datasets",
"overview": "Overview"
}
},
"descriptorlists": {
......
......@@ -4,13 +4,14 @@ import navigateTo from 'actions/navigation';
import { showSnackbar } from 'actions/snackbar';
import {createApiCaller, createPureApiCaller} from 'actions/ApiCall';
// Constants
import {RECEIVE_DATASET, APPEND_DATASET_PAGE, APPEND_ACCESSIONS_PAGE} from 'datasets/constants';
import {RECEIVE_DATASET, APPEND_DATASET_PAGE, APPEND_ACCESSIONS_PAGE, RECEIVE_DATASET_OVERVIEW} from 'datasets/constants';
// Model
import Dataset from 'model/catalog/Dataset';
import DatasetFilter from 'model/catalog/DatasetFilter';
import Page from 'model/Page';
import FilteredPage, { IPageRequest } from 'model/FilteredPage';
import { AccessionRef } from 'model/accession/AccessionRef';
import DatasetOverview from 'model/catalog/DatasetOverview';
// Service
import DatasetService from 'service/catalog/DatasetService';
// Util
......@@ -22,6 +23,7 @@ const apiLoadDataset = createApiCaller(DatasetService.getDataset, RECEIVE_DATASE
const apiLoadDatasetAccesions = createApiCaller(DatasetService.listAccessions, APPEND_ACCESSIONS_PAGE);
const apiListDatasets = createApiCaller(DatasetService.datasetList, APPEND_DATASET_PAGE);
const apiOverviewDatasets = createApiCaller(DatasetService.overview, RECEIVE_DATASET_OVERVIEW);
const apiListDatasetsPure = createPureApiCaller(DatasetService.datasetList);
......@@ -68,4 +70,20 @@ const promiselistDatasets = (page?, results?, sortBy?: string[], filter?, order?
});
};
export const updateOverviewRoute = (overview: DatasetOverview) => (dispatch) => {
dispatch(navigateTo(overview && overview.filterCode ? `/datasets/overview/${overview.filterCode}` : '/datasets/overview'));
};
export const applyOverviewFilters = (filters: string | DatasetFilter) => (dispatch) => {
dispatch(showSnackbar('Applying filters...'));
return dispatch(apiOverviewDatasets(filters))
.then((overview) => {
dispatch(updateOverviewRoute(overview));
dispatch(showSnackbar(`Filters applied.`));
});
};
export { loadMoreDatasets, promiselistDatasets, loadDataset, loadMoreAccessions };
......@@ -4,6 +4,7 @@ export const DATASET_LIST_OF_ACCESSION_FORM = 'Form/DATASET_LIST_OF_ACCESSION_FO
export const CREATE_DATASET = 'App/Dataset/RECEIVE_DATASET';
export const RECEIVE_DATASET = 'App/RECEIVE_DATASET';
export const APPEND_DATASET_PAGE = 'App/APPEND_DATASET_PAGE';
export const RECEIVE_DATASET_OVERVIEW = 'App/RECEIVE_DATASET_OVERVIEW';
export const APPEND_ACCESSIONS_PAGE = 'App/APPEND_ACCESSIONS_PAGE';
// dashboard
......
import update from 'immutability-helper';
import * as _ from 'lodash';
import {APPEND_DATASET_PAGE, RECEIVE_DATASET, APPEND_ACCESSIONS_PAGE} from 'datasets/constants';
import {
APPEND_DATASET_PAGE,
RECEIVE_DATASET,
APPEND_ACCESSIONS_PAGE,
RECEIVE_DATASET_OVERVIEW,
} from 'datasets/constants';
import { LOGIN_APP, LOGIN_USER, LOGOUT } from 'constants/login';
......@@ -10,15 +15,18 @@ import { AccessionRef } from 'model/accession/AccessionRef';
import Page from 'model/Page';
import FilteredPage from 'model/FilteredPage';
import ApiCall from 'model/ApiCall';
import DatasetOverview from 'model/catalog/DatasetOverview';
import {dereferenceReferences} from 'utilities';
const INITIAL_STATE: {
dataset: ApiCall<Dataset>,
paged: ApiCall<FilteredPage<Dataset>>,
overview: ApiCall<DatasetOverview>,
accessionRefs: ApiCall<Page<AccessionRef>>,
} = {
dataset: null,
paged: null,
overview: null,
accessionRefs: null,
};
......@@ -87,6 +95,20 @@ function datasetsPublic(state = INITIAL_STATE, action: { type?: string, payload?
});
}
case RECEIVE_DATASET_OVERVIEW: {
const { apiCall: { loading, error, timestamp, data } } = action.payload;
return update(state, {
overview: {
$set: {
loading,
error,
timestamp,
data: data !== undefined ? data : state.overview && state.overview.data,
},
},
});
}
default:
return state;
}
......
......@@ -16,6 +16,13 @@ const publicRoutes = [
subtitle: 'datasets.common.subtitle',
},
},
{
path: '/datasets/overview/:filterCode(v.+)?',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "datasets" */'datasets/ui/OverviewPage'),
}),
exact: true,
},
{
path: '/datasets/:filterCode(v.+)?',
component: Loadable({
......
......@@ -20,6 +20,11 @@
"DIGITIZER": "Digitizes data.",
"CURATOR": "Organizes and validates data and metadata in correct format, ensures quality of both."
}
},
"overview": {
"owner": "Data provider",
"crops": "Crop",
"rights": "Licence"
}
},
"dashboard": {
......@@ -232,5 +237,9 @@
"descriptorCountDesc": "Descriptor count (high to low)",
"startDate": "Experiment start date",
"endDate": "Experiment end date"
},
"tab": {
"data": "Datasets",
"overview": "Overview"
}
}
......@@ -19,6 +19,7 @@ import DatasetCard from './c/Card';
import ContentHeader from 'ui/common/heading/ContentHeader';
import { ScrollToTopOnMount } from 'ui/common/page/scrollers';
import Tabs, { Tab } from 'ui/common/Tabs';
import BrowsePageTemplate from 'ui/pages/_base/BrowsePage';
import { translate } from 'react-i18next';
......@@ -27,7 +28,7 @@ class BrowsePage extends BrowsePageTemplate<Dataset> {
protected renderDataset = (d: Dataset) => <DatasetCard dataset={ d } key={ d.uuid }/>;
public render() {
const { paged, t, loadMoreData, error, loading } = this.props;
const { paged, t, loadMoreData, error, loading, currentTab, filterCode } = this.props;
return (
<PageLayout sidebar={
......@@ -36,6 +37,10 @@ class BrowsePage extends BrowsePageTemplate<Dataset> {
<ScrollToTopOnMount/>
<PageTitle title={ t('datasets.common.modelName_plural') }/>
<ContentHeader title={ t('datasets.common.modelName_plural') } subtitle={ t('datasets.public.p.browse.subtitle') }/>
<Tabs tab={ currentTab }>
<Tab name="overview" to={ `/datasets/overview/${ filterCode || '' }` }>{ t('datasets.tab.overview') }</Tab>
<Tab name="data" to={ `/datasets/${ filterCode || '' }` }>{ t('datasets.tab.data') }</Tab>
</Tabs>
<PaginationComponent
pageObj={ paged }
onSortChange={ this.onSortChange }
......@@ -68,6 +73,7 @@ const mapStateToProps = (state, ownProps) => ({
loading: state.datasets.public.paged ? state.datasets.public.paged.loading : false,
error: state.datasets.public.paged ? state.datasets.public.paged.error : undefined,
filterCode: ownProps.match.params.filterCode,
currentTab: ownProps.match.params.tab || 'data', // current tab, or ownProps.location.pathname
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
......
import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { translate } from 'react-i18next';
// actions
import { applyOverviewFilters, updateOverviewRoute } from 'datasets/actions/public';
import { showSnackbar } from 'actions/snackbar';
import { loadPartners } from 'partners/actions/public';
// model
import ApiCall from 'model/ApiCall';
import DatasetOverview from 'model/catalog/DatasetOverview';
import DatasetFilter from 'model/catalog/DatasetFilter';
// UI
import PageLayout, { PageContents } from 'ui/layout/PageLayout';
import Tabs, { Tab } from 'ui/common/Tabs';
import ContentHeader from 'ui/common/heading/ContentHeader';
import Number from 'ui/common/Number';
import Loading from 'ui/common/Loading';
import PageTitle from 'ui/common/PageTitle';
import PropertiesCard from 'ui/common/PropertiesCard';
import { PartnerLink } from 'ui/catalog/Links';
import GridContainer from 'ui/layout/GridContainer';
import PrettyFilters from 'ui/common/filter/PrettyFilters';
import DatasetFilters from 'datasets/ui/c/Filters';
interface IOverviewPageProps extends React.ClassAttributes<any> {
overview: ApiCall<DatasetOverview>;
applyOverviewFilters: (filter: string | DatasetFilter) => void;
updateOverviewRoute: (overview: DatasetOverview) => void;
showSnackbar: (message: string) => void;
partnerNames: any;
filterCode: string;
currentTab: string;
t: any;
}
class OverviewPage extends React.Component<IOverviewPageProps> {
protected static needs = [
({search, params: {filterCode}}) => {
return applyOverviewFilters(filterCode || '');
},
() => loadPartners({page: 0, size: 50}),
];
public componentWillMount() {
const {overview: apiCall, filterCode, applyOverviewFilters} = this.props;
const {data: overview} = apiCall || {data: undefined};
if (!overview || filterCode !== overview.filterCode) {
console.log('Applying filters', filterCode);
applyOverviewFilters(filterCode || '');
}
}
private addTerm = (property, term) => {
const {overview: apiCall, applyOverviewFilters, showSnackbar} = this.props;
const {data: overview} = apiCall;
const updatedFilter: DatasetFilter = {...overview.filter};
switch (property) {
case 'crops':
case 'owner.uuid':
case 'rights':
_.set(updatedFilter, property, _.concat(_.get(updatedFilter, property), term).filter((x) => x != null));
break;
// set
default:
_.set(updatedFilter, property, term);
}
console.log(`Updated filter for ${ property } +${ term }`, updatedFilter);
showSnackbar('Applying filters...');
applyOverviewFilters(updatedFilter);
}
public render() {
const {filterCode, currentTab, overview: apiCall, applyOverviewFilters, t} = this.props;
const partnerNames = new Map(this.props.partnerNames);
const {data: overviewWrapper, loading} = apiCall || {data: undefined, loading: true};
const overview = overviewWrapper && overviewWrapper.overview;
const overviewsTerms = new Map();
if (overview) {
Object.keys(overview).map((key) => {
const overviewEl = overview[key];
const terms = [].concat(overviewEl.terms,
{
term: 'Other',
count: overviewEl.other,
},
{
term: 'Not specified',
count: overviewEl.missing,
},
);
overviewsTerms.set(key, terms);
});
}
const filterByTerm = (property, term, count) => {
const skipTerms = ['Other', 'Missing', 'Not specified'];
return (
skipTerms.indexOf(term.term) === -1
? (<a onClick={ () => this.addTerm(property, term.term) }><Number value={ count }/></a>)
: (count)
);
};
return (
<PageLayout
sidebar={
<DatasetFilters initialValues={ overviewWrapper && overviewWrapper.filter || {} } onSubmit={ applyOverviewFilters }/>
}
withFooter
>
<PageTitle title={ t('datasets.common.modelName_plural') }/>
<ContentHeader
title={ t('datasets.common.modelName_plural') }
subtitle={ t('datasets.public.p.browse.subtitle') }
/>
<Tabs tab={ currentTab }>
<Tab name="overview" to={ `/datasets/overview/${ filterCode || '' }` }>{ t('datasets.tab.overview') }</Tab>
<Tab name="data" to={ `/datasets/${ filterCode || '' }` }>{ t('datasets.tab.data') }</Tab>
</Tabs>
<PrettyFilters
prefix="datasets"
filterObj={ overviewWrapper && overviewWrapper.filter || {} }
onSubmit={ applyOverviewFilters }
/>
{ loading && <Loading/> }
{ overview &&
<div>
<PageContents className="pt-1rem">
<GridContainer>
{ overviewsTerms && overviewsTerms.get('owner.uuid') && overviewsTerms.get('owner.uuid').length > 2 &&
<PropertiesCard
propertiesList={ overviewsTerms.get('owner.uuid').map((term) => (
{
title: partnerNames && partnerNames.size > 0 && partnerNames.get(term.term) ? <PartnerLink uuid={ term.term }>{ partnerNames.get(term.term) }</PartnerLink> : term.term,
value: filterByTerm('owner.uuid', term, term.count),
}
)) }
title={ t(`datasets.common.overview.owner`) }
propertyItemProps={ {numeric: true} }
small
/>
}
{ overviewsTerms && overviewsTerms.get('crops') && overviewsTerms.get('crops').length > 2 &&
<PropertiesCard
propertiesList={ overviewsTerms.get('crops').map((term) => ({ title: term.term, value: filterByTerm('crops', term, term.count) })) }
title={ t(`datasets.common.overview.crops`) }
propertyItemProps={ {numeric: true} }
small
/>
}
{ overviewsTerms && overviewsTerms.get('rights') && overviewsTerms.get('rights').length > 2 &&
<PropertiesCard
propertiesList={ overviewsTerms.get('rights').map((term) => ({ title: term.term, value: filterByTerm('rights', term, term.count) })) }
title={ t(`datasets.common.overview.rights`) }
propertyItemProps={ {numeric: true} }
small
/>
}
</GridContainer>
</PageContents>
</div>
}
</PageLayout>
);
}
}
const mapStateToProps = (state, ownProps) => ({
overview: state.datasets.public.overview,
partnerNames: state.uuidDecoder.labels,
filterCode: ownProps.match.params.filterCode || '',
currentTab: ownProps.match.params.tab || 'overview', // current tab, or ownProps.location.pathname
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
applyOverviewFilters,
updateOverviewRoute,
showSnackbar,
}, dispatch);
export default translate()(connect(mapStateToProps, mapDispatchToProps)(OverviewPage));
......@@ -45,6 +45,7 @@ const DatasetFilters = ({ handleSubmit, initialize, t, ...other }) => (
);
export default translate()(reduxForm({
destroyOnUnmount: false,
enableReinitialize: true,
form: DATASET_FILTERFORM,
})(DatasetFilters));
import DatasetFilter from 'model/catalog/DatasetFilter';
/*
* Defined in Swagger as '#/definitions/DatasetOverview'
*/
class DatasetOverview {
public datasetCount: number;
public filter: DatasetFilter;
public filterCode: string;
public overview: any;
}
export default DatasetOverview;
......@@ -10,6 +10,7 @@ import DatasetLocation from 'model/catalog/DatasetLocation';
import FilteredPage from 'model/FilteredPage';
import Page, { IPageRequest } from 'model/Page';
import RepositoryFile from 'model/repository/RepositoryFile';
import DatasetOverview from 'model/catalog/DatasetOverview';
const URL_LIST_ACCESSIONS = UrlTemplate.parse(`/api/v1/dataset/accessions/{uuid}`);
const URL_ADD_DESCRIPTORS = UrlTemplate.parse(`/api/v1/dataset/add-descriptors/{uuid},{version}`);
......@@ -18,6 +19,7 @@ const URL_CREATE_DATASET = `/api/v1/dataset/create`;
const URL_REVIEW_DATASET = `/api/v1/dataset/for-review`;
const URL_DATASET_LIST = `/api/v1/dataset/list`;
const URL_MY_DATASETS = `/api/v1/dataset/list-mine`;
const URL_OVERVIEW = `/api/v1/dataset/overview`;
const URL_REMATCH_DATASET_ACCESSIONS = UrlTemplate.parse(`/api/v1/dataset/rematch-accessions/{uuid},{version}`);
const URL_REJECT_DATASET = `/api/v1/dataset/reject`;
const URL_REMOVE_DESCRIPTORS = UrlTemplate.parse(`/api/v1/dataset/remove-descriptors/{uuid},{version}`);
......@@ -238,6 +240,29 @@ class DatasetService {
}).then(({ data }) => data as FilteredPage<Dataset>);
}
/**
* overview at /api/v1/dataset/overview
*
* @param filter filter
* @param xhrConfig additional xhr config
*/
public static overview(filter?: DatasetFilter, xhrConfig?: any): Promise<DatasetOverview> {
const qs = QueryString.stringify({
f: typeof filter === 'string' ? filter : undefined,
}, {});
const apiUrl = URL_OVERVIEW + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { data: typeof filter === 'string' ? null : { ...filter } };
return axiosBackend.request({
...xhrConfig,
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as DatasetOverview);
}
/**
* rejectDataset at /api/v1/dataset/reject
*
......
......@@ -55,7 +55,7 @@ const PUBLIC_MENUS = [
label: 'public.menu.exploreSubsets',
},
{
to: '/datasets',
to: '/datasets/overview',
label: 'public.menu.exploreDatasets',
},
{
......
......@@ -375,7 +375,7 @@ class WelcomePage extends React.Component<IWelcomeProps, any> {
</Grid>
<Grid item xs={ 12 } md={ 3 }>
<Stats>
<Link to="/datasets">
<Link to="/datasets/overview">
<span className={ classes.amount }><Number value={ serverInfo.datasetCount } /></span>
{ t('datasets.common.stats', { count: serverInfo.datasetCount }) }
</Link>
......@@ -407,7 +407,7 @@ class WelcomePage extends React.Component<IWelcomeProps, any> {
</Grid>
<Grid item xs={ 12 } md={ 3 }>
<Stats>
<Link to="/datasets">
<Link to="/datasets/overview">
{ t('datasets.common.stats', { count: serverInfo.datasetCount }) }
</Link>
</Stats>
......
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