Skip to content

[ZEPPELIN-6737] Connect a private read-only Angular notebook view to Shared Notebook Core - #5568

Merged
voidmatcha merged 9 commits into
apache:masterfrom
kimyenac:ZEPPELIN-6737
Oct 14, 2026
Merged

voidmatcha merged 9 commits into
apache:masterfrom
kimyenac:ZEPPELIN-6737

Conversation

@kimyenac

Copy link
Copy Markdown
Contributor

What is this PR for?

Connect the Shared Notebook Core read state from ZEPPELIN-6736 to a private, read-only Angular notebook view that reads real notebook and revision data. The view is opt-in for development and testing with ?notebookCoreRead on a notebook URL (#/notebook/<id>?notebookCoreRead, also under /revision/<revisionId>). Without the flag the editable notebook route is unchanged.

Commits:

  1. Server: GET_NOTE and NOTE_REVISION replies, their AUTH_INFO and ERROR_INFO failures, and errors from the top-level onMessage handler for these two requests carry the request msgId. Broadcasts, other operations and requests without a msgId keep their previous payloads.
  2. SDK and read transport: getNote and noteRevision return the msgId they sent. NotebookReadTransport subscribes before it sends, accepts only the first reply carrying its msgId, then stops listening. It bounds each read with a timeout and keeps the Core's ACL handle for the permissions REST read, so an old success or failure cannot apply.
  3. Host: NotebookCoreReadHost owns one read store and one frozen NotebookPort for one private route lifetime. It reads only while the Shell's socket is connected, replaces loaded state with failed when the connection drops, reads again after the Shell reconnects without a reconnect loop of its own, and disposes once.
  4. View and route: a canMatch guard on the existing route boundary (ZEPPELIN-6675) selects NotebookCoreReadComponent. The component renders snapshots through a reused Observable (async pipe, OnPush) and does not mount the editable paragraph components.
  5. Real-route E2E.
  6. Read captures and replay.

Read operations

Included Path
Live note GET_NOTE → NOTE with the request msgId
Revision NOTE_REVISION → NOTE_REVISION with the request msgId. A NOTE broadcast does not change a loaded revision
Permissions GET /api/notebook/{noteId}/permissions; 403 is access-denied, other failures failed, and permissions stay unknown on failure
Forbidden AUTH_INFO with the request msgId → access-denied
Other failures ERROR_INFO with the request msgId, an empty reply, or a timeout → failed

A missing note is reported as failed. The server only describes it in ERROR_INFO text, which this PR does not parse. The server also answers the permissions read of a missing note with 200, so the view shows permissions only for a loaded note.

Excluded: run, cancel, save, commit, patch, form and configuration changes, incremental paragraph and output updates, collaboration, Angular objects, ACL writes and reconnect recovery beyond reading again. The view sends no message other than its reads, including while it mounts and unmounts.

Saved results rendered: TABLE (with persisted chart settings), TEXT, HTML, IMG, SVG, NULL. Shown as not rendered: ANGULAR (needs live Angular objects), NETWORK (no Angular renderer) and unknown types. The result component writes defaults into its config while it renders (renderGraph → setDefaultConfig), so it receives a per-render copy of the frozen chart settings that is never sent back.

Fixture provenance

e2e/fixtures/notebook-read/read-anonymous.json and read-denied.json were captured from this branch (server and UI built from the PR commits), because the replies depend on the server change above, which is not on Apache master yet. They are kept apart from the seven lifecycle captures and their inventory, and metadata.source names the branch commit. The core-contract README says to recapture them from master once this is merged. AUTH_INFO prints account names in its text, so the capture replaces them with <user>. The ACL and error evidence here covers the reads of this view only; the rest of ZEPPELIN-6673 (ACL writes, REST 401/405, SESSION_LOGOUT) is left to that issue.

What type of PR is it?

Feature

Todos

  • - Recapture e2e/fixtures/notebook-read/ from Apache master after this is merged
  • - ZEPPELIN-6673 for the remaining permission, authentication and Shell error fixtures

What is the Jira issue?

https://issues.apache.org/jira/browse/ZEPPELIN-6737

How should this be tested?

Server (NotebookServerTest, 36 tests; 6 new):

./mvnw test -pl zeppelin-server -Dtest=NotebookServerTest

The new tests cover the msgId on NOTE, NOTE_REVISION, AUTH_INFO and ERROR_INFO read replies, on the top-level exception path for both reads, and its absence on requests without a msgId, on an unrelated operation's failure and top-level error, and on NOTE broadcasts. Removing each server change made the corresponding test fail.

Frontend, from zeppelin-web-angular:

npm run test:shell                    # 292 passed
npm run typecheck:sdk-contracts
npm run check:core-contract-fixtures  # 150 passed
npm run build:notebook-core-port-proof && npm run test:notebook-route-boundary
npm run build:projects && ng build --configuration production

The core-read unit tests cover the transport (listen-before-send, msgId matching, duplicate, late, post-timeout and post-cancel replies, A → B → A, permission success and failure for an earlier activation), the host (port identity, connection loss and reconnect, exactly-once teardown), the view model and the component (only reads are sent on mount, route changes and unmount). Breaking each of these behaviors made a test fail. The replay spec runs both captures through the production transport and Core, and fails if the captured replies lose their msgId.

Real-route Playwright (e2e/tests/notebook/core-read/notebook-core-read.spec.ts), Chromium, against a server built from this branch:

  • anonymous: 7 passed, the denied-read test skipped (it needs two Shiro accounts)
  • authenticated (conf/shiro.ini.template with admin added to user1, as in CI): 8 passed

A Playwright WebSocket route records every frame the page sends and holds, injects or drops server frames. Removing the msgId check made the pending-read broadcast test fail, and keeping loaded state on a dropped connection made the reconnect test fail.

Default notebook regression with the flag off (authenticated, tests/notebook/{main,paragraph,persistence,revision}): 25 passed, 6 failed for local environment reasons, not this change: 4 React footer tests (the React remote was not built for this local dist, remoteEntry.js 404) and 2 %python execution tests (no working local Python interpreter). Full CI covers both legs.

Not run locally: Firefox and WebKit, and the full E2E suite.

Captures were recorded with:

ZEPPELIN_READ_CAPTURE_COMMIT=$(git rev-parse HEAD) PLAYWRIGHT_BASE_URL=<server> \
  npm run e2e:core-contract:live -- read-fixtures

Screenshots (if appropriate)

Questions:

  • Does the license files need to update? No
  • Is there breaking changes for older versions? No. Clients that send no msgId get the same payloads, and the SDK methods only gain a return value.
  • Does this needs documentation? The read captures are documented in the core-contract README.

🤖 Generated with Claude Code

GET_NOTE and NOTE_REVISION replies, their AUTH_INFO and ERROR_INFO
failures, and errors from the top-level handler for these two requests
now carry the msgId the client sent, so a client can tell its own read
reply from a NOTE broadcast or a reply to an earlier request.

Broadcasts, other operations and requests without a msgId keep the
previous payloads.
…vation

The SDK read requests return the msgId they sent. NotebookReadTransport
listens before it sends and passes a Core read request only the first
NOTE, NOTE_REVISION, AUTH_INFO or ERROR_INFO carrying its msgId, then
stops listening. Broadcasts, replies to other requests and replies after
a timeout or cancellation never reach the Core.

The permissions read keeps the ACL handle of the activation it started
in, so both its success and its failure are rejected once the route has
moved on. A 403 is reported as access-denied and anything else as
failed. A missing note is only described by ERROR_INFO text, so it is
reported as failed rather than guessed from the message.
NotebookCoreReadHost creates one read store and one frozen
NotebookPort, keeps them across note and revision changes, and exposes
the snapshot with the Shell's connection state as one replayed
Observable.

It sends a read only while the Shell's socket is connected. When the
connection drops it ends pending reads and replaces any loaded note
with a failed state, so stale ready state does not survive. It reads
the current target again when the Shell reconnects, without a
reconnect loop of its own. Disposing ends every read and the Core, and
a second dispose changes nothing.
With `?notebookCoreRead` on a notebook URL, a canMatch guard on the
existing route boundary selects NotebookCoreReadComponent instead of
the editable notebook. Without it the route is unchanged, and leaving
the flag destroys the private view and loads the editable notebook
afresh.

The view renders loaded snapshots and saved results only. It does not
mount the editable paragraph components, so it sends no run, save,
patch, form or configuration message, including on mount and unmount.
Saved results go through the shared result component in published
mode, with a per-render copy of the persisted chart settings because
the component writes into its config. ANGULAR and NETWORK output and
unknown result types are shown as not rendered instead of being
skipped. Permissions are shown for a loaded note only and stay unknown
when their read fails.
The spec runs the private view against a real server, through a
Playwright WebSocket route that records what the page sends and can
hold, inject or drop server frames.

It checks the rendered saved results and chart settings, that mounting
and unmounting send only the read, A -> B -> A with a late reply,
a pending read that a NOTE broadcast cannot complete, duplicate replies
and broadcasts after loading, revision entry, exit and re-entry,
cancelled and failed permission reads, a missing note, invalidation on
a dropped connection with a fresh read after the Shell reconnects, and,
with two Shiro accounts, a read the note permissions deny.
Two live captures record what the private view reads: a live note, a
revision and a missing note in anonymous mode, and a read the note
permissions deny in authenticated mode. Each read reply and each
failure carries the msgId of its request. The permissions reads answer
200 for all three anonymous reads, the missing note included, and 403
for the denied read.

The replies depend on the server change in this branch, which is not on
Apache master yet, so the captures name the commit that was built and
served and are kept apart from the lifecycle capture inventory. The
README section says to recapture from master once the change is merged.
AUTH_INFO prints account names in its text, which the capture replaces
with <user>.

A replay spec drives the production read transport and Core with both
captures in recorded order and fails if the replies lose their msgId.

@voidmatcha voidmatcha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. I built it and ran it locally, and it works as described.

What I checked

  • Links I used: the read view at http://localhost:<port>/#/notebook/<noteId>?notebookCoreRead, a revision at .../#/notebook/<noteId>/revision/<revisionId>?notebookCoreRead, and a missing note or revision with a non-existent id in the same form. For comparison I opened the normal notebook without the flag at .../#/notebook/<noteId>.
  • Browser: notes and revisions load, saved results (table, HTML) render, and ANGULAR/NETWORK show as "not shown". A missing note or revision shows a failure with Retry instead of a blank screen. Removing the flag gives the normal notebook, adding it back gives the read view, and the read view sends only GET_NOTE/NOTE_REVISION.
  • Server: NotebookServerTest passes 36/36 on a clean clone. Probing the WebSocket directly, the msgId comes back correctly on NOTE, on ERROR_INFO for a missing note, on the top-level exception path, and for overlapping reads, while requests without a msgId, unrelated operations and broadcasts behave as before.
  • Playwright core-read: anonymous mode 7 passed (the denied-read test is skipped without two accounts); Shiro mode passed all 8, including access denied. On local Git storage the revision test needed one retry because parallel tests race on the JGit index lock.

I put a few small, optional suggestions on a branch you can cherry-pick from: https://github.com/voidmatcha/zeppelin/tree/ZEPPELIN-6737-pr5568-review-fixes (details inline).

Note: I understand this read view as the "private comparison harness" from ZEPPELIN-6627, so I expect core-read/, this guard and their e2e specs to be retired together at some point (timing TBD). So my suggestions focus on what will last (the msgId correlation and the fixtures); on the view side I only put small things on the branch (the label and a few role attributes). Since that code will go away, feel free to skip it.

@if (view.snapshot.status === 'ready') {
@if (view.snapshot.acl.status === 'ready') {
<span class="core-read-permissions" data-testid="notebook-core-read-permissions">
Your access: {{ view.access }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The note lists do not know the server's admin override, so a loaded note can show "none" here. The branch hides the label in that case; removing it is fine too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, cherry-picked as b007a4c (with the role attributes). A loaded note now shows no access label when the lists do not name the user.

},
capturedAt: new Date().toISOString(),
zeppelinVersion: version.version,
source: { repository: 'kimyenac/zeppelin', branch: 'ZEPPELIN-6737', commit }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The replay spec uses the recorded replies as-is; only this source label is never read, so leaving it pointing at the fork commit is fine for this PR. Recapturing from master after the merge fixes it, and I think you could open one more issue related to ZEPPELIN-6673 or send a [MINOR] PR. The branch's capture spec records apache/zeppelin and the served serverCommit instead of hard-coding the fork.

@kimyenac kimyenac Oct 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cherry-picked as 4c21fa4. In d7ea6ef I also dropped the "until then the source is the named branch commit" exclusion and reworded the README, since the capture spec now records apache/zeppelin. The committed captures still name the branch commit. I will recapture them from Apache master as part of ZEPPELIN-6673 after this is merged, rather than opening a new issue.

noteId: recorded.data.id ?? recorded.data.noteId,
revisionId: recorded.data.revisionId
});
return recorded.msgId as string;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the recorded msgId were lost from both the sent read and its reply, undefined would match undefined, so the replay would still pass. The branch asserts that the recorded request has a non-empty msgId.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Cherry-picked as 4c21fa4, so the replay now fails when a recorded read has no msgId.

@voidmatcha voidmatcha left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@voidmatcha
voidmatcha merged commit e8d710f into apache:master Oct 14, 2026
24 checks passed
@voidmatcha

Copy link
Copy Markdown
Member

Merged into master (e8d710f).

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.

2 participants