This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new b2d216a2ad feat(python-notebook-migration): add JupyterLab docker for
notebook migration tool (#5256)
b2d216a2ad is described below
commit b2d216a2ad728454d354a45de4ea5de1e391f81f
Author: Ryan Zhang <[email protected]>
AuthorDate: Tue Jun 16 16:58:59 2026 -0700
feat(python-notebook-migration): add JupyterLab docker for notebook
migration tool (#5256)
### What changes were proposed in this PR?
Introduces the local JupyterLab docker that the upcoming
notebook-migration microservice will talk to. Three files are added
under `notebook-migration-service/src/main/resources/`:
- **`Dockerfile`** — `FROM jupyter/base-notebook:notebook-6.5.4`;
`COPY`s `custom.js` into `/home/jovyan/.jupyter/custom/custom.js` and
fixes ownership.
- **`docker-compose.yml`** — runs JupyterLab as `texera-jupyter` on host
port `9100`. Token/password auth disabled, XSRF check disabled, CSP set
to allow `frame-ancestors http://localhost:*` so Texera can embed it in
an iframe. Default URL is
`/tree`.
- **`custom.js`** — JupyterLab iframe customization. Posts `cellClicked`
messages (with cell UUID) to `window.parent` and listens for
`triggerCellClick` to scroll/highlight target cells.
### Any related issues, documentation, discussions?
Closes #5255
Parent-issue #4301
### How was this PR tested?
Verified locally that the stack comes up cleanly and Jupyter is
reachable.
### Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 4.7)
---
.../src/main/resources/Dockerfile | 31 +++++++
.../src/main/resources/custom.js | 103 +++++++++++++++++++++
.../src/main/resources/docker-compose.yml | 44 +++++++++
.../src/main/resources/start-texera-jupyter.sh | 38 ++++++++
4 files changed, 216 insertions(+)
diff --git a/notebook-migration-service/src/main/resources/Dockerfile
b/notebook-migration-service/src/main/resources/Dockerfile
new file mode 100644
index 0000000000..699b271943
--- /dev/null
+++ b/notebook-migration-service/src/main/resources/Dockerfile
@@ -0,0 +1,31 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+FROM jupyter/base-notebook:notebook-6.5.4
+
+# Copy custom JavaScript for Jupyter and the startup script
+COPY custom.js /home/jovyan/.jupyter/custom/custom.js
+COPY start-texera-jupyter.sh /usr/local/bin/start-texera-jupyter.sh
+
+# Ensure correct permissions. custom.js must stay writable by jovyan so the
+# startup script can substitute the origin placeholder at runtime.
+USER root
+RUN mkdir -p /home/jovyan/.jupyter/custom && \
+ chown -R jovyan:users /home/jovyan/.jupyter && \
+ chmod +x /usr/local/bin/start-texera-jupyter.sh
+
+USER jovyan
diff --git a/notebook-migration-service/src/main/resources/custom.js
b/notebook-migration-service/src/main/resources/custom.js
new file mode 100644
index 0000000000..70fc4ce931
--- /dev/null
+++ b/notebook-migration-service/src/main/resources/custom.js
@@ -0,0 +1,103 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// The Texera app origin. The "__TEXERA_ORIGIN__" placeholder is substituted at
+// container startup by start-texera-jupyter.sh from the TEXERA_ORIGIN env var
+// (defaults to http://localhost:4200), so deployments under a real hostname
work
+// without editing this file.
+const TEXERA_ORIGIN = "__TEXERA_ORIGIN__";
+
+// Use Jupyter's event system to ensure the notebook is fully loaded
+require(["base/js/events"], function (events) {
+ events.on("kernel_ready.Kernel", function () {
+
+ // Attach click event listener to cells. kernel_ready.Kernel fires on every
+ // kernel (re)start, so remove any previously bound handler first to avoid
+ // stacking duplicate listeners that would post N messages per click.
+ $("#notebook-container").off("click", ".cell").on("click", ".cell",
function (event) {
+ const cell = $(this);
+ const index = $(".cell").index(cell);
+ const cellContent = cell.find(".input_area").text();
+
+ // Get the UUID from the cell's metadata, or use "N/A" if it doesn't
exist
+ const cellUUID = Jupyter.notebook.get_cell(index).metadata.uuid || 'N/A';
+
+ // Send a message to the parent window (Texera app)
+ window.parent.postMessage(
+ { action: "cellClicked", cellIndex: index, cellContent: cellContent,
cellUUID: cellUUID },
+ TEXERA_ORIGIN
+ );
+ });
+ });
+});
+
+// Listen for messages from the Texera app (or parent window)
+window.addEventListener("message", function (event) {
+ // Verify the message origin
+ if (event.origin !== TEXERA_ORIGIN) {
+ console.warn("Message received from unrecognized origin:", event.origin);
+ return;
+ }
+
+ if (event.data.action === "triggerCellClick") {
+ const operatorCellUUIDs = event.data.operators || [];
+
+ if (!operatorCellUUIDs.length) {
+ console.error("No valid operator UUIDs provided in the message.");
+ return; // Exit if no UUIDs are provided
+ }
+
+ operatorCellUUIDs.forEach((cellUUID) => {
+ // Search for the cell by UUID
+ const allCells = Jupyter.notebook.get_cells();
+ const targetCell = allCells.find((cell) => cell.metadata.uuid ===
cellUUID);
+
+ if (targetCell) {
+ const cellIndex = Jupyter.notebook.find_cell_index(targetCell);
+
+ // Scroll to and highlight the cell
+ let cell = document.querySelectorAll(".cell")[cellIndex];
+ if (cell) {
+ cell.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ cell.classList.add("highlighted");
+
+ // Remove the highlight after 3 seconds
+ setTimeout(() => {
+ cell.classList.remove("highlighted");
+ }, 3000);
+ } else {
+ console.error(`Cell not found in the DOM for index ${cellIndex}.`);
+ }
+ } else {
+ console.error(`No cell found with UUID: ${cellUUID}`);
+ }
+ });
+ } else {
+ console.warn("Received unknown action:", event.data.action);
+ }
+}, false);
+
+// Add custom CSS for highlighted cells
+const style = document.createElement('style');
+style.innerHTML = `
+ .cell.highlighted {
+ background-color: lightyellow;
+ }
+`;
+document.head.appendChild(style);
diff --git a/notebook-migration-service/src/main/resources/docker-compose.yml
b/notebook-migration-service/src/main/resources/docker-compose.yml
new file mode 100644
index 0000000000..d442a57386
--- /dev/null
+++ b/notebook-migration-service/src/main/resources/docker-compose.yml
@@ -0,0 +1,44 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+name: texera-jupyter
+services:
+
+ jupyter:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: texera-jupyter
+ restart: unless-stopped
+ ports:
+ - "9100:8888"
+ environment:
+ # Texera app origin, used for the iframe CSP frame-ancestors and the
+ # postMessage origin checks in custom.js. Override for non-local
deployments.
+ - TEXERA_ORIGIN=http://localhost:4200
+ # Weak default token so the server is not fully open. The Texera-side
iframe
+ # URL must pass this through ?token=<value>.
+ - JUPYTER_TOKEN=texera
+ command: ["start-texera-jupyter.sh"]
+ healthcheck:
+ # /api returns the server version without requiring the token, so it is a
+ # reliable liveness probe even with auth enabled.
+ test: ["CMD", "python", "-c", "import urllib.request;
urllib.request.urlopen('http://localhost:8888/api')"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ start_period: 15s
diff --git
a/notebook-migration-service/src/main/resources/start-texera-jupyter.sh
b/notebook-migration-service/src/main/resources/start-texera-jupyter.sh
new file mode 100644
index 0000000000..2bfb5a3baf
--- /dev/null
+++ b/notebook-migration-service/src/main/resources/start-texera-jupyter.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -euo pipefail
+
+# Texera app origin used by custom.js (postMessage targetOrigin + inbound
origin
+# check) and by the iframe CSP frame-ancestors. Override TEXERA_ORIGIN for
+# deployments under a real hostname; defaults to the local dev origin.
+TEXERA_ORIGIN="${TEXERA_ORIGIN:-http://localhost:4200}"
+
+# Weak default token so the server is not fully open to anyone reachable on the
+# published port. The Texera-side iframe URL must pass this through
?token=<value>.
+JUPYTER_TOKEN="${JUPYTER_TOKEN:-texera}"
+
+# Substitute the origin placeholder in custom.js before the server starts
serving it.
+sed -i "s|__TEXERA_ORIGIN__|${TEXERA_ORIGIN}|g"
/home/jovyan/.jupyter/custom/custom.js
+
+exec start-notebook.sh \
+ --NotebookApp.token="${JUPYTER_TOKEN}" \
+ --NotebookApp.password='' \
+ --NotebookApp.disable_check_xsrf=True \
+ --NotebookApp.tornado_settings="{'headers': {'Content-Security-Policy':
'frame-ancestors ${TEXERA_ORIGIN}'}}" \
+ --NotebookApp.default_url=/tree