Commit d56a8824 authored by Maxym Borodenko's avatar Maxym Borodenko
Browse files

Merge branch 'server-431-datasets-overview' into 'master'

Added ui part for Datasets overview

See merge request genesys-pgr/genesys-ui!273
parents c469a6b5 f814dc88
......@@ -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
*
......
......@@ -174,7 +174,7 @@ function getLabelName(path, value, lookups, prefix, t, labels) {
}
const prettyPath: string = path
// remove array indexes square brackets [0], [1]... from path.
// remove array indexes square brackets [0], [1]... from path.
.replace(/\[(.+?)\]/g, '')
// split path title.eq -> ['title', 'eq']
.split('.')
......
......@@ -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