Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

John conroy/publication vignettes #3065

Merged
merged 29 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b2ea017
Add first pass of loading vignettes yaml from assets
john-conroy Mar 26, 2023
2af4559
Add mark's util to fill url and token in vit conf
john-conroy Mar 26, 2023
6ce4dfe
Pass vignette data to publication
john-conroy Mar 26, 2023
f4bcc5f
Add wip visualization to publication
john-conroy Mar 26, 2023
7c9094a
Merge branch 'main' into john-conroy/publication-vignettes
john-conroy Mar 26, 2023
bedf2fd
Add request init handler to util for zarr files
john-conroy Mar 27, 2023
08cbdda
Rename state variable
john-conroy Mar 27, 2023
842573c
Move publication vignette to own component
john-conroy Mar 27, 2023
d661c4b
Loop over vignette data
john-conroy Mar 27, 2023
3e94beb
Add vignette accordions
john-conroy Mar 27, 2023
ac8fe19
Open first vignette by default
john-conroy Mar 27, 2023
74fc105
Update vignette accordion title
john-conroy Mar 27, 2023
833a722
Fix vignette transition prop
john-conroy Mar 27, 2023
5d0a83a
Install react-markdown
john-conroy Mar 27, 2023
9599bfe
Add markdown description to vignette data
john-conroy Mar 27, 2023
1a67d92
Display md description in vignette
john-conroy Mar 27, 2023
5d54318
Add primary color accordion summary
john-conroy Mar 27, 2023
307e573
Control accordions
john-conroy Mar 27, 2023
e2bd434
Use styled component instead of inline styles
john-conroy Mar 27, 2023
2c88a7b
Remove header from each visualization
john-conroy Mar 27, 2023
60c16f6
Add visualizations section to sidebar
john-conroy Mar 27, 2023
bf8fe16
Add changelog
john-conroy Mar 27, 2023
6e9f231
Fix some python linting issues
john-conroy Mar 27, 2023
0047781
Dont use bare except
john-conroy Mar 27, 2023
a2b59d8
Remove extra dir
john-conroy Mar 27, 2023
b268d26
More python linting
john-conroy Mar 27, 2023
22acb0d
Fix file path
john-conroy Mar 27, 2023
6b96c51
Fix prop type
john-conroy Mar 27, 2023
d992440
Handle mutliple figures passed to vitessce
john-conroy Mar 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG-publication-vignettes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add visualization vignettes section to publication page.
55 changes: 54 additions & 1 deletion context/app/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from flask import abort, current_app
import requests

import frontmatter
from hubmap_commons.type_client import TypeClient

from .client_utils import files_from_response
Expand Down Expand Up @@ -209,6 +209,55 @@ def get_assay(name):
vitessce_conf=vitessce_conf,
vis_lifted_uuid=vis_lifted_uuid)

def _file_request(self, url, body_json=None):
headers = {'Authorization': 'Bearer ' + self.groups_token} if self.groups_token else {}

if self.groups_token:
url += f"?token={self.groups_token}"
try:
response = (
requests.get(url, headers=headers)
)
except requests.exceptions.ConnectTimeout as error:
current_app.logger.error(error)
abort(504)
try:
response.raise_for_status()
except requests.exceptions.HTTPError as error:
current_app.logger.error(error.response.text)
status = error.response.status_code
if status in [400, 404]:
# The same 404 page will be returned,
# whether it's a missing route in portal-ui,
# or a missing entity in the API.
abort(status)
if status in [401]:
# I believe we have 401 errors when the globus credentials
# have expired, but are still in the flask session.
abort(status)
raise
return response.text

def get_publication_vignettes(self, uuid):
vignettes_path = f"{current_app.config['ASSETS_ENDPOINT']}/{uuid}/vignettes/"
vignette_data = {}

i = 1
while True:
try:
vignette_dir_name = _get_vignette_dir_name(i)
description_text = self._file_request(
f"{vignettes_path}/{vignette_dir_name}/description.md")
metadata_content = frontmatter.loads(description_text)
vignette_data[vignette_dir_name] = {
**metadata_content.metadata,
'vignette_description_md': metadata_content.content}
i += 1
except Exception:
break

return vignette_data


def _make_query(constraints, uuids):
'''
Expand Down Expand Up @@ -463,3 +512,7 @@ def _get_latest_uuid(revisions):
]
return max(clean_revisions,
key=lambda revision: revision['revision_number'])['uuid']


def _get_vignette_dir_name(vignette_number):
return f"vignette_{vignette_number:02}"
3 changes: 3 additions & 0 deletions context/app/routes_browse.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ def details(type, uuid):
'vis_lifted_uuid': conf_cells_uuid.vis_lifted_uuid
})

if type == 'publication':
flask_data.update({'vignette_data': client.get_publication_vignettes(uuid)})

template = 'base-pages/react-content.html'
return render_template(
template,
Expand Down
4 changes: 3 additions & 1 deletion context/app/static/js/components/Routes/Routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function Routes({ flaskData }) {
organs,
organs_count,
organ,
vignette_data,
} = flaskData;
const urlPath = window.location.pathname;
const url = window.location.href;
Expand Down Expand Up @@ -100,7 +101,7 @@ function Routes({ flaskData }) {
if (urlPath.startsWith('/browse/publication/')) {
return (
<Route>
<Publication publication={entity} />
<Publication publication={entity} vignette_data={vignette_data} />
</Route>
);
}
Expand Down Expand Up @@ -296,6 +297,7 @@ Routes.propTypes = {
organs: PropTypes.object,
metadata: PropTypes.object,
organs_count: PropTypes.number,
vignette_data: PropTypes.object,
}),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,13 @@ const StyledFooterText = styled(Typography)`
`;

const StyledDetailPageSection = styled(DetailPageSection)`
width: 100%;
${(props) =>
props.$vizIsFullscreen &&
css`
z-index: ${props.theme.zIndex.visualization};
position: relative;
`}
`};
`;

const bodyExpandedCSS = css`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useContext, useEffect, useState } from 'react';
import ReactMarkdown from 'react-markdown';

import { AppContext } from 'js/components/Providers';
import VisualizationWrapper from 'js/components/detailPage/visualization/VisualizationWrapper';

import { fillUrls } from './utils';

async function fetchVitessceConf({ assetsEndpoint, uuid, filePath, groupsToken, vignetteDirName }) {
const urlHandler = (url, isZarr) => {
return `${url.replace('{{ base_url }}', `${assetsEndpoint}/${uuid}/data`)}${isZarr ? '' : `?token=${groupsToken}`}`;
};

const requestInitHandler = () => {
return {
headers: { Authorization: `Bearer ${groupsToken}` },
};
};
const response = await fetch(
`${assetsEndpoint}/${uuid}/vignettes/${vignetteDirName}/${filePath}?token=${groupsToken}`,
{
headers: {
'Content-Type': 'application/json',
},
},
);

if (!response.ok) {
console.error('Assets API failed', response);
return undefined;
}
const conf = await response.json();
return fillUrls(conf, urlHandler, requestInitHandler);
}

function PublicationVignette({ vignette, vignetteDirName, uuid }) {
const { assetsEndpoint, groupsToken } = useContext(AppContext);

const [vitessceConfs, setVitessceConfs] = useState(undefined);

useEffect(() => {
async function getAndSetVitessceConf() {
const figuresConfs = await Promise.all(
vignette.figures.map((figure) => {
return fetchVitessceConf({ assetsEndpoint, uuid, filePath: figure.file, groupsToken, vignetteDirName });
}),
);
setVitessceConfs(figuresConfs);
}
getAndSetVitessceConf();
}, [assetsEndpoint, groupsToken, uuid, vignette.figures, vignetteDirName]);

if (vitessceConfs) {
return (
<>
<ReactMarkdown>{vignette.vignette_description_md}</ReactMarkdown>
<VisualizationWrapper
vitData={vitessceConfs.length === 1 ? vitessceConfs[0] : vitessceConfs}
uuid={uuid}
hasNotebook={false}
shouldDisplayHeader={false}
/>
</>
);
}

return null;
}

export default PublicationVignette;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import PublicationVignette from './PublicationVignette';

export default PublicationVignette;
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* @param {object} config The "template" config containing URLs with "{{ base_url }}".
* @param {function} handleUrl Function that takes in a (potentially template) URL string and returns a filled-in URL string.
* @returns {object} The config with handleUrl having been called to process every URL.
*/
const fillUrls = (config, handleUrl, handleRequestInit) => {
return {
...config,
datasets: config.datasets.map((datasetDef) => {
return {
...datasetDef,
files: datasetDef.files.map((fileDef) => {
return {
...fileDef,
...(fileDef.url
? {
url: handleUrl(fileDef.url, fileDef.fileType.includes('zarr')),
}
: {}),
...(fileDef.fileType.includes('zarr')
? {
requestInit: handleRequestInit(),
}
: {}),
...(fileDef.options?.images
? {
options: {
...fileDef.options,
images: fileDef.options.images.map((imageDef) => {
return {
...imageDef,
url: handleUrl(imageDef.url, false),
...(imageDef.metadata?.omeTiffOffsetsUrl
? {
metadata: {
...imageDef.metadata,
omeTiffOffsetsUrl: handleUrl(imageDef.metadata.omeTiffOffsetsUrl, false),
},
}
: {}),
};
}),
},
}
: {}),
};
}),
};
}),
};
};

export { fillUrls };
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useState } from 'react';
import Accordion from '@material-ui/core/ExpansionPanel';
import Typography from '@material-ui/core/Typography';
import ArrowDropUpRoundedIcon from '@material-ui/icons/ArrowDropUpRounded';

import SectionHeader from 'js/shared-styles/sections/SectionHeader';
import { DetailPageSection } from 'js/components/detailPage/style';
import PublicationVignette from 'js/components/publications/PublicationVignette';
import PrimaryColorAccordionSummary from 'js/shared-styles/accordions/PrimaryColorAccordionSummary';
import { StyledAccordionDetails } from './style';

function PublicationsVisualizationSection({ vignette_data, uuid }) {
const [expandedIndex, setExpandedIndex] = useState(0);

const handleChange = (i) => (event, isExpanded) => {
setExpandedIndex(isExpanded ? i : false);
};

return (
<DetailPageSection id="visualizations">
<SectionHeader>Visualizations</SectionHeader>
{Object.entries(vignette_data).map(([k, v], i) => {
return (
<Accordion
key={k}
expanded={i === expandedIndex}
TransitionProps={{ mountOnEnter: i !== 0 }}
onChange={handleChange(i)}
>
<PrimaryColorAccordionSummary $isExpanded={i === expandedIndex} expandIcon={<ArrowDropUpRoundedIcon />}>
<Typography variant="subtitle1">{`Vignette ${i + 1}: ${v.name}`}</Typography>
</PrimaryColorAccordionSummary>
<StyledAccordionDetails>
<PublicationVignette vignette={v} uuid={uuid} vignetteDirName={k} />;
</StyledAccordionDetails>
</Accordion>
);
})}
</DetailPageSection>
);
}

export default PublicationsVisualizationSection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import PublicationsVisualizationSection from './PublicationsVisualizationSection';

export default PublicationsVisualizationSection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import styled from 'styled-components';

import AccordionDetails from '@material-ui/core/AccordionDetails';

const StyledAccordionDetails = styled(AccordionDetails)`
flex-direction: column;
`;

export { StyledAccordionDetails };
6 changes: 4 additions & 2 deletions context/app/static/js/pages/Publication/Publication.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import OutboundIconLink from 'js/shared-styles/Links/iconLinks/OutboundIconLink'
import { getCombinedDatasetStatus, getSectionOrder } from 'js/components/detailPage/utils';
import ContributorsTable from 'js/components/detailPage/ContributorsTable/ContributorsTable';
import PublicationsDataSection from 'js/components/publications/PublicationsDataSection';
import PublicationsVisualizationSection from 'js/components/publications/PublicationVisualizationsSection/';
import ProvSection from 'js/components/detailPage/provenance/ProvSection';
import DetailLayout from 'js/components/detailPage/DetailLayout';
import useEntityStore from 'js/stores/useEntityStore';

const entityStoreSelector = (state) => state.setAssayMetadata;

function Publication({ publication }) {
function Publication({ publication, vignette_data }) {
const {
title,
uuid,
Expand All @@ -35,7 +36,7 @@ function Publication({ publication }) {
const setAssayMetadata = useEntityStore(entityStoreSelector);
setAssayMetadata({ hubmap_id, entity_type, title, publication_venue });

const sectionOrder = getSectionOrder(['summary', 'data', 'authors', 'provenance'], {});
const sectionOrder = getSectionOrder(['summary', 'data', 'visualizations', 'authors', 'provenance'], {});

const combinedStatus = getCombinedDatasetStatus({ sub_status, status });

Expand All @@ -60,6 +61,7 @@ function Publication({ publication }) {
{hasDOI && <OutboundIconLink href={doi_url}>{doi_url}</OutboundIconLink>}
</Summary>
<PublicationsDataSection uuid={uuid} datasetUUIDs={ancestor_ids} />
<PublicationsVisualizationSection vignette_data={vignette_data} uuid={uuid} />
<ContributorsTable contributors={contributors} title="Authors" />
<ProvSection uuid={uuid} assayMetadata={publication} />
</DetailLayout>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { PrimaryColorAccordionSummary } from './style';

export default PrimaryColorAccordionSummary;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import styled, { css } from 'styled-components';

import AccordionSummary from '@material-ui/core/ExpansionPanelSummary';

const PrimaryColorAccordionSummary = styled(AccordionSummary)`
${(props) =>
props.$isExpanded &&
css`
background-color: ${props.$isExpanded ? props.theme.palette.primary.main : '#E0E0E0'};

div > * {
color: ${props.$isExpanded ? '#fff' : props.theme.palette.text.primary};
}
`}
`;

export { PrimaryColorAccordionSummary };
Loading