From b37e4cdb02eeaa58145ceccf4292100d45ea520c Mon Sep 17 00:00:00 2001 From: meganrm Date: Tue, 11 Aug 2026 16:53:59 -0700 Subject: [PATCH] Fix trajectory fetch errors being silently discarded (B1) and add loading/error UI (UX1) Co-Authored-By: Claude Sonnet 5 --- src/App.tsx | 11 ++++- src/components/Viewer.tsx | 79 ++++++++++++++++++++++---------- src/components/viewer.module.css | 26 +++++++++++ src/simulation/context.tsx | 13 +++++- src/utils/fetch3DTrajectory.ts | 4 +- 5 files changed, 104 insertions(+), 29 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index bd5e9626..0c38f4cf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -502,12 +502,16 @@ function App() { setTrajectoryStatus(TrajectoryStatus.LOADING); clearAllAnalysisState(); - await fetch3DTrajectory( + const didSucceed = await fetch3DTrajectory( url, simulariumController, setPreComputedTrajectoryPlotData, ); - setTrajectoryStatus(TrajectoryStatus.LOADED); + setTrajectoryStatus( + didSucceed + ? TrajectoryStatus.LOADED + : TrajectoryStatus.ERROR, + ); }; changeTrajectory(); } @@ -784,11 +788,13 @@ function App() { maxConcentration: simulationData.getMaxConcentration(currentModule), productName, setIsPlaying, + setTrajectoryStatus, setViewportSize, simulariumController, timeFactor, timeUnit: simulationData.timeUnit, trajectoryName, + trajectoryStatus, viewportSize, }), [ @@ -808,6 +814,7 @@ function App() { simulationData, timeFactor, trajectoryName, + trajectoryStatus, viewportSize, ], ); diff --git a/src/components/Viewer.tsx b/src/components/Viewer.tsx index 9beab242..bc938e14 100644 --- a/src/components/Viewer.tsx +++ b/src/components/Viewer.tsx @@ -5,14 +5,17 @@ import SimulariumViewer, { TimeData, } from "@aics/simularium-viewer"; import "@aics/simularium-viewer/style/style.css"; +import { Spin } from "antd"; +import { LIVE_SIMULATION_NAME } from "../constants"; import { useSimulariumSimulation, useSimulariumUi, } from "../hooks/useSimulationContext"; -import styles from "./viewer.module.css"; import useWindowResize from "../hooks/useWindowResize"; -import { LIVE_SIMULATION_NAME } from "../constants"; +import { TrajectoryStatus } from "../types"; +import { SecondaryButton } from "./shared/ButtonLibrary"; +import styles from "./viewer.module.css"; interface ViewerProps { handleTimeChange: (timeData: TimeData) => void; @@ -34,11 +37,13 @@ export default function Viewer({ handleTimeChange }: ViewerProps): ReactNode { }); const container = useRef(null); const { - viewportSize, + handleTrajectoryChange, + setTrajectoryStatus, setViewportSize, simulariumController, - handleTrajectoryChange, trajectoryName, + trajectoryStatus, + viewportSize, } = useSimulariumSimulation(); const { page } = useSimulariumUi(); @@ -122,7 +127,27 @@ export default function Viewer({ handleTimeChange }: ViewerProps): ReactNode { ); - const showHintOverlay = !userHasInteracted && is3DTrajectory; + const loadingOverlay = ( +
+ +
+ ); + + const errorOverlay = ( +
+

Failed to load this content.

+ setTrajectoryStatus(TrajectoryStatus.INITIAL)} + > + Retry + +
+ ); + + const isLoading = trajectoryStatus === TrajectoryStatus.LOADING; + const isError = trajectoryStatus === TrajectoryStatus.ERROR; + const showHintOverlay = + !userHasInteracted && is3DTrajectory && !isLoading && !isError; return (
{showHintOverlay && hintOverlay} - {}} - showCameraControls={false} - onTrajectoryFileInfoChanged={handleTrajectoryChange} - selectionStateInfo={selectionStateInfo} - onUIDisplayDataChanged={() => {}} - loadInitialData={true} - showPaths={true} - onError={console.log} - backgroundColor={[0, 0, 0]} - onRecordedMovie={() => {}} - /> + {isLoading && loadingOverlay} + {isError && errorOverlay} + {!isError && ( + {}} + showCameraControls={false} + onTrajectoryFileInfoChanged={handleTrajectoryChange} + selectionStateInfo={selectionStateInfo} + onUIDisplayDataChanged={() => {}} + loadInitialData={true} + showPaths={true} + onError={console.log} + backgroundColor={[0, 0, 0]} + onRecordedMovie={() => {}} + /> + )}
); } diff --git a/src/components/viewer.module.css b/src/components/viewer.module.css index 79d0cf16..3b824bd1 100644 --- a/src/components/viewer.module.css +++ b/src/components/viewer.module.css @@ -34,3 +34,29 @@ margin-top: 10px; z-index: var(--viewer-overlay-layer); } + +.container .loading-overlay, +.container .error-overlay { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + text-align: center; + padding: 16px 24px; + z-index: var(--viewer-overlay-layer); +} + +.container .error-overlay { + border: 1px solid var(--primary-color); + border-radius: 4px; + color: var(--text-color); + background-color: rgba(0, 0, 0, 0.5); +} + +.container .error-overlay p { + margin: 0; +} diff --git a/src/simulation/context.tsx b/src/simulation/context.tsx index c5e7c8e9..9ba290ce 100644 --- a/src/simulation/context.tsx +++ b/src/simulation/context.tsx @@ -10,7 +10,14 @@ import { NANO, ProgressionElement, } from "../constants"; -import { AgentName, Module, ProductName, Section, ViewType } from "../types"; +import { + AgentName, + Module, + ProductName, + Section, + TrajectoryStatus, + ViewType, +} from "../types"; export interface SimulariumUiContextType { addCompletedModule: (value: Module) => void; @@ -40,11 +47,13 @@ export interface SimulariumSimulationContextType { maxConcentration: number; productName: ProductName; setIsPlaying: (value: boolean) => void; + setTrajectoryStatus: (value: TrajectoryStatus) => void; setViewportSize: (value: { width: number; height: number }) => void; simulariumController: SimulariumController | null; timeFactor: number; timeUnit: string; trajectoryName: string; + trajectoryStatus: TrajectoryStatus; viewportSize: { width: number; height: number }; } @@ -82,11 +91,13 @@ export const SimulariumSimulationContext = maxConcentration: 10, productName: ProductName.AB, setIsPlaying: () => {}, + setTrajectoryStatus: () => {}, setViewportSize: () => {}, simulariumController: null, timeFactor: 30, timeUnit: NANO, trajectoryName: LIVE_SIMULATION_NAME, + trajectoryStatus: TrajectoryStatus.INITIAL, viewportSize: DEFAULT_VIEWPORT_SIZE, }); diff --git a/src/utils/fetch3DTrajectory.ts b/src/utils/fetch3DTrajectory.ts index 141ffe54..1f82ba5d 100644 --- a/src/utils/fetch3DTrajectory.ts +++ b/src/utils/fetch3DTrajectory.ts @@ -8,7 +8,7 @@ const fetch3DTrajectory = async ( url: string, simulariumController: SimulariumController, setTrajectoryPlotData: (plotData: ScatterTrace[]) => void -) => { +): Promise => { try { const response = await fetch(url); if (response.ok) { @@ -22,11 +22,13 @@ const fetch3DTrajectory = async ( ); const plotData = simulariumFile.getPlotData(); setTrajectoryPlotData(plotData[0].data as ScatterTrace[]); // we're not using histograms + return true; } else { throw new Error(`Failed to fetch - ${response.status}`); } } catch (error) { console.log(error); + return false; } };