feat(python-notebook-migration, frontend): add Jupyter notebook iframe panel component - #5271
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5271 +/- ##
============================================
+ Coverage 80.44% 80.47% +0.02%
- Complexity 3874 3875 +1
============================================
Files 1160 1162 +2
Lines 46192 46264 +72
Branches 5146 5153 +7
============================================
+ Hits 37160 37230 +70
- Misses 7372 7374 +2
Partials 1660 1660
*This pull request uses carry forward flags. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
# Conflicts: # frontend/src/app/app.module.ts
Automated Reviewer SuggestionsBased on the
|
…ter-panel # Conflicts: # frontend/src/app/app.module.ts
adopt main's reviewed versions for the already merged migration slices: backend notebook-migration-service, frontend migration service, config, build, and sql. pull the reviewed open PR content from its branches: jupyter panel visibility surface and mini-map button (apache#5265), jupyter notebook iframe panel (apache#5271), and the AI generate workflow modal and panel host (apache#5273). reconcile app.module.ts by keeping main's JupyterPanelService bootstrap listener and registering JupyterNotebookPanelComponent.
|
/request-review @mengw15 |
|
| config | throughput | MB/s | latency | max Δ latest / 7d | |
|---|---|---|---|---|---|
| 🔴 | bs=10 sw=10 sl=64 | 394 | 0.24 | 24,637/34,363/34,363 us | 🔴 +20.8% / 🔴 +111.7% |
| 🔴 | bs=100 sw=10 sl=64 | 828 | 0.506 | 119,816/152,100/152,100 us | 🔴 +9.9% / 🔴 +40.0% |
| ⚪ | bs=1000 sw=10 sl=64 | 946 | 0.578 | 1,059,104/1,104,183/1,104,183 us | ⚪ within ±5% / 🔴 +6.0% |
Baseline details
Latest main 49f9e2c from same runner
| config | metric | PR | latest main | 7d avg | Δ latest | Δ 7d |
|---|---|---|---|---|---|---|
| bs=10 sw=10 sl=64 | throughput | 394 tuples/sec | 463 tuples/sec | 767.9 tuples/sec | -14.9% | -48.7% |
| bs=10 sw=10 sl=64 | MB/s | 0.24 MB/s | 0.282 MB/s | 0.469 MB/s | -14.9% | -48.8% |
| bs=10 sw=10 sl=64 | p50 | 24,637 us | 20,396 us | 12,502 us | +20.8% | +97.1% |
| bs=10 sw=10 sl=64 | p95 | 34,363 us | 32,329 us | 16,234 us | +6.3% | +111.7% |
| bs=10 sw=10 sl=64 | p99 | 34,363 us | 32,329 us | 18,919 us | +6.3% | +81.6% |
| bs=100 sw=10 sl=64 | throughput | 828 tuples/sec | 842 tuples/sec | 974.8 tuples/sec | -1.7% | -15.1% |
| bs=100 sw=10 sl=64 | MB/s | 0.506 MB/s | 0.514 MB/s | 0.595 MB/s | -1.6% | -15.0% |
| bs=100 sw=10 sl=64 | p50 | 119,816 us | 117,517 us | 102,449 us | +2.0% | +17.0% |
| bs=100 sw=10 sl=64 | p95 | 152,100 us | 138,351 us | 108,652 us | +9.9% | +40.0% |
| bs=100 sw=10 sl=64 | p99 | 152,100 us | 138,351 us | 116,310 us | +9.9% | +30.8% |
| bs=1000 sw=10 sl=64 | throughput | 946 tuples/sec | 940 tuples/sec | 1,004 tuples/sec | +0.6% | -5.8% |
| bs=1000 sw=10 sl=64 | MB/s | 0.578 MB/s | 0.574 MB/s | 0.613 MB/s | +0.7% | -5.7% |
| bs=1000 sw=10 sl=64 | p50 | 1,059,104 us | 1,066,446 us | 999,606 us | -0.7% | +6.0% |
| bs=1000 sw=10 sl=64 | p95 | 1,104,183 us | 1,106,522 us | 1,046,770 us | -0.2% | +5.5% |
| bs=1000 sw=10 sl=64 | p99 | 1,104,183 us | 1,106,522 us | 1,076,937 us | -0.2% | +2.5% |
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,508.16,200,128000,394,0.240,24637.34,34362.92,34362.92
1,100,10,64,20,2414.25,2000,1280000,828,0.506,119816.24,152099.96,152099.96
2,1000,10,64,20,21134.48,20000,12800000,946,0.578,1059104.45,1104183.35,1104183.354d27799 to
6cb1ef4
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new Angular workspace UI component intended to host JupyterLab inside Texera via a draggable/resizable iframe panel, wired to the existing JupyterPanelService visibility surface and accompanied by unit tests.
Changes:
- Introduces
JupyterNotebookPanelComponent(TS/HTML/SCSS) to render a draggable iframe-based Jupyter panel and delegate close/minimize actions toJupyterPanelService. - Adds a component-level spec file to cover visibility transitions, URL fetch/sanitization, iframe ref registration, and button delegation.
- Registers the new component in
AppModule.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/app/workspace/component/jupyter-notebook-panel/jupyter-notebook-panel.component.ts | Implements visibility subscription, URL fetching + sanitization, and iframe ref registration for the Jupyter panel. |
| frontend/src/app/workspace/component/jupyter-notebook-panel/jupyter-notebook-panel.component.html | Adds draggable panel markup with header controls and iframe binding. |
| frontend/src/app/workspace/component/jupyter-notebook-panel/jupyter-notebook-panel.component.scss | Provides panel positioning, drag header styling, and iframe sizing styles. |
| frontend/src/app/workspace/component/jupyter-notebook-panel/jupyter-notebook-panel.component.spec.ts | Adds unit tests for visibility, URL handling, iframe ref registration, and control actions. |
| frontend/src/app/app.module.ts | Registers the new panel component with the application module. |
Comments suppressed due to low confidence (2)
frontend/src/app/workspace/component/jupyter-notebook-panel/jupyter-notebook-panel.component.spec.ts:98
- Avoid asserting on
SafeResourceUrlstringification here; instead spy onDomSanitizer.bypassSecurityTrustResourceUrland assert that the component stores the returned SafeResourceUrl when the panel becomes visible.
it("should fetch and sanitize URL when panel becomes visible", async () => {
vi.spyOn(component, "checkIframeRef").mockImplementation(() => {});
mockJupyterPanelService.jupyterNotebookPanelVisible$.next(true);
await fixture.whenStable();
fixture.detectChanges();
expect(mockNotebookMigrationService.getJupyterIframeURL).toHaveBeenCalled();
expect(component.jupyterUrl.toString()).toContain("http://localhost:8888");
});
frontend/src/app/workspace/component/jupyter-notebook-panel/jupyter-notebook-panel.component.spec.ts:120
- Asserting that the rendered iframe
srccontains the raw URL is brittle (DomSanitizer serialization is not stable). Just assert that the iframe exists and that the[src]binding produced asrcattribute, similar toFlarumComponenttests.
it("should render the iframe once a URL is available", async () => {
vi.spyOn(component, "checkIframeRef").mockImplementation(() => {});
mockJupyterPanelService.jupyterNotebookPanelVisible$.next(true);
await fixture.whenStable();
fixture.detectChanges();
const iframe = fixture.nativeElement.querySelector("iframe");
expect(iframe).not.toBeNull();
expect(iframe.getAttribute("src")).toContain("http://localhost:8888");
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…to migration-tool-jupyter-panel
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
frontend/src/app/workspace/service/jupyter-panel/jupyter-panel.service.ts:246
- The delete endpoint’s response is typed as
DeleteNotebookResponsewith asuccessflag/message, butdeleteJupyterNotebook()clears local state unconditionally on any HTTP 200 (even ifsuccess: false). This can make the UI claim deletion succeeded while the notebook still exists on the backend. Checkresp.successand only clear/hide on success; otherwise notify and keep the panel open.
this.notebookMigrationService.deleteNotebookAndMapping(wid).subscribe({
next: () => {
this.hideAndClearLocalState();
this.jupyterNotebookExists.next(false);
this.clearHighlights();
frontend/src/app/workspace/component/jupyter-notebook-panel/jupyter-notebook-panel.component.ts:84
ngAfterViewInit()callscheckIframeRef()unconditionally. If the panel is already visible on component creation (BehaviorSubject emitstrueinngOnInit) but the URL fetch hasn’t completed yet, the iframe is still gated by*ngIf="jupyterUrl"andcheckIframeRef()will log "Jupyter Iframe reference not found." even though this is a normal transient state. SincecheckIframeRef()is already triggered after a successful URL fetch, avoid calling it here.
ngAfterViewInit(): void {
// Ensure iframe is handled after it's available in the DOM
this.checkIframeRef();
}
frontend/src/app/workspace/service/jupyter-panel/jupyter-panel.service.ts:107
- This comment says the workflow-change handler drops the outgoing workflow’s mapping, but
hideAndClearLocalState()deletes the mapping forworkflowActionService.getWorkflow().wid(i.e., the current/new workflow emitted byworkflowMetaDataChanged()). Please update the comment to reflect the actual behavior to avoid confusion during future debugging.
// On every workflow change, hide the panel and drop the outgoing
// workflow's stale in-memory mapping, and clear the highlight index, so a
// switch to a workflow without a stored notebook can't leave the previous
// workflow's highlights active. This is local cleanup only: it must never
// delete from the backend, or switching workflows would erase notebooks.
mengw15
left a comment
There was a problem hiding this comment.
LGTM, thanks for addressing the comments
What changes were proposed in this PR?
Adds
JupyterNotebookPanelComponent, a draggable iframe panel that hosts JupyterLab inside the Texera workspace, mounts it in the workspace shell, and wires its delete button to remove the current workflow's stored notebook through the panel service.frontend/src/app/workspace/component/jupyter-notebook-panel/jupyter-notebook-panel.component.tsJupyterNotebookPanelComponent(standalone)jupyterUrlis typedSafeResourceUrl | nulland starts asnull.ngOnInitsubscribes tojupyterPanelService.jupyterNotebookPanelVisible$. Each emission setsisVisible. When the panel becomes visible it fetches the iframe URL fromnotebookMigrationService.getJupyterIframeURL()(a rejected fetch is caught and yieldsnullwithout ending the subscription), then assignsjupyterUrlto the sanitized URL (viaDomSanitizer.bypassSecurityTrustResourceUrl) or tonull, and callscheckIframeRef(). Anullresult (panel hidden, or a failed or empty fetch) clearsjupyterUrlso a stale URL is never shown.ngAfterViewInitcallscheckIframeRef(), which registers the iframe with the panel service viasetIframeRefinside asetTimeout, only when the panel is visible andiframeRefis present, and stays silent when the panel is hidden.deletePanel()callsjupyterPanelService.deleteJupyterNotebook().minimizePanel()callsjupyterPanelService.minimizeJupyterNotebookPanel(); visibility is driven by the observable.frontend/src/app/workspace/component/jupyter-notebook-panel/jupyter-notebook-panel.component.htmlcdkDragpanel gated on*ngIf="isVisible", withcdkDragHandleon the header so only the header is draggable.nz-popconfirmconfirmation before deleting.<iframe [src]="jupyterUrl">gated on*ngIf="jupyterUrl", so the iframe renders only once a sanitized URL is available.frontend/src/app/workspace/component/jupyter-notebook-panel/jupyter-notebook-panel.component.scssfrontend/src/app/workspace/component/workspace.component.tsandworkspace.component.htmlWorkspaceComponentimportsJupyterNotebookPanelComponentand mounts<texera-jupyter-notebook-panel>in the workspace shell.frontend/src/app/workspace/service/jupyter-panel/jupyter-panel.service.tsdeleteJupyterNotebook(): deletes the current workflow's stored notebook vianotebookMigrationService.deleteNotebookAndMapping(wid). On success it hides the panel, drops the in-memory mapping, resetsjupyterNotebookExists$, and unhighlights all operators and links. On failure it shows a notification and leaves the panel open. An unsaved workflow (nowid) resets local state without a backend call.hideAndClearLocalState()(private): hides the panel and drops the current workflow's in-memory mapping only. The workflow-change handler ininit()uses this, so switching workflows never deletes a notebook from the backend.clearHighlights()(private): unhighlights all operators and links and clears the highlight index.frontend/src/app/workspace/service/notebook-migration/notebook-migration.service.tsdeleteNotebookAndMapping(wid): sends aPOSTto/notebook-migration/delete-notebook-and-mapping, short-circuiting when the feature is disabled.Any related issues, documentation, discussions?
Closes #5270
Parent issue #4301
How was this PR tested?
Unit tests (Vitest):
jupyter-notebook-panel.component.spec.ts: visibility transitions, URL fetch and sanitization, the rejected-fetch path (the subscription stays alive andjupyterUrlstaysnull), the iframe rendering only when a URL is present and being removed on hide, iframe ref registration and the silent-when-hidden behavior, and delegation of the minimize and delete buttons.jupyter-panel.service.spec.ts:deleteJupyterNotebookcalls the backend, hides the panel, resetsjupyterNotebookExists$, and unhighlights on success; keeps the panel open and notifies on failure; and a guard that switching workflows never triggers a backend delete.Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 4.8)