From 12da14ddbbea68a4b95a6dffc6e81f0404d35e04 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Wed, 10 Apr 2024 17:01:40 -0700 Subject: [PATCH 01/26] feat: Added react-router-dom --- package-lock.json | 39 +++++++++++++++++++++++++++++++++++++++ package.json | 1 + public/index.tsx | 23 +++++++++++++++++++---- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index c26cd448..4471644d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "lodash": "^4.17.20", "nouislider-react": "^3.4.0", "react-color": "^2.19.3", + "react-router-dom": "^6.22.3", "styled-components": "^6.1.8" }, "devDependencies": { @@ -4639,6 +4640,14 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "dev": true }, + "node_modules/@remix-run/router": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", + "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", @@ -18654,6 +18663,36 @@ "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", "dev": true }, + "node_modules/react-router": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", + "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", + "dependencies": { + "@remix-run/router": "1.15.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", + "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", + "dependencies": { + "@remix-run/router": "1.15.3", + "react-router": "6.22.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-slick": { "version": "0.25.2", "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.25.2.tgz", diff --git a/package.json b/package.json index 818c85c5..a60628be 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "lodash": "^4.17.20", "nouislider-react": "^3.4.0", "react-color": "^2.19.3", + "react-router-dom": "^6.22.3", "styled-components": "^6.1.8" }, "peerDependencies": { diff --git a/public/index.tsx b/public/index.tsx index 54bceb88..ac659a73 100644 --- a/public/index.tsx +++ b/public/index.tsx @@ -1,15 +1,17 @@ +import FirebaseRequest, { DatasetMetaData } from "./firebase"; import React from "react"; import ReactDOM from "react-dom"; +import { createBrowserRouter } from "react-router-dom"; import "antd/dist/antd.less"; // Components -import "../src/aics-image-viewer/assets/styles/typography.css"; -import "./App.css"; - import { ImageViewerApp, RenderMode, ViewerChannelSettings, ViewMode } from "../src"; -import FirebaseRequest, { DatasetMetaData } from "./firebase"; import { AppProps, GlobalViewerSettings } from "../src/aics-image-viewer/components/App/types"; +import LandingPage from "../src/landing-page/components/LandingPage"; +import { ViewerArgs } from "../src/landing-page/types"; +import "../src/aics-image-viewer/assets/styles/typography.css"; +import "./App.css"; // vars filled at build time using webpack DefinePlugin console.log(`website-3d-cell-viewer ${WEBSITE3DCELLVIEWER_BUILD_ENVIRONMENT} build`); @@ -254,6 +256,19 @@ if (params) { runApp(); } +const router = createBrowserRouter([ + { + path: "/", + element: ( + + ), + }, +]); + function runApp() { ReactDOM.render( , From f3e48cde6abf00cc0ef05b225b0f359eb93e7ada Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Wed, 10 Apr 2024 17:14:27 -0700 Subject: [PATCH 02/26] feat: Loading viewer from button click on landing page --- public/index.tsx | 31 ++++++------ src/AppWrapper.tsx | 50 +++++++++++++++++++ .../components/LandingPage/content.ts | 13 +++-- .../components/LandingPage/index.tsx | 22 +++++--- src/landing-page/types.ts | 17 ++----- 5 files changed, 91 insertions(+), 42 deletions(-) create mode 100644 src/AppWrapper.tsx diff --git a/public/index.tsx b/public/index.tsx index ac659a73..e7c8554f 100644 --- a/public/index.tsx +++ b/public/index.tsx @@ -1,15 +1,16 @@ import FirebaseRequest, { DatasetMetaData } from "./firebase"; import React from "react"; import ReactDOM from "react-dom"; -import { createBrowserRouter } from "react-router-dom"; +import { createBrowserRouter, RouterProvider } from "react-router-dom"; import "antd/dist/antd.less"; // Components import { ImageViewerApp, RenderMode, ViewerChannelSettings, ViewMode } from "../src"; +import AppWrapper from "../src/AppWrapper"; import { AppProps, GlobalViewerSettings } from "../src/aics-image-viewer/components/App/types"; import LandingPage from "../src/landing-page/components/LandingPage"; -import { ViewerArgs } from "../src/landing-page/types"; +import StyleProvider from "../src/aics-image-viewer/components/StyleProvider"; import "../src/aics-image-viewer/assets/styles/typography.css"; import "./App.css"; @@ -259,19 +260,19 @@ if (params) { const router = createBrowserRouter([ { path: "/", - element: ( - - ), + element: , + }, + { + path: "viewer", + element: , }, ]); -function runApp() { - ReactDOM.render( - , - document.getElementById("cell-viewer") - ); -} +function runApp() {} +ReactDOM.render( + // , + + + , + document.getElementById("cell-viewer") +); diff --git a/src/AppWrapper.tsx b/src/AppWrapper.tsx new file mode 100644 index 00000000..0ffd32a2 --- /dev/null +++ b/src/AppWrapper.tsx @@ -0,0 +1,50 @@ +import React, { ReactElement, useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { AppProps, GlobalViewerSettings } from "./aics-image-viewer/components/App/types"; +import { ImageViewerApp, RenderMode, ViewMode } from "."; + +type AppWrapperProps = {}; + +const BASE_URL = "https://s3-us-west-2.amazonaws.com/bisque.allencell.org/v1.4.0/Cell-Viewer_Thumbnails/"; +const args: Omit = { + cellId: "2025", + imageUrl: BASE_URL + "AICS-22/AICS-22_8319_2025_atlas.json", + parentImageUrl: BASE_URL + "AICS-22/AICS-22_8319_atlas.json", + parentImageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=F8319", + imageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=C2025", +}; +const DEFAULT_VIEWER_SETTINGS: Partial = { + showAxes: false, + showBoundingBox: false, + autorotate: false, + viewMode: ViewMode.threeD, + renderMode: RenderMode.volumetric, + maskAlpha: 50, + brightness: 70, + density: 50, + levels: [0, 128, 255] as [number, number, number], + backgroundColor: [0, 0, 0] as [number, number, number], + boundingBoxColor: [255, 255, 255] as [number, number, number], +}; + +/** + * Renders additional components around the main ImageViewer component, and also collects URL and navigation state params + * to pass to the viewer. + */ +export default function AppWrapper(props: AppWrapperProps): ReactElement { + const location = useLocation(); + // Collect navigation state params (AppProps) + const locationArgs = location.state as AppProps; + // TODO: Update this with the load parameter later :) + const [viewerProps, setViewerProps] = useState(locationArgs); + console.log(viewerProps); + + return ( + + ); +} diff --git a/src/landing-page/components/LandingPage/content.ts b/src/landing-page/components/LandingPage/content.ts index 614f9eee..eec4e337 100644 --- a/src/landing-page/components/LandingPage/content.ts +++ b/src/landing-page/components/LandingPage/content.ts @@ -1,5 +1,6 @@ import { ProjectEntry } from "../../types"; +const BASE_URL = "https://s3-us-west-2.amazonaws.com/bisque.allencell.org/v1.4.0/Cell-Viewer_Thumbnails/"; export const landingPageContent: ProjectEntry[] = [ { name: "Large colony hiPSC nuclei time series, tagged lamin-B", @@ -9,13 +10,11 @@ export const landingPageContent: ProjectEntry[] = [ publicationName: "This is the name of the associated publication that the user can click to open in a new tab", inReview: true, loadParams: { - baseurl: "", - cellid: 0, - cellPath: "", - fovPath: "", - fovDownloadHref: "", - cellDownloadHref: "", - viewerSettings: { groups: [] }, + cellId: "2025", + imageUrl: BASE_URL + "AICS-22/AICS-22_8319_2025_atlas.json", + parentImageUrl: BASE_URL + "AICS-22/AICS-22_8319_atlas.json", + parentImageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=F8319", + imageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=C2025", }, }, ]; diff --git a/src/landing-page/components/LandingPage/index.tsx b/src/landing-page/components/LandingPage/index.tsx index d3606d8e..41c921bc 100644 --- a/src/landing-page/components/LandingPage/index.tsx +++ b/src/landing-page/components/LandingPage/index.tsx @@ -4,9 +4,11 @@ import { Button, Tooltip } from "antd"; import React, { ReactElement, useEffect, useState } from "react"; import { landingPageContent } from "./content"; -import { DatasetEntry, ProjectEntry, ViewerArgs } from "../../types"; +import { DatasetEntry, ProjectEntry } from "../../types"; import styled from "styled-components"; import { FlexColumnAlignCenter, FlexColumn, FlexRowAlignCenter, VisuallyHidden, FlexRow } from "./utils"; +import { useNavigate } from "react-router"; +import { AppProps } from "../../../aics-image-viewer/components/App/types"; const MAX_CONTENT_WIDTH_PX = 1060; @@ -177,12 +179,20 @@ const InReviewFlag = styled(FlexRowAlignCenter)` } `; -type LandingPageProps = { - load: (args: ViewerArgs) => void; -}; +type LandingPageProps = {}; + +// TODO: Replace this everywhere it's used +type AppPropsNoLayout = Omit; export default function LandingPage(props: LandingPageProps): ReactElement { // Rendering + const navigation = useNavigate(); + + const onClickLoad = (appProps: AppPropsNoLayout): void => { + navigation("/viewer", { + state: appProps, + }); + }; // TODO: Should the load buttons be link elements or buttons? // Currently both the link and the button inside can be tab-selected. @@ -191,7 +201,7 @@ export default function LandingPage(props: LandingPageProps): ReactElement {

{dataset.name}

{dataset.description}

-
@@ -232,7 +242,7 @@ export default function LandingPage(props: LandingPageProps): ReactElement { const loadParams = project.loadParams; const loadButton = loadParams ? (
-
diff --git a/src/landing-page/types.ts b/src/landing-page/types.ts index 3aac4c35..1dfab15e 100644 --- a/src/landing-page/types.ts +++ b/src/landing-page/types.ts @@ -1,20 +1,9 @@ -import { ViewerChannelSettings } from "../aics-image-viewer/shared/utils/viewerChannelSettings"; - -// TBD what URL parameters to include here -export type ViewerArgs = { - baseurl: string; - cellid: number; - cellPath: string; - fovPath: string; - fovDownloadHref: string; - cellDownloadHref: string; - viewerSettings: ViewerChannelSettings; -}; +import { AppProps } from "../aics-image-viewer/components/App/types"; export type DatasetEntry = { name: string; description: string; - loadParams: ViewerArgs; + loadParams: Omit; }; export type ProjectEntry = { @@ -22,7 +11,7 @@ export type ProjectEntry = { description: string; publicationLink?: URL; publicationName?: string; - loadParams?: ViewerArgs; + loadParams?: Omit; datasets?: DatasetEntry[]; inReview?: boolean; }; From 9a4f105d5b149aeb18ffb63a2e493c3edc991879 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Wed, 10 Apr 2024 17:19:42 -0700 Subject: [PATCH 03/26] fix: Load first three channels of the first dataset by default --- src/landing-page/components/LandingPage/content.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/landing-page/components/LandingPage/content.ts b/src/landing-page/components/LandingPage/content.ts index eec4e337..d1a06319 100644 --- a/src/landing-page/components/LandingPage/content.ts +++ b/src/landing-page/components/LandingPage/content.ts @@ -15,6 +15,18 @@ export const landingPageContent: ProjectEntry[] = [ parentImageUrl: BASE_URL + "AICS-22/AICS-22_8319_atlas.json", parentImageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=F8319", imageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=C2025", + viewerChannelSettings: { + groups: [ + // first 3 channels on by default! + { + name: "Channels", + channels: [ + { match: [0, 1, 2], enabled: true }, + { match: "(.+)", enabled: false }, + ], + }, + ], + }, }, }, ]; From c874c556a3a95a502a74d871d033cece1bec0ede Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 10:53:15 -0700 Subject: [PATCH 04/26] fix: Fixed dev server not recognizing subpaths --- webpack.dev.js | 1 + 1 file changed, 1 insertion(+) diff --git a/webpack.dev.js b/webpack.dev.js index 70670afb..8842674c 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -6,6 +6,7 @@ module.exports = (env) => { mode: "development", devtool: "eval-source-map", devServer: { + historyApiFallback: true, open: ["/"], port: 9020, allowedHosts: "all", From 466d5b7595b36aeb28804f898b21358783a6dedc Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 10:54:13 -0700 Subject: [PATCH 05/26] fix: Fixed crash caused by bad size check in Toolbar --- src/aics-image-viewer/components/Toolbar/index.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/aics-image-viewer/components/Toolbar/index.tsx b/src/aics-image-viewer/components/Toolbar/index.tsx index 854ed242..19d24a66 100644 --- a/src/aics-image-viewer/components/Toolbar/index.tsx +++ b/src/aics-image-viewer/components/Toolbar/index.tsx @@ -69,9 +69,12 @@ export default class Toolbar extends React.Component checkSize = debounce((): void => { const { leftRef, centerRef, rightRef, barRef } = this; - const leftRect = leftRef.current!.getBoundingClientRect(); - const centerRect = centerRef.current!.getBoundingClientRect(); - const rightRect = rightRef.current!.getBoundingClientRect(); + if (!leftRef.current || !centerRef.current || !rightRef.current || !barRef.current) { + return; + } + const leftRect = leftRef.current.getBoundingClientRect(); + const centerRect = centerRef.current.getBoundingClientRect(); + const rightRect = rightRef.current.getBoundingClientRect(); // when calculating width required to leave scroll mode, add a bit of extra width to ensure that triggers // for entering and leaving scroll mode never overlap (causing toolbar to rapidly switch when resizing) From 0f1f8e111e29ae6d798757ab5836c3cd5f73cadb Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 10:55:55 -0700 Subject: [PATCH 06/26] fix: Linting, video banner color --- src/AppWrapper.tsx | 13 ++----------- src/landing-page/components/LandingPage/index.tsx | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/AppWrapper.tsx b/src/AppWrapper.tsx index 0ffd32a2..4adf2b64 100644 --- a/src/AppWrapper.tsx +++ b/src/AppWrapper.tsx @@ -1,18 +1,10 @@ import React, { ReactElement, useState } from "react"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useLocation } from "react-router-dom"; import { AppProps, GlobalViewerSettings } from "./aics-image-viewer/components/App/types"; import { ImageViewerApp, RenderMode, ViewMode } from "."; type AppWrapperProps = {}; -const BASE_URL = "https://s3-us-west-2.amazonaws.com/bisque.allencell.org/v1.4.0/Cell-Viewer_Thumbnails/"; -const args: Omit = { - cellId: "2025", - imageUrl: BASE_URL + "AICS-22/AICS-22_8319_2025_atlas.json", - parentImageUrl: BASE_URL + "AICS-22/AICS-22_8319_atlas.json", - parentImageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=F8319", - imageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=C2025", -}; const DEFAULT_VIEWER_SETTINGS: Partial = { showAxes: false, showBoundingBox: false, @@ -36,8 +28,7 @@ export default function AppWrapper(props: AppWrapperProps): ReactElement { // Collect navigation state params (AppProps) const locationArgs = location.state as AppProps; // TODO: Update this with the load parameter later :) - const [viewerProps, setViewerProps] = useState(locationArgs); - console.log(viewerProps); + const [viewerProps] = useState(locationArgs); return ( video { From b8837074d35d00c04c711ce26cfd2d4eacf82c73 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 11:00:23 -0700 Subject: [PATCH 07/26] refactor: Renamed `landing-page` folder to `website` --- public/index.tsx | 6 +++--- src/{ => website/components}/AppWrapper.tsx | 4 ++-- .../components/LandingPage/content.ts | 0 .../components/LandingPage/index.tsx | 0 .../components/LandingPage/utils.tsx | 0 src/{landing-page => website}/types.ts | 0 6 files changed, 5 insertions(+), 5 deletions(-) rename src/{ => website/components}/AppWrapper.tsx (88%) rename src/{landing-page => website}/components/LandingPage/content.ts (100%) rename src/{landing-page => website}/components/LandingPage/index.tsx (100%) rename src/{landing-page => website}/components/LandingPage/utils.tsx (100%) rename src/{landing-page => website}/types.ts (100%) diff --git a/public/index.tsx b/public/index.tsx index e7c8554f..4f35c3c1 100644 --- a/public/index.tsx +++ b/public/index.tsx @@ -6,10 +6,10 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import "antd/dist/antd.less"; // Components -import { ImageViewerApp, RenderMode, ViewerChannelSettings, ViewMode } from "../src"; -import AppWrapper from "../src/AppWrapper"; +import { RenderMode, ViewerChannelSettings, ViewMode } from "../src"; +import AppWrapper from "../src/website/components/AppWrapper"; +import LandingPage from "../src/website/components/LandingPage"; import { AppProps, GlobalViewerSettings } from "../src/aics-image-viewer/components/App/types"; -import LandingPage from "../src/landing-page/components/LandingPage"; import StyleProvider from "../src/aics-image-viewer/components/StyleProvider"; import "../src/aics-image-viewer/assets/styles/typography.css"; import "./App.css"; diff --git a/src/AppWrapper.tsx b/src/website/components/AppWrapper.tsx similarity index 88% rename from src/AppWrapper.tsx rename to src/website/components/AppWrapper.tsx index 4adf2b64..0570fdb1 100644 --- a/src/AppWrapper.tsx +++ b/src/website/components/AppWrapper.tsx @@ -1,7 +1,7 @@ import React, { ReactElement, useState } from "react"; import { useLocation } from "react-router-dom"; -import { AppProps, GlobalViewerSettings } from "./aics-image-viewer/components/App/types"; -import { ImageViewerApp, RenderMode, ViewMode } from "."; +import { AppProps, GlobalViewerSettings } from "../../aics-image-viewer/components/App/types"; +import { ImageViewerApp, RenderMode, ViewMode } from "../.."; type AppWrapperProps = {}; diff --git a/src/landing-page/components/LandingPage/content.ts b/src/website/components/LandingPage/content.ts similarity index 100% rename from src/landing-page/components/LandingPage/content.ts rename to src/website/components/LandingPage/content.ts diff --git a/src/landing-page/components/LandingPage/index.tsx b/src/website/components/LandingPage/index.tsx similarity index 100% rename from src/landing-page/components/LandingPage/index.tsx rename to src/website/components/LandingPage/index.tsx diff --git a/src/landing-page/components/LandingPage/utils.tsx b/src/website/components/LandingPage/utils.tsx similarity index 100% rename from src/landing-page/components/LandingPage/utils.tsx rename to src/website/components/LandingPage/utils.tsx diff --git a/src/landing-page/types.ts b/src/website/types.ts similarity index 100% rename from src/landing-page/types.ts rename to src/website/types.ts From 746f4739a48db8dd0fd4c7e21ed08553960f3801 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 11:41:32 -0700 Subject: [PATCH 08/26] refactor: Moved URL handling code to its own utils file --- public/index.tsx | 240 ------------------------------- src/website/utils/url_utils.ts | 252 +++++++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+), 240 deletions(-) create mode 100644 src/website/utils/url_utils.ts diff --git a/public/index.tsx b/public/index.tsx index 4f35c3c1..d0e112bb 100644 --- a/public/index.tsx +++ b/public/index.tsx @@ -1,4 +1,3 @@ -import FirebaseRequest, { DatasetMetaData } from "./firebase"; import React from "react"; import ReactDOM from "react-dom"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; @@ -19,244 +18,6 @@ console.log(`website-3d-cell-viewer ${WEBSITE3DCELLVIEWER_BUILD_ENVIRONMENT} bui console.log(`website-3d-cell-viewer Version ${WEBSITE3DCELLVIEWER_VERSION}`); console.log(`volume-viewer Version ${VOLUMEVIEWER_VERSION}`); -export const VIEWER_3D_SETTINGS: ViewerChannelSettings = { - groups: [ - { - name: "Observed channels", - channels: [ - { name: "Membrane", match: ["(CMDRP)"], color: "E2CDB3", enabled: true, lut: ["p50", "p98"] }, - { - name: "Labeled structure", - match: ["(EGFP)|(RFPT)"], - color: "6FBA11", - enabled: true, - lut: ["p50", "p98"], - }, - { name: "DNA", match: ["(H3342)"], color: "8DA3C0", enabled: true, lut: ["p50", "p98"] }, - { name: "Bright field", match: ["(100)|(Bright)"], color: "F5F1CB", enabled: false, lut: ["p50", "p98"] }, - ], - }, - { - name: "Segmentation channels", - channels: [ - { - name: "Labeled structure", - match: ["(SEG_STRUCT)"], - color: "E0E3D1", - enabled: false, - lut: ["p50", "p98"], - }, - { name: "Membrane", match: ["(SEG_Memb)"], color: "DD9BF5", enabled: false, lut: ["p50", "p98"] }, - { name: "DNA", match: ["(SEG_DNA)"], color: "E3F4F5", enabled: false, lut: ["p50", "p98"] }, - ], - }, - { - name: "Contour channels", - channels: [ - { name: "Membrane", match: ["(CON_Memb)"], color: "FF6200", enabled: false, lut: ["p50", "p98"] }, - { name: "DNA", match: ["(CON_DNA)"], color: "F7DB78", enabled: false, lut: ["p50", "p98"] }, - ], - }, - ], - // must be the true channel name in the volume data - maskChannelName: "SEG_Memb", -}; - -type ParamKeys = "mask" | "ch" | "luts" | "colors" | "url" | "file" | "dataset" | "id" | "view"; -type Params = { [_ in ParamKeys]?: string }; - -function parseQueryString(): Params { - const pairs = location.search.slice(1).split("&"); - const result = {}; - pairs.forEach((pairString) => { - const pair = pairString.split("="); - result[pair[0]] = decodeURIComponent(pair[1] || ""); - }); - return JSON.parse(JSON.stringify(result)); -} -const params = parseQueryString(); - -const decodeURL = (url: string): string => { - const decodedUrl = decodeURIComponent(url); - return decodedUrl.endsWith("/") ? decodedUrl.slice(0, -1) : decodedUrl; -}; - -/** Try to parse a `string` as a list of 2 or more URLs. Returns `undefined` if the string is not a valid URL list. */ -const tryDecodeURLList = (url: string, delim = ","): string[] | undefined => { - if (!url.includes(delim)) { - return undefined; - } - - const urls = url.split(delim).map((u) => decodeURL(u)); - - // Verify that all urls are valid - for (const u of urls) { - try { - new URL(u); - } catch (_e) { - return undefined; - } - } - - return urls; -}; - -const BASE_URL = "https://s3-us-west-2.amazonaws.com/bisque.allencell.org/v1.4.0/Cell-Viewer_Thumbnails/"; -const args: Omit = { - cellId: "2025", - imageUrl: BASE_URL + "AICS-22/AICS-22_8319_2025_atlas.json", - parentImageUrl: BASE_URL + "AICS-22/AICS-22_8319_atlas.json", - parentImageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=F8319", - imageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=C2025", - viewerChannelSettings: VIEWER_3D_SETTINGS, -}; -const viewerSettings: Partial = { - showAxes: false, - showBoundingBox: false, - autorotate: false, - viewMode: ViewMode.threeD, - renderMode: RenderMode.volumetric, - maskAlpha: 50, - brightness: 70, - density: 50, - levels: [0, 128, 255] as [number, number, number], - backgroundColor: [0, 0, 0] as [number, number, number], - boundingBoxColor: [255, 255, 255] as [number, number, number], -}; - -async function loadDataset(dataset: string, id: string) { - const db = new FirebaseRequest(); - - const datasets = await db.getAvailableDatasets(); - - let datasetMeta: DatasetMetaData | undefined = undefined; - for (const d of datasets) { - const innerDatasets = d.datasets!; - const names = Object.keys(innerDatasets); - const matchingName = names.find((name) => name === dataset); - if (matchingName) { - datasetMeta = innerDatasets[matchingName]; - break; - } - } - if (datasetMeta === undefined) { - console.error(`No matching dataset: ${dataset}`); - return; - } - - const datasetData = await db.selectDataset(datasetMeta.manifest!); - const baseUrl = datasetData.volumeViewerDataRoot + "/"; - args.imageDownloadHref = datasetData.downloadRoot + "/" + id; - // args.fovDownloadHref = datasetData.downloadRoot + "/" + id; - - const fileInfo = await db.getFileInfoByCellId(id); - args.imageUrl = baseUrl + fileInfo!.volumeviewerPath; - args.parentImageUrl = baseUrl + fileInfo!.fovVolumeviewerPath; - runApp(); - - // only now do we have all the data needed -} - -if (params) { - if (params.mask) { - viewerSettings.maskAlpha = parseInt(params.mask, 10); - } - if (params.view) { - const mapping = { - "3D": ViewMode.threeD, - Z: ViewMode.xy, - Y: ViewMode.xz, - X: ViewMode.yz, - }; - const allowedViews = Object.keys(mapping); - if (!allowedViews.includes(params.view)) { - params.view = "3D"; - } - viewerSettings.viewMode = mapping[params.view]; - } - if (params.ch) { - // ?ch=1,2 - // ?luts=0,255,0,255 - // ?colors=ff0000,00ff00 - const initialChannelSettings: ViewerChannelSettings = { - groups: [{ name: "Channels", channels: [] }], - }; - const ch = initialChannelSettings.groups[0].channels; - - const channelsOn = params.ch.split(",").map((numstr) => parseInt(numstr, 10)); - for (let i = 0; i < channelsOn.length; ++i) { - ch.push({ match: channelsOn[i], enabled: true }); - } - // look for luts or color - if (params.luts) { - const luts = params.luts.split(","); - if (luts.length !== ch.length * 2) { - console.log("ILL-FORMED QUERYSTRING: luts must have a min/max for each ch"); - } - for (let i = 0; i < ch.length; ++i) { - ch[i]["lut"] = [luts[i * 2], luts[i * 2 + 1]]; - } - } - if (params.colors) { - const colors = params.colors.split(","); - if (colors.length !== ch.length) { - console.log("ILL-FORMED QUERYSTRING: if colors specified, must have a color for each ch"); - } - for (let i = 0; i < ch.length; ++i) { - ch[i]["color"] = colors[i]; - } - } - args.viewerChannelSettings = initialChannelSettings; - } - if (params.url) { - const imageUrls = tryDecodeURLList(params.url) ?? decodeURL(params.url); - const firstUrl = Array.isArray(imageUrls) ? imageUrls[0] : imageUrls; - - args.cellId = "1"; - args.imageUrl = imageUrls; - // this is invalid for zarr? - args.imageDownloadHref = firstUrl; - args.parentImageUrl = ""; - args.parentImageDownloadHref = ""; - // if json, then use the CFE settings for now. - // (See VIEWER_3D_SETTINGS) - // otherwise turn the first 3 channels on and group them - if (!firstUrl.endsWith("json") && !params.ch) { - args.viewerChannelSettings = { - groups: [ - // first 3 channels on by default! - { - name: "Channels", - channels: [ - { match: [0, 1, 2], enabled: true }, - { match: "(.+)", enabled: false }, - ], - }, - ], - }; - } - runApp(); - } else if (params.file) { - // quick way to load a atlas.json from a special directory. - // - // ?file=relative-path-to-atlas-on-isilon - args.cellId = "1"; - const baseUrl = "http://dev-aics-dtp-001.corp.alleninstitute.org/dan-data/"; - args.imageUrl = baseUrl + params.file; - args.parentImageUrl = baseUrl + params.file; - args.parentImageDownloadHref = ""; - args.imageDownloadHref = ""; - runApp(); - } else if (params.dataset && params.id) { - // ?dataset=aics_hipsc_v2020.1&id=232265 - loadDataset(params.dataset, params.id); - } else { - runApp(); - } -} else { - runApp(); -} - const router = createBrowserRouter([ { path: "/", @@ -268,7 +29,6 @@ const router = createBrowserRouter([ }, ]); -function runApp() {} ReactDOM.render( // , diff --git a/src/website/utils/url_utils.ts b/src/website/utils/url_utils.ts new file mode 100644 index 00000000..a09fa5ca --- /dev/null +++ b/src/website/utils/url_utils.ts @@ -0,0 +1,252 @@ +import FirebaseRequest, { DatasetMetaData } from "./firebase"; + +import { AppProps, GlobalViewerSettings } from "../../aics-image-viewer/components/App/types"; +import { RenderMode, ViewMode } from "../../aics-image-viewer/shared/enums"; +import { ViewerChannelSettings } from "../../aics-image-viewer/shared/utils/viewerChannelSettings"; + +// export const VIEWER_3D_SETTINGS: ViewerChannelSettings = { +// groups: [ +// { +// name: "Observed channels", +// channels: [ +// { name: "Membrane", match: ["(CMDRP)"], color: "E2CDB3", enabled: true, lut: ["p50", "p98"] }, +// { +// name: "Labeled structure", +// match: ["(EGFP)|(RFPT)"], +// color: "6FBA11", +// enabled: true, +// lut: ["p50", "p98"], +// }, +// { name: "DNA", match: ["(H3342)"], color: "8DA3C0", enabled: true, lut: ["p50", "p98"] }, +// { name: "Bright field", match: ["(100)|(Bright)"], color: "F5F1CB", enabled: false, lut: ["p50", "p98"] }, +// ], +// }, +// { +// name: "Segmentation channels", +// channels: [ +// { +// name: "Labeled structure", +// match: ["(SEG_STRUCT)"], +// color: "E0E3D1", +// enabled: false, +// lut: ["p50", "p98"], +// }, +// { name: "Membrane", match: ["(SEG_Memb)"], color: "DD9BF5", enabled: false, lut: ["p50", "p98"] }, +// { name: "DNA", match: ["(SEG_DNA)"], color: "E3F4F5", enabled: false, lut: ["p50", "p98"] }, +// ], +// }, +// { +// name: "Contour channels", +// channels: [ +// { name: "Membrane", match: ["(CON_Memb)"], color: "FF6200", enabled: false, lut: ["p50", "p98"] }, +// { name: "DNA", match: ["(CON_DNA)"], color: "F7DB78", enabled: false, lut: ["p50", "p98"] }, +// ], +// }, +// ], +// // must be the true channel name in the volume data +// maskChannelName: "SEG_Memb", +// }; + +type ParamKeys = "mask" | "ch" | "luts" | "colors" | "url" | "file" | "dataset" | "id" | "view"; +type Params = { [_ in ParamKeys]?: string }; + +function parseQueryString(): Params { + const pairs = location.search.slice(1).split("&"); + const result: Record = {}; + pairs.forEach((pairString) => { + const pair = pairString.split("="); + result[pair[0]] = decodeURIComponent(pair[1] || ""); + }); + return JSON.parse(JSON.stringify(result)); +} + +const decodeURL = (url: string): string => { + const decodedUrl = decodeURIComponent(url); + return decodedUrl.endsWith("/") ? decodedUrl.slice(0, -1) : decodedUrl; +}; + +/** Try to parse a `string` as a list of 2 or more URLs. Returns `undefined` if the string is not a valid URL list. */ +const tryDecodeURLList = (url: string, delim = ","): string[] | undefined => { + if (!url.includes(delim)) { + return undefined; + } + + const urls = url.split(delim).map((u) => decodeURL(u)); + + // Verify that all urls are valid + for (const u of urls) { + try { + new URL(u); + } catch (_e) { + return undefined; + } + } + + return urls; +}; + +// const BASE_URL = "https://s3-us-west-2.amazonaws.com/bisque.allencell.org/v1.4.0/Cell-Viewer_Thumbnails/"; +// const args: Omit = { +// cellId: "2025", +// imageUrl: BASE_URL + "AICS-22/AICS-22_8319_2025_atlas.json", +// parentImageUrl: BASE_URL + "AICS-22/AICS-22_8319_atlas.json", +// parentImageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=F8319", +// imageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=C2025", +// viewerChannelSettings: VIEWER_3D_SETTINGS, +// }; +// const viewerSettings: Partial = { +// showAxes: false, +// showBoundingBox: false, +// autorotate: false, +// viewMode: ViewMode.threeD, +// renderMode: RenderMode.volumetric, +// maskAlpha: 50, +// brightness: 70, +// density: 50, +// levels: [0, 128, 255] as [number, number, number], +// backgroundColor: [0, 0, 0] as [number, number, number], +// boundingBoxColor: [255, 255, 255] as [number, number, number], +// }; + +async function loadDataset(dataset: string, id: string): Promise> { + const db = new FirebaseRequest(); + const args: Partial = {}; + + const datasets = await db.getAvailableDatasets(); + + let datasetMeta: DatasetMetaData | undefined = undefined; + for (const d of datasets) { + const innerDatasets = d.datasets!; + const names = Object.keys(innerDatasets); + const matchingName = names.find((name) => name === dataset); + if (matchingName) { + datasetMeta = innerDatasets[matchingName]; + break; + } + } + if (datasetMeta === undefined) { + console.error(`No matching dataset: ${dataset}`); + return {}; + } + + const datasetData = await db.selectDataset(datasetMeta.manifest!); + const baseUrl = datasetData.volumeViewerDataRoot + "/"; + args.imageDownloadHref = datasetData.downloadRoot + "/" + id; + // args.fovDownloadHref = datasetData.downloadRoot + "/" + id; + + const fileInfo = await db.getFileInfoByCellId(id); + args.imageUrl = baseUrl + fileInfo!.volumeviewerPath; + args.parentImageUrl = baseUrl + fileInfo!.fovVolumeviewerPath; + + return args; + // only now do we have all the data needed +} + +async function getArgsFromQueryString(): Promise<{ + args: Partial; + viewerSettings: Partial; +}> { + const params = parseQueryString(); + let args: Partial = {}; + const viewerSettings: Partial = {}; + + if (params) { + if (params.mask) { + viewerSettings.maskAlpha = parseInt(params.mask, 10); + } + if (params.view) { + const mapping = { + "3D": ViewMode.threeD, + Z: ViewMode.xy, + Y: ViewMode.xz, + X: ViewMode.yz, + }; + const allowedViews = Object.keys(mapping); + let view: "3D" | "X" | "Y" | "Z"; + if (allowedViews.includes(params.view)) { + view = params.view as "3D" | "X" | "Y" | "Z"; + } else { + view = "3D"; + } + viewerSettings.viewMode = mapping[view]; + } + if (params.ch) { + // ?ch=1,2 + // ?luts=0,255,0,255 + // ?colors=ff0000,00ff00 + const initialChannelSettings: ViewerChannelSettings = { + groups: [{ name: "Channels", channels: [] }], + }; + const ch = initialChannelSettings.groups[0].channels; + + const channelsOn = params.ch.split(",").map((numstr) => parseInt(numstr, 10)); + for (let i = 0; i < channelsOn.length; ++i) { + ch.push({ match: channelsOn[i], enabled: true }); + } + // look for luts or color + if (params.luts) { + const luts = params.luts.split(","); + if (luts.length !== ch.length * 2) { + console.log("ILL-FORMED QUERYSTRING: luts must have a min/max for each ch"); + } + for (let i = 0; i < ch.length; ++i) { + ch[i]["lut"] = [luts[i * 2], luts[i * 2 + 1]]; + } + } + if (params.colors) { + const colors = params.colors.split(","); + if (colors.length !== ch.length) { + console.log("ILL-FORMED QUERYSTRING: if colors specified, must have a color for each ch"); + } + for (let i = 0; i < ch.length; ++i) { + ch[i]["color"] = colors[i]; + } + } + args.viewerChannelSettings = initialChannelSettings; + } + if (params.url) { + const imageUrls = tryDecodeURLList(params.url) ?? decodeURL(params.url); + const firstUrl = Array.isArray(imageUrls) ? imageUrls[0] : imageUrls; + + args.cellId = "1"; + args.imageUrl = imageUrls; + // this is invalid for zarr? + args.imageDownloadHref = firstUrl; + args.parentImageUrl = ""; + args.parentImageDownloadHref = ""; + // if json, then use the CFE settings for now. + // (See VIEWER_3D_SETTINGS) + // otherwise turn the first 3 channels on and group them + if (!firstUrl.endsWith("json") && !params.ch) { + args.viewerChannelSettings = { + groups: [ + // first 3 channels on by default! + { + name: "Channels", + channels: [ + { match: [0, 1, 2], enabled: true }, + { match: "(.+)", enabled: false }, + ], + }, + ], + }; + } + } else if (params.file) { + // quick way to load a atlas.json from a special directory. + // + // ?file=relative-path-to-atlas-on-isilon + args.cellId = "1"; + const baseUrl = "http://dev-aics-dtp-001.corp.alleninstitute.org/dan-data/"; + args.imageUrl = baseUrl + params.file; + args.parentImageUrl = baseUrl + params.file; + args.parentImageDownloadHref = ""; + args.imageDownloadHref = ""; + } else if (params.dataset && params.id) { + // ?dataset=aics_hipsc_v2020.1&id=232265 + const datasetArgs = await loadDataset(params.dataset, params.id); + args = { ...args, ...datasetArgs }; + } + } + + return { args, viewerSettings }; +} From 1a16c0275f361201a4c0fd84c7becdf8be5b792d Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 12:17:49 -0700 Subject: [PATCH 09/26] feat: Viewer retrieves URL parameters --- src/website/components/AppWrapper.tsx | 32 ++++++++++++++++++--------- src/website/utils/url_utils.ts | 16 ++++++++++---- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/website/components/AppWrapper.tsx b/src/website/components/AppWrapper.tsx index 0570fdb1..31cc3eac 100644 --- a/src/website/components/AppWrapper.tsx +++ b/src/website/components/AppWrapper.tsx @@ -1,7 +1,8 @@ -import React, { ReactElement, useState } from "react"; +import React, { ReactElement, useMemo, useState } from "react"; import { useLocation } from "react-router-dom"; import { AppProps, GlobalViewerSettings } from "../../aics-image-viewer/components/App/types"; import { ImageViewerApp, RenderMode, ViewMode } from "../.."; +import { getArgsFromQueryString } from "../utils/url_utils"; type AppWrapperProps = {}; @@ -25,17 +26,28 @@ const DEFAULT_VIEWER_SETTINGS: Partial = { */ export default function AppWrapper(props: AppWrapperProps): ReactElement { const location = useLocation(); - // Collect navigation state params (AppProps) - const locationArgs = location.state as AppProps; + // TODO: Update this with the load parameter later :) - const [viewerProps] = useState(locationArgs); + const [viewerSettings, setViewerSettings] = useState>(DEFAULT_VIEWER_SETTINGS); + const [viewerArgs, setViewerArgs] = useState(undefined); + + useMemo(async () => { + // Collect navigation state params (AppProps) + const locationArgs = location.state as AppProps; + // Fetching URL query parameters is async, so we need to do it here + const { args, viewerSettings } = await getArgsFromQueryString(); + + setViewerArgs({ ...locationArgs, ...args }); + setViewerSettings({ ...DEFAULT_VIEWER_SETTINGS, ...viewerSettings }); + }, []); return ( - + <> + {viewerArgs && viewerSettings ? ( + + ) : ( +

Loading...

+ )} + ); } diff --git a/src/website/utils/url_utils.ts b/src/website/utils/url_utils.ts index a09fa5ca..b2057cd8 100644 --- a/src/website/utils/url_utils.ts +++ b/src/website/utils/url_utils.ts @@ -1,7 +1,7 @@ -import FirebaseRequest, { DatasetMetaData } from "./firebase"; +import FirebaseRequest, { DatasetMetaData } from "../../../public/firebase"; import { AppProps, GlobalViewerSettings } from "../../aics-image-viewer/components/App/types"; -import { RenderMode, ViewMode } from "../../aics-image-viewer/shared/enums"; +import { ViewMode } from "../../aics-image-viewer/shared/enums"; import { ViewerChannelSettings } from "../../aics-image-viewer/shared/utils/viewerChannelSettings"; // export const VIEWER_3D_SETTINGS: ViewerChannelSettings = { @@ -139,10 +139,9 @@ async function loadDataset(dataset: string, id: string): Promise; viewerSettings: Partial; }> { @@ -250,3 +249,12 @@ async function getArgsFromQueryString(): Promise<{ return { args, viewerSettings }; } + +/** + * Returns true if the current location has a query string + * that can be successfully parsed into viewer arguments. + */ +export async function locationHasUrlParamArgs(): Promise { + const { args } = await getArgsFromQueryString(); + return Object.keys(args).length > 0; +} From 675591f0cf4a36a5b70c31d6eed58a54bdd34537 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 12:27:51 -0700 Subject: [PATCH 10/26] feat: Landing page redirects to viewer if params are received --- src/website/components/LandingPage/index.tsx | 22 ++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/website/components/LandingPage/index.tsx b/src/website/components/LandingPage/index.tsx index 93aa160b..3a64c7cc 100644 --- a/src/website/components/LandingPage/index.tsx +++ b/src/website/components/LandingPage/index.tsx @@ -1,14 +1,15 @@ import { faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button, Tooltip } from "antd"; -import React, { ReactElement, useEffect, useState } from "react"; +import React, { ReactElement, useEffect, useMemo, useState } from "react"; import { landingPageContent } from "./content"; import { DatasetEntry, ProjectEntry } from "../../types"; import styled from "styled-components"; import { FlexColumnAlignCenter, FlexColumn, FlexRowAlignCenter, VisuallyHidden, FlexRow } from "./utils"; -import { useNavigate } from "react-router"; +import { redirect, useNavigate } from "react-router"; import { AppProps } from "../../../aics-image-viewer/components/App/types"; +import { getArgsFromQueryString } from "../../utils/url_utils"; const MAX_CONTENT_WIDTH_PX = 1060; @@ -188,7 +189,24 @@ export default function LandingPage(props: LandingPageProps): ReactElement { // Rendering const navigation = useNavigate(); + useMemo(async () => { + // Check if the URL used to open the landing page has arguments; + // if so, assume that this is an old URL intended to go to the viewer. + // Navigate to the viewer while preserving URL arguments. + const { args } = await getArgsFromQueryString(); + console.log(args); + if (Object.keys(args).length > 0) { + navigation("/viewer" + location.search, { + state: args, + replace: true, + }); + // redirect("/viewer" + location.search); + } + }, []); + const onClickLoad = (appProps: AppPropsNoLayout): void => { + // TODO: Get URL search params from the appProps and append it to the viewer URL. + // Alternatively, AppWrapper should manage syncing URL and received props. navigation("/viewer", { state: appProps, }); From c68dff3fe2d5eed95a03786177f82cca8a059ec1 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 12:34:44 -0700 Subject: [PATCH 11/26] refactor: Code cleanup + linting --- src/website/components/LandingPage/index.tsx | 2 +- src/website/utils/url_utils.ts | 9 --------- webpack.dev.js | 1 + 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/website/components/LandingPage/index.tsx b/src/website/components/LandingPage/index.tsx index 3a64c7cc..f753e78b 100644 --- a/src/website/components/LandingPage/index.tsx +++ b/src/website/components/LandingPage/index.tsx @@ -7,7 +7,7 @@ import { landingPageContent } from "./content"; import { DatasetEntry, ProjectEntry } from "../../types"; import styled from "styled-components"; import { FlexColumnAlignCenter, FlexColumn, FlexRowAlignCenter, VisuallyHidden, FlexRow } from "./utils"; -import { redirect, useNavigate } from "react-router"; +import { useNavigate } from "react-router"; import { AppProps } from "../../../aics-image-viewer/components/App/types"; import { getArgsFromQueryString } from "../../utils/url_utils"; diff --git a/src/website/utils/url_utils.ts b/src/website/utils/url_utils.ts index b2057cd8..5b8dbe97 100644 --- a/src/website/utils/url_utils.ts +++ b/src/website/utils/url_utils.ts @@ -249,12 +249,3 @@ export async function getArgsFromQueryString(): Promise<{ return { args, viewerSettings }; } - -/** - * Returns true if the current location has a query string - * that can be successfully parsed into viewer arguments. - */ -export async function locationHasUrlParamArgs(): Promise { - const { args } = await getArgsFromQueryString(); - return Object.keys(args).length > 0; -} diff --git a/webpack.dev.js b/webpack.dev.js index 8842674c..9ed5abca 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -6,6 +6,7 @@ module.exports = (env) => { mode: "development", devtool: "eval-source-map", devServer: { + // Allows the dev server to handle routes historyApiFallback: true, open: ["/"], port: 9020, From b167967c36345fe17c250ccaa981e0da85c712ab Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 14:08:36 -0700 Subject: [PATCH 12/26] refactor: Added default app props, typing --- src/website/components/AppWrapper.tsx | 22 ++++++++++---------- src/website/components/LandingPage/index.tsx | 8 ++----- src/website/types.ts | 6 ++++-- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/website/components/AppWrapper.tsx b/src/website/components/AppWrapper.tsx index 31cc3eac..e40f510b 100644 --- a/src/website/components/AppWrapper.tsx +++ b/src/website/components/AppWrapper.tsx @@ -3,6 +3,7 @@ import { useLocation } from "react-router-dom"; import { AppProps, GlobalViewerSettings } from "../../aics-image-viewer/components/App/types"; import { ImageViewerApp, RenderMode, ViewMode } from "../.."; import { getArgsFromQueryString } from "../utils/url_utils"; +import { AppDataProps } from "../types"; type AppWrapperProps = {}; @@ -20,6 +21,13 @@ const DEFAULT_VIEWER_SETTINGS: Partial = { boundingBoxColor: [255, 255, 255] as [number, number, number], }; +const DEFAULT_APP_PROPS: AppDataProps = { + imageUrl: "", + cellId: "", + imageDownloadHref: "", + parentImageDownloadHref: "", +}; + /** * Renders additional components around the main ImageViewer component, and also collects URL and navigation state params * to pass to the viewer. @@ -29,11 +37,11 @@ export default function AppWrapper(props: AppWrapperProps): ReactElement { // TODO: Update this with the load parameter later :) const [viewerSettings, setViewerSettings] = useState>(DEFAULT_VIEWER_SETTINGS); - const [viewerArgs, setViewerArgs] = useState(undefined); + const [viewerArgs, setViewerArgs] = useState(DEFAULT_APP_PROPS); useMemo(async () => { // Collect navigation state params (AppProps) - const locationArgs = location.state as AppProps; + const locationArgs = location.state as AppDataProps; // Fetching URL query parameters is async, so we need to do it here const { args, viewerSettings } = await getArgsFromQueryString(); @@ -41,13 +49,5 @@ export default function AppWrapper(props: AppWrapperProps): ReactElement { setViewerSettings({ ...DEFAULT_VIEWER_SETTINGS, ...viewerSettings }); }, []); - return ( - <> - {viewerArgs && viewerSettings ? ( - - ) : ( -

Loading...

- )} - - ); + return ; } diff --git a/src/website/components/LandingPage/index.tsx b/src/website/components/LandingPage/index.tsx index f753e78b..850baae3 100644 --- a/src/website/components/LandingPage/index.tsx +++ b/src/website/components/LandingPage/index.tsx @@ -4,11 +4,10 @@ import { Button, Tooltip } from "antd"; import React, { ReactElement, useEffect, useMemo, useState } from "react"; import { landingPageContent } from "./content"; -import { DatasetEntry, ProjectEntry } from "../../types"; +import { AppDataProps, DatasetEntry, ProjectEntry } from "../../types"; import styled from "styled-components"; import { FlexColumnAlignCenter, FlexColumn, FlexRowAlignCenter, VisuallyHidden, FlexRow } from "./utils"; import { useNavigate } from "react-router"; -import { AppProps } from "../../../aics-image-viewer/components/App/types"; import { getArgsFromQueryString } from "../../utils/url_utils"; const MAX_CONTENT_WIDTH_PX = 1060; @@ -182,9 +181,6 @@ const InReviewFlag = styled(FlexRowAlignCenter)` type LandingPageProps = {}; -// TODO: Replace this everywhere it's used -type AppPropsNoLayout = Omit; - export default function LandingPage(props: LandingPageProps): ReactElement { // Rendering const navigation = useNavigate(); @@ -204,7 +200,7 @@ export default function LandingPage(props: LandingPageProps): ReactElement { } }, []); - const onClickLoad = (appProps: AppPropsNoLayout): void => { + const onClickLoad = (appProps: AppDataProps): void => { // TODO: Get URL search params from the appProps and append it to the viewer URL. // Alternatively, AppWrapper should manage syncing URL and received props. navigation("/viewer", { diff --git a/src/website/types.ts b/src/website/types.ts index 1dfab15e..c548a42a 100644 --- a/src/website/types.ts +++ b/src/website/types.ts @@ -1,9 +1,11 @@ import { AppProps } from "../aics-image-viewer/components/App/types"; +export type AppDataProps = Omit; + export type DatasetEntry = { name: string; description: string; - loadParams: Omit; + loadParams: AppDataProps; }; export type ProjectEntry = { @@ -11,7 +13,7 @@ export type ProjectEntry = { description: string; publicationLink?: URL; publicationName?: string; - loadParams?: Omit; + loadParams?: AppDataProps; datasets?: DatasetEntry[]; inReview?: boolean; }; From e76894c191e0d1d56773f621d3c597ae5d6aa7b6 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 14:08:56 -0700 Subject: [PATCH 13/26] refactor: Removed unused defaults in url_utils --- src/website/utils/url_utils.ts | 66 ---------------------------------- 1 file changed, 66 deletions(-) diff --git a/src/website/utils/url_utils.ts b/src/website/utils/url_utils.ts index 5b8dbe97..526d704c 100644 --- a/src/website/utils/url_utils.ts +++ b/src/website/utils/url_utils.ts @@ -4,49 +4,6 @@ import { AppProps, GlobalViewerSettings } from "../../aics-image-viewer/componen import { ViewMode } from "../../aics-image-viewer/shared/enums"; import { ViewerChannelSettings } from "../../aics-image-viewer/shared/utils/viewerChannelSettings"; -// export const VIEWER_3D_SETTINGS: ViewerChannelSettings = { -// groups: [ -// { -// name: "Observed channels", -// channels: [ -// { name: "Membrane", match: ["(CMDRP)"], color: "E2CDB3", enabled: true, lut: ["p50", "p98"] }, -// { -// name: "Labeled structure", -// match: ["(EGFP)|(RFPT)"], -// color: "6FBA11", -// enabled: true, -// lut: ["p50", "p98"], -// }, -// { name: "DNA", match: ["(H3342)"], color: "8DA3C0", enabled: true, lut: ["p50", "p98"] }, -// { name: "Bright field", match: ["(100)|(Bright)"], color: "F5F1CB", enabled: false, lut: ["p50", "p98"] }, -// ], -// }, -// { -// name: "Segmentation channels", -// channels: [ -// { -// name: "Labeled structure", -// match: ["(SEG_STRUCT)"], -// color: "E0E3D1", -// enabled: false, -// lut: ["p50", "p98"], -// }, -// { name: "Membrane", match: ["(SEG_Memb)"], color: "DD9BF5", enabled: false, lut: ["p50", "p98"] }, -// { name: "DNA", match: ["(SEG_DNA)"], color: "E3F4F5", enabled: false, lut: ["p50", "p98"] }, -// ], -// }, -// { -// name: "Contour channels", -// channels: [ -// { name: "Membrane", match: ["(CON_Memb)"], color: "FF6200", enabled: false, lut: ["p50", "p98"] }, -// { name: "DNA", match: ["(CON_DNA)"], color: "F7DB78", enabled: false, lut: ["p50", "p98"] }, -// ], -// }, -// ], -// // must be the true channel name in the volume data -// maskChannelName: "SEG_Memb", -// }; - type ParamKeys = "mask" | "ch" | "luts" | "colors" | "url" | "file" | "dataset" | "id" | "view"; type Params = { [_ in ParamKeys]?: string }; @@ -85,29 +42,6 @@ const tryDecodeURLList = (url: string, delim = ","): string[] | undefined => { return urls; }; -// const BASE_URL = "https://s3-us-west-2.amazonaws.com/bisque.allencell.org/v1.4.0/Cell-Viewer_Thumbnails/"; -// const args: Omit = { -// cellId: "2025", -// imageUrl: BASE_URL + "AICS-22/AICS-22_8319_2025_atlas.json", -// parentImageUrl: BASE_URL + "AICS-22/AICS-22_8319_atlas.json", -// parentImageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=F8319", -// imageDownloadHref: "https://files.allencell.org/api/2.0/file/download?collection=cellviewer-1-4/?id=C2025", -// viewerChannelSettings: VIEWER_3D_SETTINGS, -// }; -// const viewerSettings: Partial = { -// showAxes: false, -// showBoundingBox: false, -// autorotate: false, -// viewMode: ViewMode.threeD, -// renderMode: RenderMode.volumetric, -// maskAlpha: 50, -// brightness: 70, -// density: 50, -// levels: [0, 128, 255] as [number, number, number], -// backgroundColor: [0, 0, 0] as [number, number, number], -// boundingBoxColor: [255, 255, 255] as [number, number, number], -// }; - async function loadDataset(dataset: string, id: string): Promise> { const db = new FirebaseRequest(); const args: Partial = {}; From 900318004e1273ff8a19dbf5024727d67e7009e2 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 14:20:07 -0700 Subject: [PATCH 14/26] feat: Add default error page --- public/index.tsx | 4 ++-- src/website/components/ErrorPage.tsx | 36 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 src/website/components/ErrorPage.tsx diff --git a/public/index.tsx b/public/index.tsx index d0e112bb..3d3eb993 100644 --- a/public/index.tsx +++ b/public/index.tsx @@ -5,10 +5,9 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import "antd/dist/antd.less"; // Components -import { RenderMode, ViewerChannelSettings, ViewMode } from "../src"; import AppWrapper from "../src/website/components/AppWrapper"; import LandingPage from "../src/website/components/LandingPage"; -import { AppProps, GlobalViewerSettings } from "../src/aics-image-viewer/components/App/types"; +import ErrorPage from "../src/website/components/ErrorPage"; import StyleProvider from "../src/aics-image-viewer/components/StyleProvider"; import "../src/aics-image-viewer/assets/styles/typography.css"; import "./App.css"; @@ -22,6 +21,7 @@ const router = createBrowserRouter([ { path: "/", element: , + errorElement: , }, { path: "viewer", diff --git a/src/website/components/ErrorPage.tsx b/src/website/components/ErrorPage.tsx new file mode 100644 index 00000000..30471c27 --- /dev/null +++ b/src/website/components/ErrorPage.tsx @@ -0,0 +1,36 @@ +import React, { ReactElement } from "react"; +import { FlexColumnAlignCenter } from "./LandingPage/utils"; +import { ErrorResponse, Link, useRouteError } from "react-router-dom"; +import { Button } from "antd"; + +type ErrorPageProps = {}; + +const isErrorResponse = (error: unknown): error is ErrorResponse => { + return typeof (error as ErrorResponse).status === "number" && typeof (error as ErrorResponse).statusText === "string"; +}; + +export default function ErrorPage(props: ErrorPageProps): ReactElement { + const error = useRouteError() as unknown; + let errorMessage = ""; + + if (isErrorResponse(error)) { + errorMessage = error.status + " " + error.statusText; + } else if (error instanceof Error) { + errorMessage = error.message; + } else { + errorMessage = "Unknown error"; + } + + return ( +
+ +

{errorMessage}

+

Sorry, something went wrong.

+
+ + + +
+
+ ); +} From 5c4f77ffcacea9d2d11217ada4bb632c7808fa32 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 16:36:57 -0700 Subject: [PATCH 15/26] feat: Added hash routing on public deployments --- public/index.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/public/index.tsx b/public/index.tsx index 3d3eb993..cc403264 100644 --- a/public/index.tsx +++ b/public/index.tsx @@ -1,6 +1,6 @@ import React from "react"; import ReactDOM from "react-dom"; -import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import { createBrowserRouter, createHashRouter, RouterProvider } from "react-router-dom"; import "antd/dist/antd.less"; @@ -17,7 +17,7 @@ console.log(`website-3d-cell-viewer ${WEBSITE3DCELLVIEWER_BUILD_ENVIRONMENT} bui console.log(`website-3d-cell-viewer Version ${WEBSITE3DCELLVIEWER_VERSION}`); console.log(`volume-viewer Version ${VOLUMEVIEWER_VERSION}`); -const router = createBrowserRouter([ +const routes = [ { path: "/", element: , @@ -27,7 +27,16 @@ const router = createBrowserRouter([ path: "viewer", element: , }, -]); +]; + +let router; +if (WEBSITE3DCELLVIEWER_BUILD_ENVIRONMENT === "dev") { + router = createBrowserRouter(routes); +} else { + // Production mode. + // TODO: Use createBrowserRouter when building to S3. + router = createHashRouter(routes); +} ReactDOM.render( // , From b72d82d5c5c55eb9608e5352f8caba704cd543d6 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 16:37:19 -0700 Subject: [PATCH 16/26] fix: Fixed URL search params when using hash-based routing --- src/website/components/AppWrapper.tsx | 10 ++++++--- src/website/components/LandingPage/index.tsx | 13 ++++++----- src/website/utils/url_utils.ts | 23 ++++++++++---------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/website/components/AppWrapper.tsx b/src/website/components/AppWrapper.tsx index e40f510b..233b992a 100644 --- a/src/website/components/AppWrapper.tsx +++ b/src/website/components/AppWrapper.tsx @@ -1,8 +1,8 @@ import React, { ReactElement, useMemo, useState } from "react"; -import { useLocation } from "react-router-dom"; +import { useLocation, useSearchParams } from "react-router-dom"; import { AppProps, GlobalViewerSettings } from "../../aics-image-viewer/components/App/types"; import { ImageViewerApp, RenderMode, ViewMode } from "../.."; -import { getArgsFromQueryString } from "../utils/url_utils"; +import { getArgsFromParams } from "../utils/url_utils"; import { AppDataProps } from "../types"; type AppWrapperProps = {}; @@ -38,12 +38,16 @@ export default function AppWrapper(props: AppWrapperProps): ReactElement { // TODO: Update this with the load parameter later :) const [viewerSettings, setViewerSettings] = useState>(DEFAULT_VIEWER_SETTINGS); const [viewerArgs, setViewerArgs] = useState(DEFAULT_APP_PROPS); + const [searchParams] = useSearchParams(); useMemo(async () => { // Collect navigation state params (AppProps) const locationArgs = location.state as AppDataProps; // Fetching URL query parameters is async, so we need to do it here - const { args, viewerSettings } = await getArgsFromQueryString(); + const { args, viewerSettings } = await getArgsFromParams(searchParams); + + console.log("locationArgs", locationArgs); + console.log("args", args); setViewerArgs({ ...locationArgs, ...args }); setViewerSettings({ ...DEFAULT_VIEWER_SETTINGS, ...viewerSettings }); diff --git a/src/website/components/LandingPage/index.tsx b/src/website/components/LandingPage/index.tsx index 850baae3..88233247 100644 --- a/src/website/components/LandingPage/index.tsx +++ b/src/website/components/LandingPage/index.tsx @@ -8,7 +8,8 @@ import { AppDataProps, DatasetEntry, ProjectEntry } from "../../types"; import styled from "styled-components"; import { FlexColumnAlignCenter, FlexColumn, FlexRowAlignCenter, VisuallyHidden, FlexRow } from "./utils"; import { useNavigate } from "react-router"; -import { getArgsFromQueryString } from "../../utils/url_utils"; +import { getArgsFromParams } from "../../utils/url_utils"; +import { useSearchParams } from "react-router-dom"; const MAX_CONTENT_WIDTH_PX = 1060; @@ -184,26 +185,26 @@ type LandingPageProps = {}; export default function LandingPage(props: LandingPageProps): ReactElement { // Rendering const navigation = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); useMemo(async () => { // Check if the URL used to open the landing page has arguments; // if so, assume that this is an old URL intended to go to the viewer. // Navigate to the viewer while preserving URL arguments. - const { args } = await getArgsFromQueryString(); - console.log(args); + const { args } = await getArgsFromParams(searchParams); if (Object.keys(args).length > 0) { - navigation("/viewer" + location.search, { + console.log("Detected URL parameters. Redirecting from landing page to viewer."); + navigation("viewer" + "?" + searchParams.toString(), { state: args, replace: true, }); - // redirect("/viewer" + location.search); } }, []); const onClickLoad = (appProps: AppDataProps): void => { // TODO: Get URL search params from the appProps and append it to the viewer URL. // Alternatively, AppWrapper should manage syncing URL and received props. - navigation("/viewer", { + navigation("viewer", { state: appProps, }); }; diff --git a/src/website/utils/url_utils.ts b/src/website/utils/url_utils.ts index 526d704c..7c93b046 100644 --- a/src/website/utils/url_utils.ts +++ b/src/website/utils/url_utils.ts @@ -4,17 +4,18 @@ import { AppProps, GlobalViewerSettings } from "../../aics-image-viewer/componen import { ViewMode } from "../../aics-image-viewer/shared/enums"; import { ViewerChannelSettings } from "../../aics-image-viewer/shared/utils/viewerChannelSettings"; -type ParamKeys = "mask" | "ch" | "luts" | "colors" | "url" | "file" | "dataset" | "id" | "view"; +const paramKeys = ["mask", "ch", "luts", "colors", "url", "file", "dataset", "id", "view"]; +type ParamKeys = (typeof paramKeys)[number]; type Params = { [_ in ParamKeys]?: string }; -function parseQueryString(): Params { - const pairs = location.search.slice(1).split("&"); - const result: Record = {}; - pairs.forEach((pairString) => { - const pair = pairString.split("="); - result[pair[0]] = decodeURIComponent(pair[1] || ""); - }); - return JSON.parse(JSON.stringify(result)); +function urlSearchParamsToParams(searchParams: URLSearchParams): Params { + const result: Params = {}; + for (const [key, value] of searchParams.entries()) { + if (paramKeys.includes(key)) { + result[key] = value; + } + } + return result; } const decodeURL = (url: string): string => { @@ -75,11 +76,11 @@ async function loadDataset(dataset: string, id: string): Promise; viewerSettings: Partial; }> { - const params = parseQueryString(); + const params = urlSearchParamsToParams(urlSearchParams); let args: Partial = {}; const viewerSettings: Partial = {}; From 33e4990b9a1f4ff9c839a8b854664f112a540548 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 16:37:58 -0700 Subject: [PATCH 17/26] fix: Linting --- src/website/components/AppWrapper.tsx | 2 +- src/website/components/LandingPage/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/website/components/AppWrapper.tsx b/src/website/components/AppWrapper.tsx index 233b992a..1dc42f27 100644 --- a/src/website/components/AppWrapper.tsx +++ b/src/website/components/AppWrapper.tsx @@ -1,6 +1,6 @@ import React, { ReactElement, useMemo, useState } from "react"; import { useLocation, useSearchParams } from "react-router-dom"; -import { AppProps, GlobalViewerSettings } from "../../aics-image-viewer/components/App/types"; +import { GlobalViewerSettings } from "../../aics-image-viewer/components/App/types"; import { ImageViewerApp, RenderMode, ViewMode } from "../.."; import { getArgsFromParams } from "../utils/url_utils"; import { AppDataProps } from "../types"; diff --git a/src/website/components/LandingPage/index.tsx b/src/website/components/LandingPage/index.tsx index 88233247..e796b5ea 100644 --- a/src/website/components/LandingPage/index.tsx +++ b/src/website/components/LandingPage/index.tsx @@ -185,7 +185,7 @@ type LandingPageProps = {}; export default function LandingPage(props: LandingPageProps): ReactElement { // Rendering const navigation = useNavigate(); - const [searchParams, setSearchParams] = useSearchParams(); + const [searchParams] = useSearchParams(); useMemo(async () => { // Check if the URL used to open the landing page has arguments; From 4bf22187d0eff082796171ae2371a08279db3dad Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 17:02:22 -0700 Subject: [PATCH 18/26] refactor: Code cleanup --- public/index.tsx | 1 - src/website/components/AppWrapper.tsx | 19 +++++++------------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/public/index.tsx b/public/index.tsx index cc403264..4b352e60 100644 --- a/public/index.tsx +++ b/public/index.tsx @@ -39,7 +39,6 @@ if (WEBSITE3DCELLVIEWER_BUILD_ENVIRONMENT === "dev") { } ReactDOM.render( - // , , diff --git a/src/website/components/AppWrapper.tsx b/src/website/components/AppWrapper.tsx index 1dc42f27..15aed429 100644 --- a/src/website/components/AppWrapper.tsx +++ b/src/website/components/AppWrapper.tsx @@ -5,8 +5,6 @@ import { ImageViewerApp, RenderMode, ViewMode } from "../.."; import { getArgsFromParams } from "../utils/url_utils"; import { AppDataProps } from "../types"; -type AppWrapperProps = {}; - const DEFAULT_VIEWER_SETTINGS: Partial = { showAxes: false, showBoundingBox: false, @@ -29,28 +27,25 @@ const DEFAULT_APP_PROPS: AppDataProps = { }; /** - * Renders additional components around the main ImageViewer component, and also collects URL and navigation state params - * to pass to the viewer. + * Wrapper around the main ImageViewer component. Handles the collection of parameters from the + * URL and routing state to pass to the viewer. */ -export default function AppWrapper(props: AppWrapperProps): ReactElement { +export default function AppWrapper(): ReactElement { const location = useLocation(); - // TODO: Update this with the load parameter later :) const [viewerSettings, setViewerSettings] = useState>(DEFAULT_VIEWER_SETTINGS); const [viewerArgs, setViewerArgs] = useState(DEFAULT_APP_PROPS); const [searchParams] = useSearchParams(); + // On load, fetch parameters from the URL and routing state, then merge. useMemo(async () => { // Collect navigation state params (AppProps) const locationArgs = location.state as AppDataProps; // Fetching URL query parameters is async, so we need to do it here - const { args, viewerSettings } = await getArgsFromParams(searchParams); - - console.log("locationArgs", locationArgs); - console.log("args", args); + const { args: urlArgs, viewerSettings: urlViewerSettings } = await getArgsFromParams(searchParams); - setViewerArgs({ ...locationArgs, ...args }); - setViewerSettings({ ...DEFAULT_VIEWER_SETTINGS, ...viewerSettings }); + setViewerArgs({ ...DEFAULT_APP_PROPS, ...locationArgs, ...urlArgs }); + setViewerSettings({ ...DEFAULT_VIEWER_SETTINGS, ...urlViewerSettings }); }, []); return ; From 5b56362aa26782aa0d8544e4752752953b05ac19 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 17:02:38 -0700 Subject: [PATCH 19/26] feat: Improved error page messaging; link styling --- .../components/StyleProvider/styles.css | 4 ++++ src/website/components/ErrorPage.tsx | 22 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/aics-image-viewer/components/StyleProvider/styles.css b/src/aics-image-viewer/components/StyleProvider/styles.css index cce74e9a..dda036a8 100644 --- a/src/aics-image-viewer/components/StyleProvider/styles.css +++ b/src/aics-image-viewer/components/StyleProvider/styles.css @@ -95,6 +95,10 @@ font-weight: 400; } + a { + color: var(--color-text-link); + } + & *::selection { /** * Override Ant + Less styling, since it uses a very light purple that's diff --git a/src/website/components/ErrorPage.tsx b/src/website/components/ErrorPage.tsx index 30471c27..17ff6e42 100644 --- a/src/website/components/ErrorPage.tsx +++ b/src/website/components/ErrorPage.tsx @@ -24,8 +24,26 @@ export default function ErrorPage(props: ErrorPageProps): ReactElement { return (
-

{errorMessage}

-

Sorry, something went wrong.

+

Sorry, something went wrong.

+ +

We encountered the following error:

+ +

{errorMessage}

+

+ Check the browser console for more details. +

+
+

+ If the issue persists after a refresh,{" "} + + please click here to report it. + +

+

From 21728646cd08e8e635170e88ff7151879fdfe7ec Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 17:15:03 -0700 Subject: [PATCH 20/26] feat: Updated reporting link with icon, link styling when focused --- src/aics-image-viewer/components/StyleProvider/styles.css | 4 ++++ src/website/components/ErrorPage.tsx | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/aics-image-viewer/components/StyleProvider/styles.css b/src/aics-image-viewer/components/StyleProvider/styles.css index dda036a8..85f39ab7 100644 --- a/src/aics-image-viewer/components/StyleProvider/styles.css +++ b/src/aics-image-viewer/components/StyleProvider/styles.css @@ -97,6 +97,10 @@ a { color: var(--color-text-link); + &:focus, + &:focus-visible { + text-decoration: underline; + } } & *::selection { diff --git a/src/website/components/ErrorPage.tsx b/src/website/components/ErrorPage.tsx index 17ff6e42..60296177 100644 --- a/src/website/components/ErrorPage.tsx +++ b/src/website/components/ErrorPage.tsx @@ -2,6 +2,8 @@ import React, { ReactElement } from "react"; import { FlexColumnAlignCenter } from "./LandingPage/utils"; import { ErrorResponse, Link, useRouteError } from "react-router-dom"; import { Button } from "antd"; +import { faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; type ErrorPageProps = {}; @@ -41,10 +43,16 @@ export default function ErrorPage(props: ErrorPageProps): ReactElement { target="_blank" > please click here to report it. +


+ {/* TODO: Bad practice to wrap a button inside a link, since it's confusing for tab navigation. */} From c15caf112820c5d0a27378c9a0fe7e762e3d765e Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 11 Apr 2024 18:29:10 -0700 Subject: [PATCH 21/26] refactor: Code cleanup --- src/website/components/AppWrapper.tsx | 6 +- src/website/components/LandingPage/index.tsx | 8 +- src/website/utils/url_utils.ts | 168 +++++++++---------- 3 files changed, 89 insertions(+), 93 deletions(-) diff --git a/src/website/components/AppWrapper.tsx b/src/website/components/AppWrapper.tsx index 15aed429..89d8e6c1 100644 --- a/src/website/components/AppWrapper.tsx +++ b/src/website/components/AppWrapper.tsx @@ -28,7 +28,7 @@ const DEFAULT_APP_PROPS: AppDataProps = { /** * Wrapper around the main ImageViewer component. Handles the collection of parameters from the - * URL and routing state to pass to the viewer. + * URL and location state (from routing) to pass to the viewer. */ export default function AppWrapper(): ReactElement { const location = useLocation(); @@ -37,11 +37,9 @@ export default function AppWrapper(): ReactElement { const [viewerArgs, setViewerArgs] = useState(DEFAULT_APP_PROPS); const [searchParams] = useSearchParams(); - // On load, fetch parameters from the URL and routing state, then merge. useMemo(async () => { - // Collect navigation state params (AppProps) + // On load, fetch parameters from the URL and location state, then merge. const locationArgs = location.state as AppDataProps; - // Fetching URL query parameters is async, so we need to do it here const { args: urlArgs, viewerSettings: urlViewerSettings } = await getArgsFromParams(searchParams); setViewerArgs({ ...DEFAULT_APP_PROPS, ...locationArgs, ...urlArgs }); diff --git a/src/website/components/LandingPage/index.tsx b/src/website/components/LandingPage/index.tsx index e796b5ea..46fecd02 100644 --- a/src/website/components/LandingPage/index.tsx +++ b/src/website/components/LandingPage/index.tsx @@ -180,9 +180,7 @@ const InReviewFlag = styled(FlexRowAlignCenter)` } `; -type LandingPageProps = {}; - -export default function LandingPage(props: LandingPageProps): ReactElement { +export default function LandingPage(): ReactElement { // Rendering const navigation = useNavigate(); const [searchParams] = useSearchParams(); @@ -202,8 +200,8 @@ export default function LandingPage(props: LandingPageProps): ReactElement { }, []); const onClickLoad = (appProps: AppDataProps): void => { - // TODO: Get URL search params from the appProps and append it to the viewer URL. - // Alternatively, AppWrapper should manage syncing URL and received props. + // TODO: Make URL search params from the appProps and append it to the viewer URL so the URL can be shared directly. + // Alternatively, AppWrapper should manage syncing URL and viewer props. navigation("viewer", { state: appProps, }); diff --git a/src/website/utils/url_utils.ts b/src/website/utils/url_utils.ts index 7c93b046..8eb6e351 100644 --- a/src/website/utils/url_utils.ts +++ b/src/website/utils/url_utils.ts @@ -84,102 +84,102 @@ export async function getArgsFromParams(urlSearchParams: URLSearchParams): Promi let args: Partial = {}; const viewerSettings: Partial = {}; - if (params) { - if (params.mask) { - viewerSettings.maskAlpha = parseInt(params.mask, 10); + if (params.mask) { + viewerSettings.maskAlpha = parseInt(params.mask, 10); + } + if (params.view) { + const mapping = { + "3D": ViewMode.threeD, + Z: ViewMode.xy, + Y: ViewMode.xz, + X: ViewMode.yz, + }; + const allowedViews = Object.keys(mapping); + let view: "3D" | "X" | "Y" | "Z"; + if (allowedViews.includes(params.view)) { + view = params.view as "3D" | "X" | "Y" | "Z"; + } else { + view = "3D"; } - if (params.view) { - const mapping = { - "3D": ViewMode.threeD, - Z: ViewMode.xy, - Y: ViewMode.xz, - X: ViewMode.yz, - }; - const allowedViews = Object.keys(mapping); - let view: "3D" | "X" | "Y" | "Z"; - if (allowedViews.includes(params.view)) { - view = params.view as "3D" | "X" | "Y" | "Z"; - } else { - view = "3D"; - } - viewerSettings.viewMode = mapping[view]; + viewerSettings.viewMode = mapping[view]; + } + if (params.ch) { + // ?ch=1,2 + // ?luts=0,255,0,255 + // ?colors=ff0000,00ff00 + const initialChannelSettings: ViewerChannelSettings = { + groups: [{ name: "Channels", channels: [] }], + }; + const ch = initialChannelSettings.groups[0].channels; + + const channelsOn = params.ch.split(",").map((numstr) => parseInt(numstr, 10)); + for (let i = 0; i < channelsOn.length; ++i) { + ch.push({ match: channelsOn[i], enabled: true }); } - if (params.ch) { - // ?ch=1,2 - // ?luts=0,255,0,255 - // ?colors=ff0000,00ff00 - const initialChannelSettings: ViewerChannelSettings = { - groups: [{ name: "Channels", channels: [] }], - }; - const ch = initialChannelSettings.groups[0].channels; - - const channelsOn = params.ch.split(",").map((numstr) => parseInt(numstr, 10)); - for (let i = 0; i < channelsOn.length; ++i) { - ch.push({ match: channelsOn[i], enabled: true }); - } - // look for luts or color - if (params.luts) { - const luts = params.luts.split(","); - if (luts.length !== ch.length * 2) { - console.log("ILL-FORMED QUERYSTRING: luts must have a min/max for each ch"); - } + // look for luts or color + if (params.luts) { + const luts = params.luts.split(","); + if (luts.length !== ch.length * 2) { + console.warn("ILL-FORMED QUERYSTRING: luts must have a min/max for each ch"); + } else { for (let i = 0; i < ch.length; ++i) { ch[i]["lut"] = [luts[i * 2], luts[i * 2 + 1]]; } } - if (params.colors) { - const colors = params.colors.split(","); - if (colors.length !== ch.length) { - console.log("ILL-FORMED QUERYSTRING: if colors specified, must have a color for each ch"); - } + } + if (params.colors) { + const colors = params.colors.split(","); + if (colors.length !== ch.length) { + console.warn("ILL-FORMED QUERYSTRING: if colors specified, must have a color for each ch"); + } else { for (let i = 0; i < ch.length; ++i) { ch[i]["color"] = colors[i]; } } - args.viewerChannelSettings = initialChannelSettings; } - if (params.url) { - const imageUrls = tryDecodeURLList(params.url) ?? decodeURL(params.url); - const firstUrl = Array.isArray(imageUrls) ? imageUrls[0] : imageUrls; - - args.cellId = "1"; - args.imageUrl = imageUrls; - // this is invalid for zarr? - args.imageDownloadHref = firstUrl; - args.parentImageUrl = ""; - args.parentImageDownloadHref = ""; - // if json, then use the CFE settings for now. - // (See VIEWER_3D_SETTINGS) - // otherwise turn the first 3 channels on and group them - if (!firstUrl.endsWith("json") && !params.ch) { - args.viewerChannelSettings = { - groups: [ - // first 3 channels on by default! - { - name: "Channels", - channels: [ - { match: [0, 1, 2], enabled: true }, - { match: "(.+)", enabled: false }, - ], - }, - ], - }; - } - } else if (params.file) { - // quick way to load a atlas.json from a special directory. - // - // ?file=relative-path-to-atlas-on-isilon - args.cellId = "1"; - const baseUrl = "http://dev-aics-dtp-001.corp.alleninstitute.org/dan-data/"; - args.imageUrl = baseUrl + params.file; - args.parentImageUrl = baseUrl + params.file; - args.parentImageDownloadHref = ""; - args.imageDownloadHref = ""; - } else if (params.dataset && params.id) { - // ?dataset=aics_hipsc_v2020.1&id=232265 - const datasetArgs = await loadDataset(params.dataset, params.id); - args = { ...args, ...datasetArgs }; + args.viewerChannelSettings = initialChannelSettings; + } + if (params.url) { + const imageUrls = tryDecodeURLList(params.url) ?? decodeURL(params.url); + const firstUrl = Array.isArray(imageUrls) ? imageUrls[0] : imageUrls; + + args.cellId = "1"; + args.imageUrl = imageUrls; + // this is invalid for zarr? + args.imageDownloadHref = firstUrl; + args.parentImageUrl = ""; + args.parentImageDownloadHref = ""; + // if json, then use the CFE settings for now. + // (See VIEWER_3D_SETTINGS) + // otherwise turn the first 3 channels on and group them + if (!firstUrl.endsWith("json") && !params.ch) { + args.viewerChannelSettings = { + groups: [ + // first 3 channels on by default! + { + name: "Channels", + channels: [ + { match: [0, 1, 2], enabled: true }, + { match: "(.+)", enabled: false }, + ], + }, + ], + }; } + } else if (params.file) { + // quick way to load a atlas.json from a special directory. + // + // ?file=relative-path-to-atlas-on-isilon + args.cellId = "1"; + const baseUrl = "http://dev-aics-dtp-001.corp.alleninstitute.org/dan-data/"; + args.imageUrl = baseUrl + params.file; + args.parentImageUrl = baseUrl + params.file; + args.parentImageDownloadHref = ""; + args.imageDownloadHref = ""; + } else if (params.dataset && params.id) { + // ?dataset=aics_hipsc_v2020.1&id=232265 + const datasetArgs = await loadDataset(params.dataset, params.id); + args = { ...args, ...datasetArgs }; } return { args, viewerSettings }; From b6c00f0d5e4257c76e214b0c86e85a301ecbcfe5 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Wed, 17 Apr 2024 09:40:28 -0700 Subject: [PATCH 22/26] refactor: Moved `website` directory out of `src` --- public/index.tsx | 6 +++--- tsconfig.json | 5 +++-- {src/website => website}/components/AppWrapper.tsx | 4 ++-- {src/website => website}/components/ErrorPage.tsx | 0 .../website => website}/components/LandingPage/content.ts | 0 {src/website => website}/components/LandingPage/index.tsx | 0 {src/website => website}/components/LandingPage/utils.tsx | 0 {src/website => website}/types.ts | 2 +- {src/website => website}/utils/url_utils.ts | 8 ++++---- 9 files changed, 13 insertions(+), 12 deletions(-) rename {src/website => website}/components/AppWrapper.tsx (92%) rename {src/website => website}/components/ErrorPage.tsx (100%) rename {src/website => website}/components/LandingPage/content.ts (100%) rename {src/website => website}/components/LandingPage/index.tsx (100%) rename {src/website => website}/components/LandingPage/utils.tsx (100%) rename {src/website => website}/types.ts (83%) rename {src/website => website}/utils/url_utils.ts (94%) diff --git a/public/index.tsx b/public/index.tsx index 4b352e60..2a0d4f50 100644 --- a/public/index.tsx +++ b/public/index.tsx @@ -5,9 +5,9 @@ import { createBrowserRouter, createHashRouter, RouterProvider } from "react-rou import "antd/dist/antd.less"; // Components -import AppWrapper from "../src/website/components/AppWrapper"; -import LandingPage from "../src/website/components/LandingPage"; -import ErrorPage from "../src/website/components/ErrorPage"; +import AppWrapper from "../website/components/AppWrapper"; +import LandingPage from "../website/components/LandingPage"; +import ErrorPage from "../website/components/ErrorPage"; import StyleProvider from "../src/aics-image-viewer/components/StyleProvider"; import "../src/aics-image-viewer/assets/styles/typography.css"; import "./App.css"; diff --git a/tsconfig.json b/tsconfig.json index 32b40cd6..98996d3b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "./tsconfig.base", - "include": ["src/**/*"], + "include": ["src/**/*", "website/**/*"], "compilerOptions": { - "module": "ES6" + "module": "ES6", + "outDir": "./dist" } } diff --git a/src/website/components/AppWrapper.tsx b/website/components/AppWrapper.tsx similarity index 92% rename from src/website/components/AppWrapper.tsx rename to website/components/AppWrapper.tsx index 89d8e6c1..b21d2e74 100644 --- a/src/website/components/AppWrapper.tsx +++ b/website/components/AppWrapper.tsx @@ -1,7 +1,7 @@ import React, { ReactElement, useMemo, useState } from "react"; import { useLocation, useSearchParams } from "react-router-dom"; -import { GlobalViewerSettings } from "../../aics-image-viewer/components/App/types"; -import { ImageViewerApp, RenderMode, ViewMode } from "../.."; +import { GlobalViewerSettings } from "../../src/aics-image-viewer/components/App/types"; +import { ImageViewerApp, RenderMode, ViewMode } from "../../src"; import { getArgsFromParams } from "../utils/url_utils"; import { AppDataProps } from "../types"; diff --git a/src/website/components/ErrorPage.tsx b/website/components/ErrorPage.tsx similarity index 100% rename from src/website/components/ErrorPage.tsx rename to website/components/ErrorPage.tsx diff --git a/src/website/components/LandingPage/content.ts b/website/components/LandingPage/content.ts similarity index 100% rename from src/website/components/LandingPage/content.ts rename to website/components/LandingPage/content.ts diff --git a/src/website/components/LandingPage/index.tsx b/website/components/LandingPage/index.tsx similarity index 100% rename from src/website/components/LandingPage/index.tsx rename to website/components/LandingPage/index.tsx diff --git a/src/website/components/LandingPage/utils.tsx b/website/components/LandingPage/utils.tsx similarity index 100% rename from src/website/components/LandingPage/utils.tsx rename to website/components/LandingPage/utils.tsx diff --git a/src/website/types.ts b/website/types.ts similarity index 83% rename from src/website/types.ts rename to website/types.ts index c548a42a..6b27d411 100644 --- a/src/website/types.ts +++ b/website/types.ts @@ -1,4 +1,4 @@ -import { AppProps } from "../aics-image-viewer/components/App/types"; +import { AppProps } from "../src/aics-image-viewer/components/App/types"; export type AppDataProps = Omit; diff --git a/src/website/utils/url_utils.ts b/website/utils/url_utils.ts similarity index 94% rename from src/website/utils/url_utils.ts rename to website/utils/url_utils.ts index 8eb6e351..776b1f5c 100644 --- a/src/website/utils/url_utils.ts +++ b/website/utils/url_utils.ts @@ -1,8 +1,8 @@ -import FirebaseRequest, { DatasetMetaData } from "../../../public/firebase"; +import FirebaseRequest, { DatasetMetaData } from "../../public/firebase"; -import { AppProps, GlobalViewerSettings } from "../../aics-image-viewer/components/App/types"; -import { ViewMode } from "../../aics-image-viewer/shared/enums"; -import { ViewerChannelSettings } from "../../aics-image-viewer/shared/utils/viewerChannelSettings"; +import { AppProps, GlobalViewerSettings } from "../../src/aics-image-viewer/components/App/types"; +import { ViewMode } from "../../src/aics-image-viewer/shared/enums"; +import { ViewerChannelSettings } from "../../src/aics-image-viewer/shared/utils/viewerChannelSettings"; const paramKeys = ["mask", "ch", "luts", "colors", "url", "file", "dataset", "id", "view"]; type ParamKeys = (typeof paramKeys)[number]; From 68a2c40904aa9513277f5e726000faab023ebb99 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Wed, 17 Apr 2024 09:41:47 -0700 Subject: [PATCH 23/26] doc: Removed stray comment --- website/utils/url_utils.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/website/utils/url_utils.ts b/website/utils/url_utils.ts index 776b1f5c..98886242 100644 --- a/website/utils/url_utils.ts +++ b/website/utils/url_utils.ts @@ -149,9 +149,8 @@ export async function getArgsFromParams(urlSearchParams: URLSearchParams): Promi args.imageDownloadHref = firstUrl; args.parentImageUrl = ""; args.parentImageDownloadHref = ""; - // if json, then use the CFE settings for now. - // (See VIEWER_3D_SETTINGS) - // otherwise turn the first 3 channels on and group them + // if json, will not override channel settings. + // otherwise turn the first 3 channels on by default and group them if (!firstUrl.endsWith("json") && !params.ch) { args.viewerChannelSettings = { groups: [ From 18e9fe3801f98d95e664bd0bc13c279ea357329b Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Wed, 17 Apr 2024 09:46:57 -0700 Subject: [PATCH 24/26] feat: Added props to AppWrapper for viewer settings and args --- website/components/AppWrapper.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/website/components/AppWrapper.tsx b/website/components/AppWrapper.tsx index b21d2e74..1460d095 100644 --- a/website/components/AppWrapper.tsx +++ b/website/components/AppWrapper.tsx @@ -5,6 +5,11 @@ import { ImageViewerApp, RenderMode, ViewMode } from "../../src"; import { getArgsFromParams } from "../utils/url_utils"; import { AppDataProps } from "../types"; +type AppWrapperProps = { + viewerSettings?: Partial; + viewerArgs?: AppDataProps; +}; + const DEFAULT_VIEWER_SETTINGS: Partial = { showAxes: false, showBoundingBox: false, @@ -26,15 +31,21 @@ const DEFAULT_APP_PROPS: AppDataProps = { parentImageDownloadHref: "", }; +const defaultAppWrapperProps = { + viewerSettings: DEFAULT_VIEWER_SETTINGS, + viewerArgs: DEFAULT_APP_PROPS, +}; + /** * Wrapper around the main ImageViewer component. Handles the collection of parameters from the * URL and location state (from routing) to pass to the viewer. */ -export default function AppWrapper(): ReactElement { +export default function AppWrapper(inputProps: AppWrapperProps): ReactElement { + const props = { ...defaultAppWrapperProps, ...inputProps }; const location = useLocation(); - const [viewerSettings, setViewerSettings] = useState>(DEFAULT_VIEWER_SETTINGS); - const [viewerArgs, setViewerArgs] = useState(DEFAULT_APP_PROPS); + const [viewerSettings, setViewerSettings] = useState>(props.viewerSettings); + const [viewerArgs, setViewerArgs] = useState(props.viewerArgs); const [searchParams] = useSearchParams(); useMemo(async () => { From e06bb53146c68af1cbda493608634dc2915457b0 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Tue, 23 Apr 2024 09:23:09 -0700 Subject: [PATCH 25/26] refactor: Sort imports --- website/components/LandingPage/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/components/LandingPage/index.tsx b/website/components/LandingPage/index.tsx index 46fecd02..600b85ab 100644 --- a/website/components/LandingPage/index.tsx +++ b/website/components/LandingPage/index.tsx @@ -2,14 +2,14 @@ import { faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button, Tooltip } from "antd"; import React, { ReactElement, useEffect, useMemo, useState } from "react"; +import { useNavigate } from "react-router"; +import { useSearchParams } from "react-router-dom"; +import styled from "styled-components"; import { landingPageContent } from "./content"; import { AppDataProps, DatasetEntry, ProjectEntry } from "../../types"; -import styled from "styled-components"; import { FlexColumnAlignCenter, FlexColumn, FlexRowAlignCenter, VisuallyHidden, FlexRow } from "./utils"; -import { useNavigate } from "react-router"; import { getArgsFromParams } from "../../utils/url_utils"; -import { useSearchParams } from "react-router-dom"; const MAX_CONTENT_WIDTH_PX = 1060; From d0e88623a5f35fbae06c15a4778c07f32004a97d Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Tue, 23 Apr 2024 09:26:45 -0700 Subject: [PATCH 26/26] refactor: Replaced useMemo with useEffect, added missing router type --- public/index.tsx | 3 ++- website/components/AppWrapper.tsx | 12 ++++++------ website/components/LandingPage/index.tsx | 21 +++++++++++---------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/public/index.tsx b/public/index.tsx index 2a0d4f50..e0a4c958 100644 --- a/public/index.tsx +++ b/public/index.tsx @@ -1,6 +1,7 @@ import React from "react"; import ReactDOM from "react-dom"; import { createBrowserRouter, createHashRouter, RouterProvider } from "react-router-dom"; +import { Router } from "@remix-run/router"; import "antd/dist/antd.less"; @@ -29,7 +30,7 @@ const routes = [ }, ]; -let router; +let router: Router; if (WEBSITE3DCELLVIEWER_BUILD_ENVIRONMENT === "dev") { router = createBrowserRouter(routes); } else { diff --git a/website/components/AppWrapper.tsx b/website/components/AppWrapper.tsx index 1460d095..fd6b5d47 100644 --- a/website/components/AppWrapper.tsx +++ b/website/components/AppWrapper.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, useMemo, useState } from "react"; +import React, { ReactElement, useEffect, useState } from "react"; import { useLocation, useSearchParams } from "react-router-dom"; import { GlobalViewerSettings } from "../../src/aics-image-viewer/components/App/types"; import { ImageViewerApp, RenderMode, ViewMode } from "../../src"; @@ -48,13 +48,13 @@ export default function AppWrapper(inputProps: AppWrapperProps): ReactElement { const [viewerArgs, setViewerArgs] = useState(props.viewerArgs); const [searchParams] = useSearchParams(); - useMemo(async () => { + useEffect(() => { // On load, fetch parameters from the URL and location state, then merge. const locationArgs = location.state as AppDataProps; - const { args: urlArgs, viewerSettings: urlViewerSettings } = await getArgsFromParams(searchParams); - - setViewerArgs({ ...DEFAULT_APP_PROPS, ...locationArgs, ...urlArgs }); - setViewerSettings({ ...DEFAULT_VIEWER_SETTINGS, ...urlViewerSettings }); + getArgsFromParams(searchParams).then(({ args: urlArgs, viewerSettings: urlViewerSettings }) => { + setViewerArgs({ ...DEFAULT_APP_PROPS, ...locationArgs, ...urlArgs }); + setViewerSettings({ ...DEFAULT_VIEWER_SETTINGS, ...urlViewerSettings }); + }); }, []); return ; diff --git a/website/components/LandingPage/index.tsx b/website/components/LandingPage/index.tsx index 600b85ab..10b1ecbe 100644 --- a/website/components/LandingPage/index.tsx +++ b/website/components/LandingPage/index.tsx @@ -1,7 +1,7 @@ import { faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button, Tooltip } from "antd"; -import React, { ReactElement, useEffect, useMemo, useState } from "react"; +import React, { ReactElement, useEffect, useState } from "react"; import { useNavigate } from "react-router"; import { useSearchParams } from "react-router-dom"; import styled from "styled-components"; @@ -185,18 +185,19 @@ export default function LandingPage(): ReactElement { const navigation = useNavigate(); const [searchParams] = useSearchParams(); - useMemo(async () => { + useEffect(() => { // Check if the URL used to open the landing page has arguments; // if so, assume that this is an old URL intended to go to the viewer. // Navigate to the viewer while preserving URL arguments. - const { args } = await getArgsFromParams(searchParams); - if (Object.keys(args).length > 0) { - console.log("Detected URL parameters. Redirecting from landing page to viewer."); - navigation("viewer" + "?" + searchParams.toString(), { - state: args, - replace: true, - }); - } + getArgsFromParams(searchParams).then(({ args }) => { + if (Object.keys(args).length > 0) { + console.log("Detected URL parameters. Redirecting from landing page to viewer."); + navigation("viewer" + "?" + searchParams.toString(), { + state: args, + replace: true, + }); + } + }); }, []); const onClickLoad = (appProps: AppDataProps): void => {