Commit d49d7d34 authored by Oleksii Savran's avatar Oleksii Savran Committed by Matija Obreza

Diversity trees: Public pages

parent 0ffacb5b
import Article from 'model/cms/Article';
import Crop from 'model/genesys/Crop';
import ImageGallery from 'model/repository/ImageGallery';
import RepositoryFile from 'model/repository/RepositoryFile';
import Dataset from 'model/catalog/Dataset';
import Subset from 'model/subset/Subset';
import DescriptorList from 'model/catalog/DescriptorList';
import DiversityTree from 'model/DiversityTree';
/*
* Defined in Swagger as '#/definitions/CropDetails'
*/
class CropDetails extends Crop {
public accessionCount: number;
public blurb: Article;
public files: RepositoryFile[];
public imageGallery: ImageGallery;
public overview: any;
public recentDatasets: Dataset[];
public recentSubsets: Subset[];
public recentDescriptorLists: DescriptorList[];
public diversityTrees: DiversityTree[];
}
export default CropDetails;
...@@ -4,11 +4,9 @@ import { bindActionCreators } from 'redux'; ...@@ -4,11 +4,9 @@ import { bindActionCreators } from 'redux';
import { WithTranslation, withTranslation } from 'react-i18next'; import { WithTranslation, withTranslation } from 'react-i18next';
import withStyles from '@material-ui/core/styles/withStyles'; import withStyles from '@material-ui/core/styles/withStyles';
import * as _ from 'lodash'; import * as _ from 'lodash';
import axios from 'axios';
// Actions // Actions
import { loadCropDetails } from 'crop/actions/public'; import { loadCropDetails } from 'crop/actions/public';
import { repositoryDownloadUrl } from 'repository/actions/public';
import { relinkAccessions } from 'crop/actions/admin'; import { relinkAccessions } from 'crop/actions/admin';
import { applyFiltersAsync as applyFilters, applyOverviewFiltersAsync as applyOverviewFilters } from 'accessions/actions/public'; import { applyFiltersAsync as applyFilters, applyOverviewFiltersAsync as applyOverviewFilters } from 'accessions/actions/public';
import { showSnackbar } from 'actions/snackbar'; import { showSnackbar } from 'actions/snackbar';
...@@ -38,9 +36,9 @@ import { IPageRequest } from '@genesys/client/model/Page'; ...@@ -38,9 +36,9 @@ import { IPageRequest } from '@genesys/client/model/Page';
import ErrorMessage from 'ui/common/error/ErrorMessage'; import ErrorMessage from 'ui/common/error/ErrorMessage';
import ImageGalleryView from 'repository/ui/c/ImageGalleryView'; import ImageGalleryView from 'repository/ui/c/ImageGalleryView';
import TreePreviewer from 'crop/ui/c/TreePreviewer'; import TreePreviewer from 'crop/ui/c/TreePreviewer';
import RepositoryFile from '@genesys/client/model/repository/RepositoryFile'; // import RepositoryFile from 'model/repository/RepositoryFile';
import { ScrollToTopOnMount } from 'ui/common/page/scrollers'; import { ScrollToTopOnMount } from 'ui/common/page/scrollers';
import { DatasetLink, SubsetLink } from 'ui/genesys/Links'; import { DatasetLink, DiversityTreeLink, SubsetLink } from 'ui/genesys/Links';
import { DescriptorListLink } from 'ui/catalog/Links'; import { DescriptorListLink } from 'ui/catalog/Links';
import { Grid } from '@material-ui/core'; import { Grid } from '@material-ui/core';
...@@ -67,8 +65,6 @@ interface IDisplayPageProps extends React.ClassAttributes<any>, WithTranslation ...@@ -67,8 +65,6 @@ interface IDisplayPageProps extends React.ClassAttributes<any>, WithTranslation
relinkAccessions: (shortName: string) => void; relinkAccessions: (shortName: string) => void;
applyOverviewFilters: (filters: string | AccessionFilter, page?: IPageRequest) => void; applyOverviewFilters: (filters: string | AccessionFilter, page?: IPageRequest) => void;
showSnackbar: (snack: string) => void; showSnackbar: (snack: string) => void;
repositoryDownloadUrl: (file: RepositoryFile) => string;
treeData: object;
} }
class DisplayPage extends React.Component<IDisplayPageProps, any> { class DisplayPage extends React.Component<IDisplayPageProps, any> {
...@@ -79,56 +75,12 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> { ...@@ -79,56 +75,12 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> {
}, },
]; ];
public state: {
treesData: {treeData: any, treeFile: any}[],
} = {
treesData: null,
};
public componentDidMount() { public componentDidMount() {
const { loadCropDetails, shortName, cropDetails, repositoryDownloadUrl } = this.props; const { loadCropDetails, shortName, cropDetails } = this.props;
if (shortName && (! cropDetails || shortName !== cropDetails.shortName)) { if (shortName && (! cropDetails || shortName !== cropDetails.shortName)) {
loadCropDetails(shortName); loadCropDetails(shortName);
this.setState({ treesData: null }); this.setState({ treesData: null });
} }
if (typeof window !== 'undefined') {
if (cropDetails && cropDetails.files && cropDetails.shortName === shortName) {
console.log(`Crop files`, cropDetails.files);
const treeFiles = cropDetails.files && cropDetails.files.filter((file) => {
return file.contentType === 'application/json' && file.originalFilename.match(/tree\.json$/);
});
if (treeFiles && treeFiles.length > 0) {
const treeFilePromises = treeFiles.map((treeFile) => axios.get(repositoryDownloadUrl(treeFile))
.then((response) => ({ treeData: response.data, treeFile }))
.catch((err) => { console.log(`Err`, err)}));
Promise.all(treeFilePromises)
.then((treesData) => this.setState({treesData: treesData.filter((treeData) => !!treeData)}))
.catch((err) => { console.log(`Error getting tree data`, err)});
}
}
}
}
public componentDidUpdate(prevProps: IDisplayPageProps) {
const { cropDetails: currentCrop } = prevProps;
const { cropDetails, repositoryDownloadUrl } = this.props;
const { treesData } = this.state;
if (cropDetails && (! currentCrop || (currentCrop.shortName !== cropDetails.shortName))) {
console.log(`Crop files`, cropDetails.files);
const treeFiles = cropDetails.files && cropDetails.files.filter((file) => {
return file.contentType === 'application/json' && file.originalFilename.match(/tree\.json$/);
});
if (treeFiles && treeFiles.length > 0 && ! treesData) {
const treeFilePromises = treeFiles.map((treeFile) => axios.get(repositoryDownloadUrl(treeFile))
.then((response) => ({ treeData: response.data, treeFile }))
.catch((err) => { console.log(`Err`, err)}));
Promise.all(treeFilePromises)
.then((treesData) => this.setState({treesData: treesData.filter((treeData) => !!treeData)}))
.catch((err) => { console.log(`Error getting tree data`, err)});
}
}
} }
private applyCropFilter = () => { private applyCropFilter = () => {
...@@ -172,7 +124,6 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> { ...@@ -172,7 +124,6 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> {
public render() { public render() {
const { cropDetails, error, shortName, classes, t } = this.props; const { cropDetails, error, shortName, classes, t } = this.props;
const { treesData } = this.state;
const crop = cropDetails; const crop = cropDetails;
return ( return (
...@@ -256,9 +207,6 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> { ...@@ -256,9 +207,6 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> {
propertyItemProps={ {classes: {propertiesRowLabel: classes.italicProperties} } } propertyItemProps={ {classes: {propertiesRowLabel: classes.italicProperties} } }
/> />
} }
{ treesData && treesData.map((tree) => (tree && tree.treeData.children && tree.treeData.children.length > 0 &&
<TreePreviewer key={ tree.treeFile.uuid } source={ tree.treeFile } treeData={ tree.treeData } name={ tree.treeFile.subject || tree.treeFile.title || crop.name } />
))}
</GridContainer > </GridContainer >
} }
<PropertiesCard title={ t(`crop.public.p.display.otherNames`) } <PropertiesCard title={ t(`crop.public.p.display.otherNames`) }
...@@ -314,6 +262,21 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> { ...@@ -314,6 +262,21 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> {
/> />
</Grid> </Grid>
} }
{ cropDetails.diversityTrees && cropDetails.diversityTrees.length > 0 &&
<Grid item lg={ 4 } xs={ 12 } style={ { minHeight: '100%' } }>
<PropertiesCard
style={ {height: '100%'} }
title={ t('DiversityTrees') } // todo: fix translations
propertyItemProps={ { keepEmpty: true } }
propertiesList={
cropDetails.diversityTrees
.map((diversityTree) => (
{ value: <DiversityTreeLink to={ diversityTree }/> }
))
}
/>
</Grid>
}
</GridContainer> </GridContainer>
</PageContents> </PageContents>
} }
...@@ -335,7 +298,6 @@ const mapDispatchToProps = (dispatch) => bindActionCreators({ ...@@ -335,7 +298,6 @@ const mapDispatchToProps = (dispatch) => bindActionCreators({
relinkAccessions, relinkAccessions,
applyOverviewFilters, applyOverviewFilters,
showSnackbar, showSnackbar,
repositoryDownloadUrl,
}, dispatch); }, dispatch);
......
import * as React from 'react'; import * as React from 'react';
// import { createStyles, withStyles } from '@material-ui/core/styles';
//
// const styles = createStyles({
// leafNode: {},
// node: {},
// });
let Tree; let Tree;
...@@ -6,21 +12,59 @@ interface IConfiguredTreeProps extends React.ClassAttributes<any> { ...@@ -6,21 +12,59 @@ interface IConfiguredTreeProps extends React.ClassAttributes<any> {
treeData: any[]; treeData: any[];
initialZoom: number; initialZoom: number;
zoomable?: boolean; zoomable?: boolean;
onClickHandler?: any;
}
class NodeLabel extends React.PureComponent<any, any> {
private timeout: any = null;
public state = {
count: 0,
};
private onClick = (e) => {
const { nodeData } = this.props;
if (this.state.count === 1) {
console.log('expand!');
clearTimeout(this.timeout);
this.setState({ count: 0 });
return;
}
e.stopPropagation();
this.setState({ count: 1 });
this.timeout = setTimeout(() => {
console.log('custom handler!', nodeData);
this.setState({ count: 0 });
}, 300);
};
public render() {
const { nodeData } = this.props;
return (
<div onClick={ this.onClick }>
<h6 className="pl-20 pr-20 m-0" >{ nodeData.name }</h6>
</div>
)
}
} }
class ConfiguredTree extends React.Component<IConfiguredTreeProps, any> { class ConfiguredTree extends React.Component<IConfiguredTreeProps, any> {
private treeContainer = null; private treeContainer = null;
// private timeout: any = null;
public constructor(props, context) { public constructor(props, context) {
super(props, context); super(props, context);
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
Tree = require('react-d3-tree').Tree; Tree = require('react-d3-tree').Tree;
} }
this.state = {
translate: null,
};
} }
public state = {
count: 0,
translate: null,
};
public componentDidMount() { public componentDidMount() {
if (this.treeContainer) { if (this.treeContainer) {
const dimensions = this.treeContainer.getBoundingClientRect(); const dimensions = this.treeContainer.getBoundingClientRect();
...@@ -34,12 +78,13 @@ class ConfiguredTree extends React.Component<IConfiguredTreeProps, any> { ...@@ -34,12 +78,13 @@ class ConfiguredTree extends React.Component<IConfiguredTreeProps, any> {
} }
public render() { public render() {
const {treeData, initialZoom, zoomable = false} = this.props; const { treeData, initialZoom, zoomable = false } = this.props;
return ( return (
<div style={ { height: '100%' } } ref={ (ref) => this.treeContainer = ref }> <div style={ { height: '100%' } } ref={ (ref) => this.treeContainer = ref } /* onClickCapture={ this.test } */>
{ treeData && typeof window !== 'undefined' && { treeData && typeof window !== 'undefined' &&
<Tree <Tree
// onClick={ this.onNodeClick }
separation={ { separation={ {
siblings: 0.25, siblings: 0.25,
nonSiblings: 1, nonSiblings: 1,
...@@ -61,20 +106,20 @@ class ConfiguredTree extends React.Component<IConfiguredTreeProps, any> { ...@@ -61,20 +106,20 @@ class ConfiguredTree extends React.Component<IConfiguredTreeProps, any> {
circle: { circle: {
fill: '#88ba42', fill: '#88ba42',
}, },
name: { // name: {
strokeWidth: 0.4, // strokeWidth: 0.4,
transform: 'translate(-13px, -1px)', // transform: 'translate(-13px, -1px)',
textAnchor: 'end', // textAnchor: 'end',
}, // },
}, },
leafNode: { leafNode: {
circle: { circle: {
fill: '#777777', fill: '#777777',
}, },
name: { // name: {
strokeWidth: 0.4, // strokeWidth: 0.4,
transform: 'translate(13px)', // transform: 'translate(13px)',
}, // },
}, },
}, },
} } } }
...@@ -85,6 +130,15 @@ class ConfiguredTree extends React.Component<IConfiguredTreeProps, any> { ...@@ -85,6 +130,15 @@ class ConfiguredTree extends React.Component<IConfiguredTreeProps, any> {
textAnchor: 'start', textAnchor: 'start',
y: 0, y: 0,
} } } }
allowForeignObjects
nodeLabelComponent={{
render: <NodeLabel />,
foreignObjectWrapper: {
y: -6,
x: -10,
height: '1.1rem',
},
}}
/> />
} }
</div> </div>
......
...@@ -17,7 +17,7 @@ const publicRoutes = [ ...@@ -17,7 +17,7 @@ const publicRoutes = [
// }, // },
// }, // },
{ {
path: '/divtree/:uuid([a-z\\-0-9]+)', path: '/divtree/:uuid([a-z\\-0-9]+)/:tab?',
component: Loadable({ component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "divtree" */'divtree/ui/DisplayPage'), loader: () => import(/* webpackMode:"lazy", webpackChunkName: "divtree" */'divtree/ui/DisplayPage'),
}), }),
......
...@@ -2,6 +2,8 @@ import * as React from 'react'; ...@@ -2,6 +2,8 @@ import * as React from 'react';
import { WithTranslation, withTranslation } from 'react-i18next'; import { WithTranslation, withTranslation } from 'react-i18next';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import axios from 'axios';
import { createStyles, WithStyles, withStyles } from '@material-ui/core/styles';
// Actions // Actions
import { import {
rematchDiversityTreeAccessions, rematchDiversityTreeAccessions,
...@@ -13,6 +15,7 @@ import { ...@@ -13,6 +15,7 @@ import {
editDiversityTree, editDiversityTree,
} from 'divtree/actions/public'; } from 'divtree/actions/public';
import navigateTo from 'actions/navigation'; import navigateTo from 'actions/navigation';
import { repositoryDownloadUrl } from 'repository/actions/public';
// import { addSubsetAccessionsToMyList } from 'list/actions/public'; // import { addSubsetAccessionsToMyList } from 'list/actions/public';
// Models // Models
import DiversityTree from '@genesys/client/model/DiversityTree'; import DiversityTree from '@genesys/client/model/DiversityTree';
...@@ -28,8 +31,37 @@ import ErrorMessage from 'ui/common/error/ErrorMessage'; ...@@ -28,8 +31,37 @@ import ErrorMessage from 'ui/common/error/ErrorMessage';
import { ScrollToTopOnMount } from 'ui/common/page/scrollers'; import { ScrollToTopOnMount } from 'ui/common/page/scrollers';
import BackButton from 'ui/common/buttons/BackButton'; import BackButton from 'ui/common/buttons/BackButton';
import DiversityTreeDisplay from './c/DiversityTreeDisplay'; import DiversityTreeDisplay from './c/DiversityTreeDisplay';
import Tabs, { Tab } from 'ui/common/Tabs';
import RepositoryFile from 'model/repository/RepositoryFile';
import ConfiguredTree from 'crop/ui/c/ConfiguredTree';
interface IDisplayPageProps extends React.ClassAttributes<any>, WithTranslation { const styles = () => createStyles({
layoutOverrides: {
'& > div': {
'& > div:first-child': {
'flexShrink': 0,
'flexGrow': 2,
'height': '100%',
'display': 'flex',
'flexDirection': 'column',
'& > div:first-child': {
flexBasis: 'auto',
},
},
},
},
treeWrapper: {
'direction': 'ltr',
'fontSize': '1.2rem',
'flexGrow': 2,
'display': 'flex',
'& > div': {
flexGrow: 2,
},
},
});
interface IDisplayPageProps extends React.ClassAttributes<any>, WithTranslation, WithStyles {
uuid: string; uuid: string;
divtree: DiversityTree; divtree: DiversityTree;
userRole: string[]; userRole: string[];
...@@ -46,6 +78,8 @@ interface IDisplayPageProps extends React.ClassAttributes<any>, WithTranslation ...@@ -46,6 +78,8 @@ interface IDisplayPageProps extends React.ClassAttributes<any>, WithTranslation
rematchDiversityTreeAccessions: (divtree: DiversityTree) => void; rematchDiversityTreeAccessions: (divtree: DiversityTree) => void;
addDiversityTreeAccessionsToMyList: (divtree: DiversityTree) => void; addDiversityTreeAccessionsToMyList: (divtree: DiversityTree) => void;
accessionRefs: Page<DiversityTreeAccessionRef>; accessionRefs: Page<DiversityTreeAccessionRef>;
currentTab?: string;
repositoryDownloadUrl: (file: RepositoryFile) => string;
} }
class DisplayPage extends React.Component<IDisplayPageProps, any> { class DisplayPage extends React.Component<IDisplayPageProps, any> {
...@@ -63,33 +97,76 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> { ...@@ -63,33 +97,76 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> {
super(props, context); super(props, context);
} }
public state = {
treeData: null,
treeDataLoading: false,
};
public componentDidMount() { public componentDidMount() {
const { divtree, uuid, loadDiversityTree, loadMoreAccessions } = this.props; const { divtree, uuid, loadDiversityTree, loadMoreAccessions } = this.props;
if (uuid && (! divtree || uuid !== divtree.uuid)) { if (uuid && (! divtree || uuid !== divtree.uuid)) {
loadDiversityTree(uuid); loadDiversityTree(uuid);
loadMoreAccessions(uuid); loadMoreAccessions(uuid);
} }
if (typeof window !== 'undefined') {
if (divtree && divtree.treeFile && divtree.uuid === uuid) {
console.log('DivTree file', divtree.treeFile);
// const treeFiles = cropDetails.files && cropDetails.files.filter((file) => {
// return file.contentType === 'application/json' && file.originalFilename.match(/tree\.json$/);
// });
this.loadTreeFile(divtree.treeFile);
}
}
} }
public componentDidUpdate() { public componentDidUpdate(prevProps: IDisplayPageProps) {
const { loading, divtree, uuid, loadDiversityTree, loadMoreAccessions } = this.props; const { loading, divtree, uuid, loadDiversityTree, loadMoreAccessions } = this.props;
const { divtree: prevDivTree } = prevProps;
if (!loading && uuid && (! divtree || uuid !== divtree.uuid)) { if (!loading && uuid && (! divtree || uuid !== divtree.uuid)) {
loadDiversityTree(uuid); loadDiversityTree(uuid);
loadMoreAccessions(uuid); loadMoreAccessions(uuid);
} }
if (divtree && (! prevDivTree || (prevDivTree.uuid !== divtree.uuid))) {
this.loadTreeFile(divtree.treeFile);
}
} }
private loadTreeFile = (treeFile: RepositoryFile) => {
const { repositoryDownloadUrl } = this.props;
console.log('load divtree', treeFile);
this.setState({ treeDataLoading: true });
axios
.get(repositoryDownloadUrl(treeFile))
.then((response) => {
console.log(`Tree data`, treeFile, response.data);
if (typeof response.data === 'object') {
this.setState({ treeData: response.data, treeDataLoading: false });
}
})
.catch((err) => {
console.log(`Error getting tree data`, err);
this.setState({ treeDataLoading: false });
});
};
private loadMoreAccessions = (paged: Page<DiversityTreeAccessionRef>) => { private loadMoreAccessions = (paged: Page<DiversityTreeAccessionRef>) => {
const { loadMoreAccessions, divtree } = this.props; const { loadMoreAccessions, divtree } = this.props;
loadMoreAccessions(divtree.uuid, paged); loadMoreAccessions(divtree.uuid, paged);
} }
public render() { public render() {
const { error, divtree, loading, userRole, t, accessionRefs, rematchDiversityTreeAccessions, publishDiversityTree, rejectDiversityTree, approveDiversityTree, deleteDiversityTree, editDiversityTree } = this.props; const {
error, divtree, loading, userRole, t, accessionRefs, rematchDiversityTreeAccessions, publishDiversityTree,
rejectDiversityTree, approveDiversityTree, deleteDiversityTree, editDiversityTree, uuid, classes, currentTab,
} = this.props;
const { treeData, treeDataLoading } = this.state;
const isActionsActive: boolean = userRole.findIndex((role) => role === 'ROLE_ADMINISTRATOR') !== -1 || (divtree && divtree.state === PublishState.REVIEWING); const isActionsActive: boolean = userRole.findIndex((role) => role === 'ROLE_ADMINISTRATOR') !== -1 || (divtree && divtree.state === PublishState.REVIEWING);
return ( return (
<PageLayout> <PageLayout className={ classes.layoutOverrides }>
<ScrollToTopOnMount/> <ScrollToTopOnMount/>
<PageTitle title={ !loading ? divtree && divtree.title : t('common:label.loading', { what: t('divtree.public.p.display.title') }) }/> <PageTitle title={ !loading ? divtree && divtree.title : t('common:label.loading', { what: t('divtree.public.p.display.title') }) }/>
<ContentHeaderWithButton <ContentHeaderWithButton
...@@ -103,13 +180,25 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> { ...@@ -103,13 +180,25 @@ class DisplayPage extends React.Component<IDisplayPageProps, any> {
/> />
} }
/> />
<Tabs
<PageContents className="pt-1rem"> tab={ currentTab }
{ loading ? <Loading /> : tabs={
[
<Tab key="data" name="data" to={ `/divtree/${uuid}` }>
{ t('Info') }
</Tab>,
<Tab key="view" name="view" to={ `/divtree/${uuid}/view` } disabled={ !treeData }>
{ t('View') }
</Tab>,
]
}
/>
{ currentTab === 'data' && (loading ? <Loading /> :
<PageContents className="pt-1rem">
<div> <div>
{ error && <ErrorMessage error={ error }/> } { error && <ErrorMessage error={ error }/> }
{ divtree && { divtree &&
<DiversityTreeDisplay <DiversityTreeDisplay