This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-5256-0b34fc9c487bd98b3ee54e6feccc205dd261283c in repository https://gitbox.apache.org/repos/asf/texera.git
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
