feat(python-notebook-migration, frontend): add Jupyter notebook iframe panel component - #5271
feat(python-notebook-migration, frontend): add Jupyter notebook iframe panel component#5271zyratlo wants to merge 24 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5271 +/- ##
============================================
+ Coverage 79.57% 79.60% +0.02%
Complexity 3832 3832
============================================
Files 1160 1162 +2
Lines 46192 46210 +18
Branches 5145 5140 -5
============================================
+ Hits 36757 36784 +27
+ Misses 7797 7796 -1
+ Partials 1638 1630 -8
*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.
| <button | ||
| class="delete-button" | ||
| nz-popconfirm | ||
| nzPopconfirmTitle="Confirm to delete Jupyter notebook" |
There was a problem hiding this comment.
This button says "Confirm to delete Jupyter notebook", but closePanel() → closeJupyterNotebookPanel() only hides the panel and drops the in-memory mapping — the DB rows stay. That leaves a state the user can't get out of from the UI:
- Click delete → mapping cleared, panel hidden.
jupyterNotebookExists$is not reset (it's only set ininit()), so feat(python-notebook-migration, frontend): add AI-generate-workflow modal, panel host, and LLM loading spinner #5273's menu button stays visible. - Click that button →
openJupyterNotebookPanel()checkshasMapping(), finds nothing, and warns "No Jupyter notebook associated with this workflow" instead of opening. - The notebook is still in the DB; the only way back is to switch workflows and return, which re-fetches and restores the mapping.
So today "delete" doesn't delete, but it does break reopening for the rest of the session. Deferring the real backend delete is fine — #4301 lists it as a requirement ("can also delete the notebook associated with the workflow"). Could you link the issue tracking it from the description and here?
A few things that might be worth capturing in that issue, since they aren't obvious from the current code:
closeJupyterNotebookPanel()is also called byinit()on every workflow change. Dropping the in-memory mapping there is right (it's just cache eviction —fetchNotebookAndMappingrestores it on the way back), but a backend delete can't live in the same method, or switching workflows would destroy the stored notebook. Splitting "hide the panel" from "delete the notebook" keeps the two apart.notebook.widis UNIQUE andworkflow_notebook_mappinghasFOREIGN KEY (wid, nid) ... ON DELETE CASCADE, so a singleDELETE FROM notebook WHERE wid = ?covers both tables.- Beyond the DB rows: the in-memory mapping,
jupyterNotebookExists$,cellToHighlightMapping, and thenotebook.ipynbstill sitting in the Jupyter container all need clearing — and ideally only after the backend confirms, so a failed request doesn't leave the same dead end.
Not blocking.
There was a problem hiding this comment.
Thinking about this more — since the backend has no delete endpoint yet, would it be cleaner to either drop the delete button from this PR (minimize alone is enough for now) or land the backend delete first?
There was a problem hiding this comment.
I think it makes more sense to remove the close/delete from this PR and add it in a separate one with the rest of the backend delete logic. So this PR only has the minimize feature.
What changes were proposed in this PR?
Introduces
JupyterNotebookPanelComponent, the draggable iframe shell that hosts JupyterLab inside the Texera workspace. It consumes the visibility surface added inmigration-tool-panel-controls(jupyterNotebookPanelVisible$/closeJupyterNotebookPanel/minimizeJupyterNotebookPanel) and registers itself with the panel service viasetIframeRefonce itsViewChildis in the DOM.frontend/src/app/workspace/component/jupyter-notebook-panel/jupyter-notebook-panel.component.tsJupyterNotebookPanelComponentngOnInitsubscribes tojupyterPanelService.jupyterNotebookPanelVisible$; on each emission it setsisVisible, and when visibility flips totrueit fetches the iframe URL fromnotebookMigrationService.getJupyterIframeURL(), runs it throughDomSanitizer.bypassSecurityTrustResourceUrl, stores the result injupyterUrl, and callscheckIframeRef()ngAfterViewInitcallscheckIframeRef(), which registers the iframe with the panel service viasetIframeRef(deferred by asetTimeoutand guarded onisVisibleand theiframeRefbeing present)closePanel()callsjupyterPanelService.closeJupyterNotebookPanel()minimizePanel()setsisVisibletofalseand callsjupyterPanelService.minimizeJupyterNotebookPanel().frontend/src/app/workspace/component/jupyter-notebook-panel/jupyter-notebook-panel.component.htmlcdkDrag-enabled panel with header (title + close/minimize buttons; close usesnz-popconfirmfor a "delete" confirmation)<iframe [src]="jupyterUrl">body bound to the sanitized URL, inside a panel container gated on*ngIf="isVisible".frontend/src/app/workspace/component/jupyter-notebook-panel/jupyter-notebook-panel.component.scssfrontend/src/app/workspace/component/jupyter-notebook-panel/jupyter-notebook-panel.component.spec.tsfrontend/src/app/app.module.tsJupyterNotebookPanelComponentand adds it to the@NgModule.declarationsarray.Note on the close/delete button
The panel's close/delete button currently hides the panel and clears the in-memory notebook mapping. It does not yet delete the notebook from the database. Full end-to-end deletion (a backend delete endpoint, removing the persisted notebook and mapping rows, resetting the notebook-exists signal, and clearing the notebook loaded in Jupyter) is intentionally out of scope for this PR, which covers only the panel component.
That work will land in a follow-up PR merged after this one. The button's "delete" wording and icon are left as-is here so the follow-up does not need to churn them back; they become accurate once the deletion feature merges.
Any related issues, documentation, discussions?
Closes #5270
Parent issue #4301
How was this PR tested?
jupyter-notebook-panel.component.spec.tsJupyterPanelService(via a controllableSubject<boolean>for the visibility observable) andNotebookMigrationService(returning a fixture URL)Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 4.8)