-
Notifications
You must be signed in to change notification settings - Fork 3
Open file selections and metadata in Vol-E v2 #621
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
base: main
Are you sure you want to change the base?
Changes from all commits
c67849b
84fe81e
db600bb
3e20869
a709ee5
b802a85
111c4da
6d6a32f
eea223c
f3e9aa6
d9b71a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import { ContextualMenuItemType, DefaultButton, Icon, IContextualMenuItem } from | |
| import { isEmpty } from "lodash"; | ||
| import * as React from "react"; | ||
| import { useDispatch, useSelector } from "react-redux"; | ||
| import { v4 as uuidv4 } from "uuid"; | ||
|
|
||
| import AnnotationName from "../../entity/Annotation/AnnotationName"; | ||
| import FileDetail from "../../entity/FileDetail"; | ||
|
|
@@ -38,6 +39,7 @@ interface Apps { | |
|
|
||
| type AppOptions = { | ||
| openInCfe: () => void; | ||
| openInVole: () => void; | ||
| }; | ||
|
|
||
| const SUPPORTED_APPS_HEADER = { | ||
|
|
@@ -160,7 +162,7 @@ const APPS = ( | |
| key: AppKeys.VOLE, | ||
| text: "Vol-E", | ||
| title: `Open files with Vol-E`, | ||
| href: `https://volumeviewer.allencell.org/viewer?url=${fileDetails?.path}/`, | ||
| onClick: options?.openInVole, | ||
| disabled: !fileDetails?.path, | ||
| target: "_blank", | ||
| onRenderContent(props, defaultRenders) { | ||
|
|
@@ -205,6 +207,12 @@ const APPS = ( | |
| } as IContextualMenuItem, | ||
| }); | ||
|
|
||
| type VoleMessage = { | ||
| scenes?: string[]; | ||
| meta: Record<string, Record<string, unknown>>; | ||
| sceneIndex?: number; | ||
| }; | ||
|
|
||
| function getSupportedApps( | ||
| apps: Apps, | ||
| isSmallFile: boolean, | ||
|
|
@@ -267,6 +275,44 @@ function getFileExtension(fileDetails: FileDetail): string { | |
| return fileDetails.path.slice(fileDetails.path.lastIndexOf(".") + 1).toLowerCase(); | ||
| } | ||
|
|
||
| /** | ||
| * Opens a window at `openUrl`, then attempts to send the data in `entry` to it. | ||
| * | ||
| * This requires a bit of protocol to accomplish: | ||
| * 1. We add some query params to `openUrl` before opening: `msgorigin` is this site's origin for | ||
| * validation, and `storageid` uniquely identifies the message we want to send. | ||
| * 2. *The opened window must check if these params are present* and post the value of `storageid` | ||
| * back to us (via `window.opener`, validated using `msgorigin`) once it's loaded and ready. | ||
| * 3. Once we receive that message, we send over `entry`. | ||
| * | ||
| * This is currently only used by `openInVole`, but is broken out into a separate function to | ||
| * emphasize that this protocol is both message- and receiver-agnostic, and could be used to send | ||
| * large bundles of data to other apps as well. | ||
| */ | ||
| function openWindowWithMessage(openUrl: URL, message: any): void { | ||
| if (message === undefined || message === null) { | ||
| window.open(openUrl); | ||
| return; | ||
| } | ||
|
|
||
| const storageid = uuidv4(); | ||
| openUrl.searchParams.append("msgorigin", window.location.origin); | ||
| openUrl.searchParams.append("storageid", storageid); | ||
|
|
||
| const handle = window.open(openUrl); | ||
| const loadHandler = (event: MessageEvent): void => { | ||
| if (event.origin !== openUrl.origin || event.data !== storageid) { | ||
| return; | ||
| } | ||
| handle?.postMessage(message, openUrl.origin); | ||
| window.removeEventListener("message", loadHandler); | ||
| }; | ||
|
|
||
| window.addEventListener("message", loadHandler); | ||
| // ensure handlers can't build up with repeated failed requests | ||
| window.setTimeout(() => window.removeEventListener("message", loadHandler), 60000); | ||
| } | ||
|
|
||
| export default (fileDetails?: FileDetail, filters?: FileFilter[]): IContextualMenuItem[] => { | ||
| const path = fileDetails?.path; | ||
| const size = fileDetails?.size; | ||
|
|
@@ -299,6 +345,67 @@ export default (fileDetails?: FileDetail, filters?: FileFilter[]): IContextualMe | |
| fileService | ||
| ); | ||
|
|
||
| // custom hook this, like `useOpenInCfe`? | ||
| const openInVole = React.useCallback(async (): Promise<void> => { | ||
|
Comment on lines
+348
to
+349
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't end up breaking out a custom hook for this because:
Any of the above could fall the other way for someone else or change in the future though, so I also kept the comment. |
||
| const VOLE_BASE_URL = "https://vole.allencell.org/viewer"; | ||
|
|
||
| const allDetails = await fileSelection.fetchAllDetails(); | ||
| const details = allDetails.filter((detail) => { | ||
| const fileExt = getFileExtension(detail); | ||
| return fileExt === "zarr" || fileExt === ""; | ||
| }); | ||
|
|
||
| const scenes: string[] = []; | ||
| const message: VoleMessage = { meta: {} }; | ||
|
|
||
| for (const detail of details) { | ||
| const sceneMeta: Record<string, unknown> = {}; | ||
| for (const annotation of detail.annotations) { | ||
| const isSingleValue = annotation.values.length === 1; | ||
| const value = isSingleValue ? annotation.values[0] : annotation.values; | ||
| sceneMeta[annotation.name] = value; | ||
| } | ||
| scenes.push(detail.path); | ||
| if (Object.keys(sceneMeta).length > 0) { | ||
| message.meta[detail.path] = sceneMeta; | ||
| } | ||
| } | ||
|
|
||
| const openUrl = new URL(VOLE_BASE_URL); | ||
|
|
||
| // Start on the focused scene | ||
| const sceneIndex = details.findIndex((detail) => detail.path === fileDetails?.path); | ||
|
|
||
| // Prefer putting the image URLs directly in the query string for easy sharing, if the | ||
| // length of the URL would be reasonable | ||
| const includeUrls = | ||
| details.length < 5 || | ||
| details.reduce((acc, detail) => acc + detail.path.length + 1, 0) <= 250; | ||
|
|
||
| if (includeUrls) { | ||
| // We can fit all the URLs we want! | ||
| openUrl.searchParams.append("url", details.map(({ path }) => path).join("+")); | ||
| if (sceneIndex > 0) { | ||
| openUrl.searchParams.append("scene", sceneIndex.toString()); | ||
| } | ||
| } else { | ||
| // There are more scene URLs than we want to put in the full URL. We need to send them over as a message. | ||
| // Include only the URL of the focused scene, so the link is usable even if the message fails. | ||
| const initialImageUrl = details[Math.max(sceneIndex, 0)].path; | ||
| openUrl.searchParams.append("url", initialImageUrl); | ||
| message.scenes = scenes; | ||
| if (sceneIndex > 0) { | ||
| message.sceneIndex = sceneIndex; | ||
| } | ||
| } | ||
|
|
||
| if (includeUrls && Object.keys(message.meta).length === 0) { | ||
| window.open(openUrl); | ||
| } else { | ||
| openWindowWithMessage(openUrl, message); | ||
| } | ||
| }, [fileDetails, fileSelection]); | ||
|
|
||
| const plateLink = fileDetails?.getLinkToPlateUI(loadBalancerBaseUrl); | ||
| const annotationNameToLinkMap = React.useMemo( | ||
| () => | ||
|
|
@@ -365,7 +472,7 @@ export default (fileDetails?: FileDetail, filters?: FileFilter[]): IContextualMe | |
| }) | ||
| .sort((a, b) => (a.text || "").localeCompare(b.text || "")); | ||
|
|
||
| const apps = APPS(fileDetails, { openInCfe }); | ||
| const apps = APPS(fileDetails, { openInCfe, openInVole }); | ||
|
|
||
| // Determine is the file is small or not asynchronously | ||
| React.useEffect(() => { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was already present somewhere further down the dependency tree, so depending on it explicitly doesn't change much.