Skip to content

Commit

Permalink
NickAkhmetov / HMP-235 Add support for text-only publication vignettes (
Browse files Browse the repository at this point in the history
#3159)

* HMP-235 Add support for text-only publication vignettes

* Explanatory comments

* Revert contexts to `jsx` for now to avoid breaking storybook

* Update CHANGELOG-hmp-235.md

Co-authored-by: L Choy <[email protected]>

* Review fixes
- Account for potentially undefined/null vignettes (no guarantee that it'll be an empty list)
- Move multiFetcher to shared utils folder

* Don't use fallback value for urls so `useSWR` properly handles conditional fetch

---------

Co-authored-by: L Choy <[email protected]>
  • Loading branch information
NickAkhmetov and lchoy authored Jul 12, 2023
1 parent f675369 commit ca6964b
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 83 deletions.
1 change: 1 addition & 0 deletions CHANGELOG-hmp-235.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add support for text-only publication vignettes without Vitessce visualizations.
19 changes: 16 additions & 3 deletions context/app/static/js/components/Contexts.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import React, { useContext } from 'react';
import { useContext, createContext } from 'react';

const FlaskDataContext = React.createContext({});
const AppContext = React.createContext({});
// TODO:
// I tried converting this to a .tsx file, but it made storybook fail; created HMP-250 to track this
// We should continue specifying shapes of contexts as we start using them in our TS files
const FlaskDataContext = createContext({});

/**
* @typedef AppContextType
* @property {string} assetsEndpoint
* @property {string} groupsToken
*/

/**
* @type {AppContextType}
*/
const AppContext = createContext({});

FlaskDataContext.displayName = 'FlaskDataContext';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,88 +1,26 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import ReactMarkdown from 'react-markdown';

import { useAppContext } from 'js/components/Contexts';
import VisualizationWrapper from 'js/components/detailPage/visualization/VisualizationWrapper';
import useStickyToggle from 'js/hooks/useStickyToggle';

import { fillUrls } from './utils';

async function fetchVitessceConf({ assetsEndpoint, uuid, filePath, groupsToken, vignetteDirName, signal }) {
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}`,
{
signal,
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 useVitessceConfs(assetsEndpoint, groupsToken, uuid, vignette, vignetteDirName) {
const [vitessceConfs, setVitessceConfs] = useState(undefined);

useEffect(() => {
const abortController = new AbortController();
async function getAndSetVitessceConf() {
const figuresConfs = await Promise.all(
vignette.figures.map((figure) => {
return fetchVitessceConf({
assetsEndpoint,
uuid,
filePath: figure.file,
groupsToken,
vignetteDirName,
signal: abortController.signal,
});
}),
);
setVitessceConfs(figuresConfs);
}
// Only fetch the vitessce confs if they haven't been fetched yet
if (!vitessceConfs) {
getAndSetVitessceConf();
return () => {
abortController.abort();
};
}
return () => {
// Do nothing if the vitessce confs have already been fetched
};
}, [assetsEndpoint, groupsToken, uuid, vignette.figures, vignetteDirName, vitessceConfs]);

return vitessceConfs;
}
import { usePublicationVignetteConfs } from './hooks';

function PublicationVignette({ vignette, vignetteDirName, uuid, mounted }) {
const { assetsEndpoint, groupsToken } = useAppContext();

const vitessceConfs = useVitessceConfs(assetsEndpoint, groupsToken, uuid, vignette, vignetteDirName);
const vitessceConfs = usePublicationVignetteConfs({
uuid,
vignette,
vignetteDirName,
});

// Workaround to make the visualization render only after the accordion section has been expanded while
// still letting the prerequisites for the visualizations prefetch
const hasBeenMounted = useStickyToggle(mounted);

if (vitessceConfs) {
return (
<>
<ReactMarkdown>{vignette.description}</ReactMarkdown>
return (
<>
<ReactMarkdown>{vignette.description}</ReactMarkdown>
{vitessceConfs && (
<VisualizationWrapper
vitData={vitessceConfs.length === 1 ? vitessceConfs[0] : vitessceConfs}
uuid={uuid}
Expand All @@ -91,11 +29,9 @@ function PublicationVignette({ vignette, vignetteDirName, uuid, mounted }) {
hasBeenMounted={hasBeenMounted}
isPublicationPage
/>
</>
);
}

return null;
)}
</>
);
}

export default PublicationVignette;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import useSWR from 'swr';
import { useAppContext } from 'js/components/Contexts';
import { multiFetcher } from 'js/helpers/multiFetcher';
import { fillUrls } from './utils';
import { PublicationVignette } from '../types';

type PublicationVignetteConfsInput = {
uuid: string;
vignette: PublicationVignette;
vignetteDirName: string;
};

export function usePublicationVignetteConfs({ uuid, vignetteDirName, vignette }: PublicationVignetteConfsInput) {
const { assetsEndpoint, groupsToken } = useAppContext();
// Extract file paths from the vignette object to form the urls to fetch for this vignette
const urls = vignette.figures?.map(
({ file }) => `${assetsEndpoint}/${uuid}/vignettes/${vignetteDirName}/${file}?token=${groupsToken}`,
);
const { data } = useSWR(urls, multiFetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
});

if (data) {
const urlHandler = (url: string, isZarr: boolean) => {
return `${url.replace('{{ base_url }}', `${assetsEndpoint}/${uuid}/data`)}${
isZarr ? '' : `?token=${groupsToken}`
}`;
};

const requestInitHandler = () => {
return {
headers: { Authorization: `Bearer ${groupsToken}` },
};
};
// Formats the vitessce config data to replace the {{ base_url }} placeholder with the actual url.
// TODO: Improve this `unknown`; I couldn't figure out how to import the appropriate `VitessceConfig` type from Vitessce.
const formattedData: unknown[] = data.map((d) => fillUrls(d, urlHandler, requestInitHandler));
return formattedData;
}
return undefined;
}
11 changes: 11 additions & 0 deletions context/app/static/js/components/publications/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type VignetteFigure = {
file: string;
name: string;
};

export type PublicationVignette = {
name: string;
description: string;
figures?: VignetteFigure[];
directory_name: string;
};
5 changes: 5 additions & 0 deletions context/app/static/js/helpers/multiFetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Fetcher function that lets SWR fetch multiple urls at once
export const multiFetcher = (...urls: string[]) => {
const f = (url: string) => fetch(url).then((response) => response.json());
return Promise.all(urls.map((url) => f(url)));
};
4 changes: 2 additions & 2 deletions context/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"sourceMap": true,
"noEmit": true,
"paths": {
"@/*": [
"./app/static/js/*"
"js/*": [
"./*"
]
}
},
Expand Down

0 comments on commit ca6964b

Please sign in to comment.