Skip to content

feat(viewer): edit and save Views in the embedded Neuroglancer - #463

Merged
allison-truhlar merged 11 commits into
ngviews-12-relink-uifrom
ngviews-13-edit-save
Oct 16, 2026
Merged

allison-truhlar merged 11 commits into
ngviews-12-relink-uifrom
ngviews-13-edit-save

Conversation

@allison-truhlar

@allison-truhlar allison-truhlar commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Editable-Views branch 13: a View's owner edits it in the embedded Neuroglancer viewer (/view/<read_key>) and keeps the changes with an explicit Save or Discard. This builds on #458 (relink UI) and the existing PUT /api/neuroglancer/views/{short_key}.

Editing reads the iframe's window.viewer, so it works only when Fileglancer and Neuroglancer share an origin. With a cross-origin Neuroglancer, such as the CLI default neuroglancer-demo.appspot.com, the owner sees "Editing isn't available with this Neuroglancer deployment" and the viewer stays read-only.

What changed

  • Viewer bridge: the new useNeuroglancerViewer hook polls the iframe for window.viewer. It reports ready for a same-origin viewer and unavailable when access throws, and it exposes getState, setState, and change subscriptions.
  • Dirty tracking: useViewEditState compares the viewer's toJSON() with a saved baseline. Until the user first interacts, each change resets the baseline, so Neuroglancer's own normalization and position updates during load don't count as edits. After a Save, the baseline is the state the request sent, so a camera move during an in-flight Save stays unsaved.
  • Save bar: when the owner has unsaved changes, a bar reads "Unsaved changes — saving updates this View for everyone with its link." and offers Discard and Save. A failed Save keeps the changes and shows an error toast. The viewer sets the iframe src once, at first load, so the refetch after a Save doesn't reload Neuroglancer.
  • Broken-sources banner: the owner sees each broken, relinkable source once per dataset, with a Relink button that opens RelinkDialog from feat(views): relink broken View sources from the Views table and on Data Link create #458. After a relink, the bridge pushes the new state into the viewer; when the bridge is unavailable, the iframe reloads with the relinked state. The Relink button stays disabled while the View has unsaved changes, with the tooltip "Save or discard changes first".
  • Unsaved-changes guard: with unsaved changes, in-app links (logo, Views breadcrumb, profile menu) and the Copy link, Download JSON, and Open in Neuroglancer actions open a dialog with Save and continue, Continue without saving, and Cancel. Exports use the state that Save sent, or the last saved state if the owner continues without saving. Closing or reloading the tab triggers the browser's beforeunload prompt. BrowserRouter has no useBlocker, so a capture-phase click listener catches same-origin links before React Router handles them. Browser back and forward still leave without asking.
  • Reload warning: if Neuroglancer reloads while the View has unsaved changes, a toast warns that the changes may be lost, and the Save bar hides until the viewer is ready again. Neuroglancer restores its URL hash, so edits can survive the reload. The viewer then compares with the saved baseline instead of resetting it.
  • Owner only: every edit control reads useCanEditView. A non-owner gets no Save bar, note, banner, dialog, or leave prompt.
  • Dev-only Neuroglancer passthrough: the new neuroglancer_url setting (FGC_NEUROGLANCER_URL) serves that Neuroglancer at /neuroglancer/, so you can test editing locally. The setting is unset by default, and the routes exist only when you set it. The passthrough is a GET proxy that rejects dot segments, %, ?, and # in the path. Production serves Neuroglancer from its reverse proxy instead. The Vite dev server proxies /neuroglancer to the backend. For setup, see "Testing View editing locally" in docs/Development.md.

Testing

The backend suite (1122) and frontend suite (517) pass, ESLint and Prettier are clean, and node-check stays at the known 5-error baseline.

I tested editing by hand against the real Neuroglancer through the dev passthrough:

  • Opening a View and touching nothing doesn't mark it as unsaved
  • Opening a View and changing something shows the "unsaved changes" bar with a button to save. Clicking the save button shows a success toast (if successful; an error toast if not). Reloading or reopening the View from the Views page keeps the new saved View.
  • Clicking the "Discard" button in the "unsaved changes" bar causes the viewer to jump back to the saved state.
  • Attempting to navigate back to the Views page while there are unsaved changes brings up a dialog with options to save and continue, discard and continue, or cancel. Tested all three.
  • Opening a View when the same-origin Neuroglancer is not available (e.g., via the dev passthrough) shows a banner at the top that editing isn't available.

@krokicki

@allison-truhlar
allison-truhlar added this pull request to stack #429 October 6, 2026 20:20
@allison-truhlar
allison-truhlar marked this pull request as ready for review October 6, 2026 20:43
…icter passthrough paths

- The embedded viewer fixed its iframe src on first state load, which could
  land before the viewers config and lock in the appspot fallback (so editing
  showed as unavailable on same-origin deployments). The internal NG base URL
  is now null until the config loads, and the viewer waits for it.
- The beforeunload guard fired for non-owners, who cannot save.
- A failed background refetch replaced the viewer (and unsaved edits) with
  "View not found"; cached state now keeps rendering.
- The dev passthrough rejects '%' so a once-decoded %2e%2e can't escape the
  configured prefix upstream.
If the iframe reloads while the View is dirty, toast that changes may have
been lost and hide the Save bar until Neuroglancer is ready again. Then
compare with the saved baseline instead of re-baselining: Neuroglancer
restores its own URL hash, so edits can survive the reload and must stay
unsaved.
…edits

With unsaved changes, in-app link clicks (logo, Views breadcrumb, profile
menu) and Copy link / Download JSON / Open in Neuroglancer open a dialog:
Save and continue, Continue without saving, or Cancel. Exports use the state
just saved, or the last saved one when continuing without saving.

BrowserRouter has no useBlocker, so a capture-phase click listener on the
document intercepts same-origin links before React Router's Link handles
them. Browser back/forward still leaves without asking.
…ough

test_no_passthrough_when_unset built Settings without neuroglancer_url, so a
developer's config.yaml that sets it made the test fail. Pass None explicitly.
…ache

Save and continue saves, then leaves the page, which cancels the state
refetch the save triggered. Reopening the View then rendered the cached
pre-save state first, and because the iframe src is fixed at first load,
Neuroglancer kept showing it after the refetch landed. Wait for the mount
refetch before fixing the src.
The message, each source path, and its Relink button now share a row,
with equal gaps between them, so the banner takes one line for a single
broken source. Extra sources wrap after the message, which ends in a
colon. The path uses the message's font so the row reads evenly.
@allison-truhlar
allison-truhlar merged commit 1c6baac into main Oct 16, 2026
5 of 8 checks passed
@allison-truhlar
allison-truhlar deleted the ngviews-13-edit-save branch October 16, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant