/ concept-collection / numbl-figure-viewer
Sign in
concept-collection / numbl-figure-viewer
Load a figure handed over from numbl via the URL hash
On startup, read the figure from the viewer URL hash: either encoded directly (fig=) or as a reference to an encrypted upload (u=&k=&iv=) which is fetched and decrypted. Runs once (the upload host is one-shot), strips the hash from the address bar, shows a loading state, and surfaces errors.
Jeremy Magland <jmagland@flatironinstitute.org> committed commit ffb6f968b8ff parent d24678a Browse files
1 changed file+54−8
src/App.tsxmodified+54−8View file
@@ -1,5 +1,10 @@
11 import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2-import { importFigureHdf5, FigureView, type FigureState } from "numbl/graphics";
2+import {
3+ importFigureHdf5,
4+ loadFigureFromHash,
5+ FigureView,
6+ type FigureState,
7+} from "numbl/graphics";
38 import { buildTree } from "./tree";
49 import { ObjectTree } from "./ObjectTree";
510 import { InfoPanel } from "./InfoPanel";
@@ -61,6 +66,40 @@ export function App() {
6166 setRightOpen(!isMobile);
6267 }, [isMobile]);
6368
69+ // Receive a figure handed over from numbl via the URL hash (postMessage can't
70+ // reach a COOP-isolated opener's cross-origin popup). Two forms: the figure
71+ // gzip-encoded directly (`fig=`), or a reference to an encrypted upload
72+ // (`u=&k=&iv=`) which we fetch + decrypt. Runs once (the upload host is
73+ // one-shot); the hash is stripped from the address bar immediately.
74+ const hashLoaded = useRef(false);
75+ useEffect(() => {
76+ if (hashLoaded.current) return;
77+ hashLoaded.current = true;
78+ const hash = window.location.hash;
79+ if (!hash.startsWith("#fig=") && !hash.startsWith("#u=")) return;
80+ try {
81+ history.replaceState(
82+ null,
83+ "",
84+ window.location.pathname + window.location.search
85+ );
86+ } catch {
87+ /* ignore */
88+ }
89+ setLoading(true);
90+ setError(null);
91+ loadFigureFromHash(hash)
92+ .then(fig => {
93+ if (!fig) return;
94+ setFigure(fig);
95+ setFileName("from numbl");
96+ setSelectedId("figure");
97+ setViewState(emptyViewState());
98+ })
99+ .catch(e => setError(e instanceof Error ? e.message : String(e)))
100+ .finally(() => setLoading(false));
101+ }, []);
102+
64103 const tree = useMemo(
65104 () => (figure ? buildTree(figure, fileName) : null),
66105 [figure, fileName]
@@ -174,13 +213,20 @@ export function App() {
174213 >
175214 <div className="empty-inner">
176215 <div className="empty-icon">▦</div>
177- <h2>Open a numbl figure</h2>
178- <p>
179- Drag a <code>.h5</code> figure file here, or click to choose one.
180- </p>
181- <p className="muted small">
182- Exported from numbl via “Download data (.h5)”.
183- </p>
216+ {loading ? (
217+ <h2>Loading figure…</h2>
218+ ) : (
219+ <>
220+ <h2>Open a numbl figure</h2>
221+ <p>
222+ Drag a <code>.h5</code> figure file here, or click to choose
223+ one.
224+ </p>
225+ <p className="muted small">
226+ Exported from numbl via “Download data (.h5)”.
227+ </p>
228+ </>
229+ )}
184230 </div>
185231 </div>
186232 ) : (
moveopenescclose