Skip to content

Commit

Permalink
PXP-6562 Feat/study viewer (#728)
Browse files Browse the repository at this point in the history
* rebase

* study viewer

* dont crash homepage

* access

* fix logic

* requestor redir

* fix title

* fetch doc data

* fix trigger

* object file download

* fix button logic

* requset error handle

* fix: useGuppyForExplorer

* fix lgtm

* doc

* fix doc

* remove dar dau

* datasets

* use accessibleValidationValue

* read-storage only

* no default for title

* better gql query generation

* openMode and defaultOpenTitle

* sv config as array

* doc

* put projectIsOpenData back

* fix: reset to trigger re-fetch

* error msg

* more fix about array of configs

* hide download button if no file

* fix crash for table items

* link in table

* link in table

* fix: don't flood guppy if request fails

* fix: filedmapping for ssv

* fix file optional

* hide help msg when needed

* update doc

* clean up

* fix login redirect flow

* disable request access btn when needed

* check access before sends out

* batch request

* clean
  • Loading branch information
mfshao authored Sep 29, 2020
1 parent a6cea28 commit 7b8324f
Show file tree
Hide file tree
Showing 14 changed files with 1,084 additions and 12 deletions.
47 changes: 47 additions & 0 deletions docs/study_viewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Study Viewer

Example configuration:

```
[
{
"dataType": "dataset",
"title": "Datasets", // page title
"titleField": "name", // row title
"listItemConfig": { // required
// displayed outside of table:
"blockFields": ["short_description"],
// displayed in table:
"tableFields": ["condition", ...],
},
"singleItemConfig": { //optional, if omitted, "listItemConfig" block will be used for both pages
// displayed outside of table:
"blockFields": ["long_description"],
// displayed in table:
"tableFields": ["condition", ...],
},
"fieldMapping": [...],
"rowAccessor": "project_id", // rows unique ID
"downloadField": "object_id", // GUID
"fileDataType": "clinicalTrialFile", // ES index of the clinical trial object files, optional
"docDataType": "openAccessFile", // ES index of the open access documents, optional
"openMode": "open-first", // optional, configure how the study viewer list do with each collapsible card on initial loading, see details in notes
"openFirstRowAccessor": "", // optional, only works if `openMode` is `open-first`
},
{
....another study viewer config
}
]
```

## Notes

1. The configuration above is subject to change. After `Tube` supports generating nested ES document then we can remove the `fileDataType` and `docDataType` fields.
2. Required fields for `fileData` and `docData` ES indices are: `file_name`, `file_size`, `data_format` and `data_type`. For `fileData`, additional required field is `object_id`; and for `docData`, `doc_url` is also required.
3. The field `rowAccessor` should be a field that exists in all 3 ES indices. The study viewer will use that field to cross query with different ES indices.
4. About `openMode` and `openFirstRowAccessor`, the list view of study browser supports 3 display modes on initial loading:
- `open-all`: opens all collapsible cards by default. And this is the default option if `openMode` is omitted in the config
- `close-all`: closes all collapsible cards by default
- `open-first`: opens the first collapsible card in the list and keeps all other cards closing
- When in `open-first` mode, user can specify a value using `openFirstRowAccessor`. The study viewer will try to find a study with that title in the list and bring it to the top of the list in order to open it.
5. The access request logic depends on `Requestor`, for more info, see [Requestor](https://github.com/uc-cdis/requestor/).
4 changes: 4 additions & 0 deletions src/Login/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,16 @@ class Login extends React.Component {
const location = this.props.location; // this is the react-router "location"
// compose next according to location.from
let next = (location.from) ? `${basename}${location.from}` : basename;
if (location.state && location.state.from) {
next = `${basename}${location.state.from}`;
}
// clean up url: no double slashes
next = next.replace(/\/+/g, '/');
const queryParams = querystring.parse(location.search ? location.search.replace(/^\?+/, '') : '');
if (queryParams.next) {
next = basename === '/' ? queryParams.next : basename + queryParams.next;
}
next = next.replace('?request_access', '?request_access_logged_in');
const customImage = components.login && components.login.image ?
components.login.image
: 'gene';
Expand Down
133 changes: 133 additions & 0 deletions src/StudyViewer/SingleStudyViewer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { Space, Typography, Spin, Result } from 'antd';
import { FileOutlined, FilePdfOutlined } from '@ant-design/icons';
import BackLink from '../components/BackLink';
import { humanFileSize } from '../utils.js';
import { ReduxStudyDetails, fetchDataset, fetchFiles, resetMultipleStudyData, fetchStudyViewerConfig } from './reduxer';
import getReduxStore from '../reduxStore';
import './StudyViewer.css';

const { Title } = Typography;

class SingleStudyViewer extends React.Component {
constructor(props) {
super(props);
this.state = {
dataType: undefined,
rowAccessor: undefined,
};
}

static getDerivedStateFromProps(nextProps, prevState) {
const newState = {};
if (nextProps.match.params.dataType
&& nextProps.match.params.dataType !== prevState.dataType) {
newState.dataType = nextProps.match.params.dataType;
}
if (nextProps.match.params.rowAccessor
&& nextProps.match.params.rowAccessor !== prevState.rowAccessor) {
newState.rowAccessor = nextProps.match.params.rowAccessor;
}
return Object.keys(newState).length ? newState : null;
}

render() {
if (this.props.noConfigError) {
this.props.history.push('/not-found');
}
if (!this.props.dataset) {
if (this.state.dataType && this.state.rowAccessor) {
getReduxStore().then(
store =>
Promise.allSettled(
[
store.dispatch(fetchDataset(decodeURIComponent(this.state.dataType),
decodeURIComponent(this.state.rowAccessor))),
store.dispatch(fetchFiles(decodeURIComponent(this.state.dataType), 'object', decodeURIComponent(this.state.rowAccessor))),
store.dispatch(fetchFiles(decodeURIComponent(this.state.dataType), 'open-access', decodeURIComponent(this.state.rowAccessor))),
store.dispatch(resetMultipleStudyData()),
],
));
}
return (
<div className='study-viewer'>
<div className='study-viewer_loading'>
<Spin size='large' tip='Loading data...' />
</div>
</div>
);
}

const studyViewerConfig = fetchStudyViewerConfig(this.state.dataType);
const dataset = this.props.dataset;
const backURL = this.props.location.pathname.substring(0, this.props.location.pathname.lastIndexOf('/'));
if (_.isEmpty(dataset)) {
return (
<div className='study-viewer'>
<BackLink url={backURL} label='Back' />
<Result
title='No data available'
/>
</div>
);
}
return (
<div className='study-viewer'>
<BackLink url={backURL} label='Back' />
<Space className='study-viewer__space' direction='vertical'>
<div className='study-viewer__title'>
<Title level={4}>{dataset.title}</Title>
</div>
<div className='study-viewer__details'>
<ReduxStudyDetails
data={dataset}
fileData={this.props.fileData}
studyViewerConfig={studyViewerConfig}
/>
<div className='study-viewer__details-sidebar'>
<Space direction='vertical' style={{ width: '100%' }}>
{(this.props.docData.length > 0) ?
<div className='study-viewer__details-sidebar-box'>
<Space className='study-viewer__details-sidebar-space' direction='vertical'>
<div className='h3-typo'>Study Documents</div>
{this.props.docData.map((doc) => {
const iconComponent = (doc.data_format === 'PDF') ? <FilePdfOutlined /> : <FileOutlined />;
const linkText = `${doc.file_name} (${doc.data_format} - ${humanFileSize(doc.file_size)})`;
const linkComponent = <a href={doc.doc_url}>{linkText}</a>;
return (<div key={doc.file_name}>
{iconComponent}
{linkComponent}
</div>);
})}
</Space>
</div>
: null
}
</Space>
</div>
</div>
</Space>
</div>
);
}
}

SingleStudyViewer.propTypes = {
dataset: PropTypes.object,
docData: PropTypes.array,
fileData: PropTypes.array,
noConfigError: PropTypes.string,
history: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
};

SingleStudyViewer.defaultProps = {
dataset: undefined,
docData: [],
fileData: [],
noConfigError: undefined,
};

export default SingleStudyViewer;
75 changes: 75 additions & 0 deletions src/StudyViewer/StudyCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Card, Collapse } from 'antd';
import { PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons';
import { ReduxStudyDetails } from './reduxer';
import './StudyViewer.css';

const { Panel } = Collapse;

class StudyCard extends React.Component {
constructor(props) {
super(props);
this.state = {
panelExpanded: props.initialPanelExpandStatus,
};
}

onCollapseChange = () => {
this.setState(prevState => ({
panelExpanded: !prevState.panelExpanded,
}));
};

render() {
return (
<Card
className='study-viewer__card'
title={this.props.data.title}
>
<Collapse
defaultActiveKey={(this.state.panelExpanded) ? ['1'] : []}
expandIcon={({ isActive }) =>
((isActive) ? <MinusCircleOutlined /> : <PlusCircleOutlined />)}
onChange={this.onCollapseChange}
ghost
>
<Panel
header={(this.state.panelExpanded) ? 'Hide details' : 'Show details'}
key='1'
>
<ReduxStudyDetails
data={this.props.data}
fileData={this.props.fileData}
studyViewerConfig={this.props.studyViewerConfig}
displayLearnMoreBtn
/>
</Panel>
</Collapse>
</Card>
);
}
}

StudyCard.propTypes = {
data: PropTypes.shape({
accessRequested: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
rowAccessorValue: PropTypes.string.isRequired,
blockData: PropTypes.object,
tableData: PropTypes.object,
accessibleValidationValue: PropTypes.string,
fileData: PropTypes.array,
docData: PropTypes.array,
}).isRequired,
fileData: PropTypes.array,
studyViewerConfig: PropTypes.object,
initialPanelExpandStatus: PropTypes.bool.isRequired,
};

StudyCard.defaultProps = {
fileData: [],
studyViewerConfig: {},
};

export default StudyCard;
Loading

0 comments on commit 7b8324f

Please sign in to comment.