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

Accession overviews

parent 468ea15d
......@@ -14,7 +14,8 @@
"item": "Item",
"item_plural": "Items",
"list": "List",
"list_plural": "Lists"
"list_plural": "Lists",
"prettyNumber": "{{value, number}}"
},
"message": {
"confirmDelete": "Deleting the {{what, lowercase}} record is only possible when there is no associated data."
......
......@@ -181,6 +181,47 @@
"40": "Cryopreserved collection",
"50": "DNA collection",
"99": "Other"
},
"sampleStatus": {
"100": "Wild",
"110": "Natural",
"120": "Semi-natural/wild",
"130": "Semi-natural/sown",
"200": "Weedy",
"300": "Traditional cultivar/Landrace",
"400": "Breeding/Research Material",
"410": "Breeders Line",
"411": "Synthetic population",
"412": "Hybrid",
"413": "Founder stock/base population",
"414": "Inbred line",
"415": "Segregating population",
"416": "Clonal selection",
"420": "Genetic stock",
"421": "Mutant",
"422": "Cytogenetic stocks",
"423": "Other genetic stocks",
"500": "Advanced/improved cultivar",
"600": "GMO",
"999": "Other"
},
"overview": {
"institute code": "Holding Institute",
"institute country code3": "Country of holding institute",
"crop shortName": "Crop name",
"cropName": "Crop",
"sampStat": "Biological status of accession",
"taxonomy genus": "Genus",
"taxonomy species": "Species",
"taxonomy genusSpecies": "Genus species",
"countryOfOrigin code3": "Country of Origin",
"donorCode": "FAO WIEWS code of donor institute",
"mlsStatus": "Available for distribution under the MLS",
"available": "Available for distribution",
"duplSite": "Site of safety duplication",
"breederCode": "Breeder code",
"sgsv": "Safety duplicated in Svalbard",
"storage": "Type of Germplasm storage"
}
}
}
......@@ -6,14 +6,19 @@ import FilteredPage, { IPageRequest } from 'model/FilteredPage';
import Accession from 'model/Accession';
import AccessionFilter from 'model/AccessionFilter';
import { list as listAccessions, getByUuid, getByDoi } from 'actions/genesys/accessionService';
import { RECEIVE_ACCESSIONS, RECEIVE_ACCESSION } from 'constants/accessions';
import {list as listAccessions, getByUuid, getByDoi, listOverview as listAccessionOverview} from 'actions/genesys/accessionService';
import {RECEIVE_ACCESSIONS, RECEIVE_ACCESSION, RECEIVE_ACCESSION_OVERVIEW} from 'constants/accessions';
const receiveAccessions = (paged: FilteredPage<Accession>, error = null) => ({
type: RECEIVE_ACCESSIONS,
payload: { paged, error },
});
const receiveAccessionOverview = (overview: any, error = null) => ({
type: RECEIVE_ACCESSION_OVERVIEW,
payload: { overview, error },
});
const receiveAccession = (accession: Accession, error = null) => ({
type: RECEIVE_ACCESSION,
payload: { accession, error },
......@@ -21,12 +26,12 @@ const receiveAccession = (accession: Accession, error = null) => ({
export { listAccessions as listAccessionsPromise };
export const updateRoute = (paged: FilteredPage<Accession>) => (dispatch) => {
export const updateRoute = (paged: FilteredPage<Accession>, path: string = '/a') => (dispatch) => {
const qs = {
s: paged.sort[0].property === Accession.DEFAULT_SORT.property ? undefined : paged.sort[0].property,
d: paged.sort[0].direction === Accession.DEFAULT_SORT.direction ? undefined : paged.sort[0].direction,
};
dispatch(navigateTo(paged.filterCode ? `/a/${paged.filterCode}` : '/a', qs));
dispatch(navigateTo(paged.filterCode ? `${path}/${paged.filterCode}` : path, qs));
};
export const applyFilters = (filters: string | AccessionFilter, page: IPageRequest = { page: 0 }) => (dispatch) => {
......@@ -34,6 +39,7 @@ export const applyFilters = (filters: string | AccessionFilter, page: IPageReque
return dispatch(listAccessions(filters, page))
.then((paged) => {
dispatch(receiveAccessions(paged));
dispatch(overviewAccessions(paged.filterCode));
dispatch(updateRoute(paged));
}).catch((error) => {
console.log(`API error`, error);
......@@ -41,6 +47,29 @@ export const applyFilters = (filters: string | AccessionFilter, page: IPageReque
});
};
export const applyOverviewFilters = (filters: string | AccessionFilter, page: IPageRequest = { page: 0 }) => (dispatch) => {
console.log('Applying new filter', filters);
return dispatch(listAccessions(filters, page))
.then((paged) => {
dispatch(receiveAccessions(paged));
dispatch(overviewAccessions(paged.filterCode));
dispatch(updateRoute(paged, '/a/overview'));
}).catch((error) => {
console.log(`API error`, error);
dispatch(receiveAccessions(null, error.response));
});
};
export const loadAccessionsOverviewPage = (filterCode: string) => (dispatch) => {
return dispatch(listAccessionOverview(filterCode))
.then((paged) => {
dispatch(receiveAccessionOverview(paged));
}).catch((error) => {
console.log(`API error`, error);
dispatch(receiveAccessionOverview(null, error.response));
});
};
export const loadAccessionsPage = (page: IPageRequest) => (dispatch, getState) => {
const filterCode = getState().accessions.paged.filterCode;
return dispatch(listAccessions(filterCode, page))
......@@ -65,3 +94,13 @@ export const loadAccession = ({ uuid, doi }: { uuid?: string, doi?: string }) =>
dispatch(receiveAccession(null, error.response));
});
};
export const overviewAccessions = (filterCode: string) => (dispatch) => {
return dispatch(listAccessionOverview(filterCode))
.then((overview) => {
dispatch(receiveAccessionOverview(overview));
}).catch((error) => {
console.log(`API error`, error);
dispatch(receiveAccession(null, error.response));
});
};
......@@ -42,3 +42,8 @@ export const list = (filter: string | AccessionFilter, page: IPageRequest) => (d
const authorization = getState().login.access_token;
return AccessionService.list(authorization, filter, page);
};
export const listOverview = (filter: string | AccessionFilter) => (dispatch, getState) => {
const authorization = getState().login.access_token;
return AccessionService.listOverview(authorization, filter);
};
export const RECEIVE_ACCESSIONS = 'accessions/RECEIVE_ACCESSIONS';
export const RECEIVE_ACCESSION_OVERVIEW = 'accessions/RECEIVE_ACCESSION_OVERVIEW';
export const RECEIVE_ACCESSION = 'accessions/RECEIVE_ACCESSION';
export const ACCESSION_FILTERFORM = 'Form/Accession/ACCESSION_FILTERFORM';
......
import update from 'immutability-helper';
import { IReducerAction } from 'model/common.model';
import { RECEIVE_ACCESSIONS, RECEIVE_ACCESSION } from 'constants/accessions';
import {RECEIVE_ACCESSIONS, RECEIVE_ACCESSION, RECEIVE_ACCESSION_OVERVIEW} from 'constants/accessions';
import FilteredPage from 'model/FilteredPage';
import Accession from 'model/Accession';
......@@ -11,11 +11,13 @@ const INITIAL_STATE: {
accessionError: any;
paged: FilteredPage<Accession>;
pagedError: any;
overview: any;
} = {
accession: null,
accessionError: null,
paged: null,
pagedError: null,
overview: null,
};
function accessions(state = INITIAL_STATE, action: IReducerAction) {
......@@ -38,6 +40,13 @@ function accessions(state = INITIAL_STATE, action: IReducerAction) {
});
}
case RECEIVE_ACCESSION_OVERVIEW: {
const { overview, error } = action.payload;
return update(state, {
overview: { $set: overview },
pagedError: { $set: error },
});
}
default:
return state;
}
......
......@@ -11,6 +11,7 @@ import FilteredPage, { IPageRequest } from 'model/FilteredPage';
const URL_GET_BY_DOI = `${API_ROOT}/api/v1/acn/{doi}`; // UrlTemplate doesn't like the / in DOI
const URL_GET_BY_UUID = UrlTemplate.parse(`${API_ROOT}/api/v1/acn/{UUID}`);
const URL_LIST = `${API_ROOT}/api/v1/acn/list`;
const URL_LIST_OVERVIEW = `${API_ROOT}/api/v1/acn/overview`;
/*
* Defined in Swagger as 'accession'
......@@ -87,6 +88,20 @@ class AccessionService {
}).then(({ data }) => data as FilteredPage<Accession>);
}
public static listOverview(authToken: string, filter: string | AccessionFilter) {
const qs = QueryString.stringify({
f: typeof filter === 'string' ? filter : undefined,
}, {});
const apiUrl = URL_LIST_OVERVIEW + (qs ? `?${qs}` : '');
const content = { data: typeof filter === 'string' ? null : { ...filter } };
return authenticatedRequest(authToken, {
url: apiUrl,
method: 'POST',
headers: {'Content-Type': 'application/json'},
...content,
}).then(({ data }) => data);
}
}
export default AccessionService;
......@@ -2,12 +2,14 @@ import * as React from 'react';
import Grid from '@material-ui/core/Grid';
import {Properties, PropertiesItem} from 'ui/common/Properties';
import {PageSection} from 'ui/layout/PageLayout';
import { translate } from 'react-i18next';
interface IBundledProps extends React.ClassAttributes<any> {
children?: any;
propertiesList: Array<{title: string, value: any}>;
title: string;
small?: boolean;
t: any;
}
......@@ -15,7 +17,7 @@ class PropertiesCard extends React.Component<IBundledProps, any> {
public render() {
const {children = null, propertiesList, title, small = false} = this.props;
const {children = null, propertiesList, title, small = false, t} = this.props;
return (
<Grid item lg={ small ? 6 : 12 } md={ 12 } xs={ 12 }>
......@@ -24,7 +26,11 @@ class PropertiesCard extends React.Component<IBundledProps, any> {
<Grid item md={ children ? 7 : 12 } sm={ 12 }>
<Properties>
{
propertiesList.map((property) => (<PropertiesItem title={ property.title }>{ property.value }</PropertiesItem>))
propertiesList.map((property) => (
<PropertiesItem title={ property.title }>
{ property.value && typeof property.value === 'number' ? t(`common:label.prettyNumber`, {value: property.value}) : property.value }
</PropertiesItem>
))
}
</Properties>
</Grid>
......@@ -40,4 +46,4 @@ class PropertiesCard extends React.Component<IBundledProps, any> {
}
}
export default PropertiesCard;
export default translate()(PropertiesCard);
......@@ -14,22 +14,35 @@ import MuiTab from '@material-ui/core/Tab';
import {withStyles} from '@material-ui/core/styles';
/*tslint:disable*/
const styles = () => ({
const styles = (theme) => ({
root: {
width: '100%',
height: '48px',
borderBottom: '1px #ccc solid'
borderBottom: '1px #ccc solid',
[theme.breakpoints.down('md')]: {
height: '96px',
},
},
actionsArea: {
display: 'flex' as 'flex',
overflow: 'hidden' as 'hidden',
alignItems: 'center' as 'center',
position: 'absolute' as 'absolute',
overflow: 'hidden' as 'hidden',
height: '48px',
maxWidth: '50%',
right: 0,
'& > * > *': {
margin: '0 8px',
},
[theme.breakpoints.down('md')]: {
right: 'auto' as 'auto',
},
},
tabsArea: {
[theme.breakpoints.down('md')]: {
position: 'relative' as 'relative',
bottom: '-48px'
},
}
});
/*tslint:enable*/
......@@ -71,14 +84,14 @@ class Tabs extends React.Component<any> {
return (
<div className={ classes.root }>
<div className="float-left">
<div className={ `pr-20 pl-20 float-right ${classes.actionsArea}` }>
{ actions }
</div>
<div className={ `float-left ${classes.tabsArea}` }>
<MuiTabs value={ currentTab } indicatorColor="primary" onChange={ tabChange }>
{ tabs.map((tab) => <MuiTab key={ tab.to } label={ tab.label } />) }
</MuiTabs>
</div>
<div className={ `float-right pr-20 pl-20 ${classes.actionsArea}` }>
{ actions }
</div>
</div>
);
}
......
......@@ -20,6 +20,7 @@ import PaginationComponent from 'ui/common/pagination';
import AccessionCard from 'ui/genesys/accession/AccessionCard';
import Tabs, {Tab} from 'ui/common/Tabs';
import AccessionFilters from './c/Filters';
// TODO only for demo
import Button from '@material-ui/core/Button';
class BrowsePage extends BrowsePageTemplate<Accession> {
......
import * as React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import { parse } from 'query-string';
import { translate } from 'react-i18next';
// Actions
import {applyFilters, loadAccessionsPage, listAccessionsPromise, updateRoute, applyOverviewFilters, loadAccessionsOverviewPage} from 'actions/accessions';
// Models
import Accession from 'model/Accession';
import FilteredPage, {IPageRequest} from 'model/FilteredPage';
import AccessionFilter from 'model/AccessionFilter';
// UI
import PageLayout, { PageContents } from 'ui/layout/PageLayout';
import GridLayout from 'ui/layout/GridLayout';
import ContentHeader from 'ui/common/heading/ContentHeader';
import Tabs, {Tab} from 'ui/common/Tabs';
import PropertiesCard from 'ui/common/PropertiesCard';
import PrettyFilters from 'ui/common/filter/PrettyFilters';
import AccessionFilters from './c/Filters';
// TODO only for demo
import Button from '@material-ui/core/Button';
interface IOverviewPageProps extends React.ClassAttributes<any> {
overview: any;
paged: FilteredPage<Accession>;
filterCode: string;
applyOverviewFilters: (filters: string | AccessionFilter, page?: IPageRequest) => void;
updateRoute: (paged: FilteredPage<Accession>, path?: string) => void;
currentTab: string;
t: any;
}
class BrowsePage extends React.Component<IOverviewPageProps, any> {
protected static needs = [
({ search, params: { filterCode } }) => {
const qs = parse(search || '');
const page = { direction: qs.d, properties: null };
if (qs.s) {
page.properties = [ ...qs.s ];
}
return applyOverviewFilters(filterCode || '', page);
},
];
constructor(props: IOverviewPageProps, context: any) {
super(props, context);
}
public componentWillMount() {
const { overview, paged, filterCode, applyOverviewFilters, updateRoute } = this.props;
if (! overview) {
applyOverviewFilters(filterCode || '');
} else {
updateRoute(paged, '/a/overview');
}
}
public render() {
const { paged, currentTab, applyOverviewFilters, overview, t } = this.props;
const overviewKeys = ['institute.code', 'institute.country.code3', 'cropName', 'crop.shortName', 'sampStat', 'taxonomy.genus', 'taxonomy.genusSpecies', 'taxonomy.species',
'countryOfOrigin.code3', 'donorCode', 'mlsStatus', 'available', 'duplSite', 'sgsv', 'storage', 'breederCode'];
const overviewsTerms = new Map();
if (overview) {
overviewKeys.forEach((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);
});
}
return (
<PageLayout
sidebar={
<AccessionFilters initialValues={ paged && paged.filter || {} } onSubmit={ applyOverviewFilters }/>
}
>
<ContentHeader title="Accession browser" subTitle="Explore curated sets of accessions" />
<Tabs
tab={ currentTab }
actions={
<span>
<Button> Select all </Button>
<Button> Delete selected </Button>
<Button> Share selected </Button>
</span>
}
>
<Tab name="data" to={ `/a/` }>Accessions</Tab>
<Tab name="overview" to={ `/a/overview/` }>Overview</Tab>
<Tab name="map" to={ `/a/map/` }>Map</Tab>
</Tabs>
<PrettyFilters
prefix="accessions"
filterObj={ paged && paged.filter || {} }
onSubmit={ applyOverviewFilters }
/>
{ overview &&
<div style={ {display: 'flex', marginTop: '1em', marginBottom: '1em'} }>
<PageContents>
<GridLayout>
{ overviewsTerms && overviewsTerms.get('institute.code') && overviewsTerms.get('institute.code').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('institute.code').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.institute code`) } small/>
}
{ overviewsTerms && overviewsTerms.get('institute.country.code3') && overviewsTerms.get('institute.country.code3').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('institute.country.code3').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.institute country code3`) } small/>
}
{ overviewsTerms && overviewsTerms.get('cropName') && overviewsTerms.get('cropName').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('cropName').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.cropName`) } small/>
}
{ overviewsTerms && overviewsTerms.get('crop.shortName') && overviewsTerms.get('crop.shortName').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('crop.shortName').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.crop shortName`) } small/>
}
{ overviewsTerms && overviewsTerms.get('sampStat') && overviewsTerms.get('sampStat').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('sampStat')
.filter((term) => term.term !== 'other').map((term) => ({title: t(`accession.sampleStatus.${term.term}`), value: term.count })) } title={ t(`accession.overview.sampStat`) } small/>
}
{ overviewsTerms && overviewsTerms.get('taxonomy.genus') && overviewsTerms.get('taxonomy.genus').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('taxonomy.genus').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.taxonomy genus`) } small/>
}
{ overviewsTerms && overviewsTerms.get('taxonomy.genusSpecies') && overviewsTerms.get('taxonomy.genusSpecies').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('taxonomy.genusSpecies').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.taxonomy genusSpecies`) } small/>
}
{ overviewsTerms && overviewsTerms.get('taxonomy.species') && overviewsTerms.get('taxonomy.species').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('taxonomy.species').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.taxonomy species`) } small/>
}
{ overviewsTerms && overviewsTerms.get('countryOfOrigin.code3') && overviewsTerms.get('countryOfOrigin.code3').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('countryOfOrigin.code3').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.countryOfOrigin code3`) } small/>
}
{ overviewsTerms && overviewsTerms.get('donorCode') && overviewsTerms.get('donorCode').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('donorCode').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.donorCode`) } small/>
}
{ overviewsTerms && overviewsTerms.get('mlsStatus') && overviewsTerms.get('mlsStatus').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('mlsStatus').map((term) => ({title: term.term === '1' ? 'Yes' : 'No', value: term.count })) } title={ t(`accession.overview.mlsStatus`) } small/>
}
{ overviewsTerms && overviewsTerms.get('available') && overviewsTerms.get('available').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('available').map((term) => ({title: term.term === '1' ? 'Yes' : 'No', value: term.count })) } title={ t(`accession.overview.available`) } small/>
}
{ overviewsTerms && overviewsTerms.get('duplSite') && overviewsTerms.get('duplSite').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('duplSite').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.duplSite`) } small/>
}
{ overviewsTerms && overviewsTerms.get('sgsv') && overviewsTerms.get('sgsv').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('sgsv').map((term) => ({title: term.term === '1' ? 'Yes' : 'No', value: term.count })) } title={ t(`accession.overview.sgsv`) } small/>
}
{ overviewsTerms && overviewsTerms.get('storage') && overviewsTerms.get('storage').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('storage').map((term) => ({title: t(`accession.storage.${term.term}`), value: term.count })) } title={ t(`accession.overview.storage`) } small/>
}
{ overviewsTerms && overviewsTerms.get('breederCode') && overviewsTerms.get('breederCode').length > 2 &&
<PropertiesCard propertiesList={ overviewsTerms.get('breederCode').map((term) => ({title: term.term, value: term.count })) } title={ t(`accession.overview.breederCode`) } small/>
}
</GridLayout>
</PageContents>
</div>
}
</PageLayout>
);
}
}
const mapStateToProps = (state, ownProps) => ({
paged: state.accessions.paged || undefined,
overview: state.accessions.overview,
filterCode: ownProps.match.params.filterCode,
currentTab: ownProps.match.params.tab || 'overview', // current tab, or ownProps.location.pathname
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
applyFilters,
loadDataPage: loadAccessionsPage,
loadDataPromise: listAccessionsPromise,
applyOverviewFilters,
loadAccessionsOverviewPage,
updateRoute,
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(translate()(BrowsePage));
......@@ -35,5 +35,6 @@ const AccessionFilters = ({handleSubmit, initialValues, initialize, ...other}) =
export default reduxForm({
enableReinitialize: true,
destroyOnUnmount: false,
form: ACCESSION_FILTERFORM,
})(AccessionFilters);
......@@ -10,6 +10,7 @@ import SubsetBrowsePage from 'ui/pages/subsets/BrowsePage';
import SubsetDisplayPage from 'ui/pages/subsets/DisplayPage';
import AccessionBrowsePage from 'ui/pages/accessions/BrowsePage';
import AccessionDisplayPage from 'ui/pages/accessions/DisplayPage';
import AccessionOverviewPage from 'ui/pages/accessions/OverviewPage';
import InstituteBrowsePage from 'ui/pages/institutes/BrowsePage';
import InstituteDisplayPage from 'ui/pages/institutes/DisplayPage';
import NotFound from 'ui/common/not-found'; // TODO Move to ui/pages!
......@@ -67,6 +68,11 @@ const routes = [
component: AccessionBrowsePage,
exact: true,
},
{
path: '/a/overview/:filterCode(v.+)?',
component: AccessionOverviewPage,
exact: true,
},
{
path: '/a/:uuid([a-z\\-0-9]+)',
component: AccessionDisplayPage,
......
Supports Markdown
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