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 b143a4fd72 fix(notebook-migration): apply the jupyter panel's 
read-only setup before the notebook loads (#8909)
b143a4fd72 is described below

commit b143a4fd72160f71ceb88fa80d790c5f8d146db6
Author: Ryan Zhang <[email protected]>
AuthorDate: Wed Oct 7 19:36:18 2026 +0000

    fix(notebook-migration): apply the jupyter panel's read-only setup before 
the notebook loads (#8909)
    
    ### What changes were proposed in this PR?
    
    The Jupyter panel's cells became editable as soon as a viewer clicked
    into one. Classic Notebook resets each editor's `readOnly` from
    `cell.is_editable()` on every focus and re-enables the keyboard manager
    at the same time, so setting `readOnly` once from `custom.js` did not
    hold. That setup also raced the notebook: `custom.js` loads
    asynchronously and usually misses `notebook_loaded`, and only worked
    because it was bound to `kernel_ready` too.
    
    This PR moves the setup into `custom-preload.js`, a hook Notebook 6.5
    provides that `main.js` waits for before it builds the notebook.
    Overrides placed there are always in effect:
    
    * `Cell.prototype.is_editable` returns false, so Notebook itself keeps
    every editor read only, including after focus.
    * `KeyboardManager.prototype.enable` is a no op, and the manager is
    disabled once the notebook loads, so focusing a cell no longer brings
    back Shift+Enter, `d d` and the other shortcuts.
    * `KernelSelector.prototype._spec_not_found` continues without a kernel
    instead of opening the "Kernel not found" dialog. The panel never runs
    cells, so a notebook whose kernel is not installed opens normally.
    Python notebooks still match `python3` and start their kernel as before.
    * The markdown `unrender` override moves over unchanged.
    
    `custom.js` keeps only the Texera messaging. Its cell click handler is
    now delegated from `document` and bound immediately, rather than on
    `kernel_ready`, so clicking a cell to highlight its operator also works
    when no kernel starts. It stays in `custom.js` because it needs the
    origin `start-texera-jupyter.sh` substitutes.
    
    `jupyter.dockerfile` copies the new file, and the local dev README lists
    it among the files that need an image rebuild.
    
    ### Any related issues, documentation, discussions?
    
    Closes #8908
    Parent-issue #4301
    
    R support (#8009) depends on this: R notebooks open in the panel without
    a kernel, which hit both the dialog and the editable cells.
    
    ### How was this PR tested?
    
    These files have no automated tests. Tested by hand in the Jupyter
    container, with real keystrokes:
    
    * An R notebook (no kernel installed): no dialog. Typing, Enter,
    Backspace, Shift+Enter, `d d` and `b` change nothing. Double clicking a
    markdown heading leaves it rendered. R syntax highlighting is intact,
    and the cell click handler is bound.
    * A Python notebook: the kernel still starts, highlighting is unchanged,
    and an editor stays read only after focus (before this change it became
    editable).
    * `custom-preload.js` is served by the rebuilt image. A server that was
    already running keeps serving Notebook's default stub until restarted,
    since Notebook caches static file locations.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Claude Opus 5.5)
---
 bin/dockerfiles/jupyter.dockerfile                 |  1 +
 bin/local-dev/README.md                            |  6 +-
 .../src/main/resources/custom-preload.js           | 51 ++++++++++++++++
 .../src/main/resources/custom.js                   | 70 +++++-----------------
 4 files changed, 69 insertions(+), 59 deletions(-)

diff --git a/bin/dockerfiles/jupyter.dockerfile 
b/bin/dockerfiles/jupyter.dockerfile
index 1f911c5de4..2d234c2a3e 100644
--- a/bin/dockerfiles/jupyter.dockerfile
+++ b/bin/dockerfiles/jupyter.dockerfile
@@ -29,6 +29,7 @@ FROM jupyter/base-notebook:notebook-6.5.4
 # The customizations live with notebook-migration-service, which owns the 
Jupyter
 # integration. Paths are repo-root relative: the build context is the repo 
root.
 COPY notebook-migration-service/src/main/resources/custom.js 
/home/jovyan/.jupyter/custom/custom.js
+COPY notebook-migration-service/src/main/resources/custom-preload.js 
/home/jovyan/.jupyter/custom/custom-preload.js
 COPY notebook-migration-service/src/main/resources/custom.css 
/home/jovyan/.jupyter/custom/custom.css
 COPY notebook-migration-service/src/main/resources/start-texera-jupyter.sh 
/usr/local/bin/start-texera-jupyter.sh
 
diff --git a/bin/local-dev/README.md b/bin/local-dev/README.md
index ab84a9fbf6..b9ccbfa10a 100644
--- a/bin/local-dev/README.md
+++ b/bin/local-dev/README.md
@@ -114,9 +114,9 @@ invocation.
 
 `jupyter` is the only managed service that runs from a Texera-built image 
instead of
 natively, so edits to its customizations under
-`notebook-migration-service/src/main/resources/` (`custom.js`, `custom.css`,
-`start-texera-jupyter.sh`) do nothing until the image is rebuilt. CI publishes 
it, but a
-local edit needs a local build under the same tag:
+`notebook-migration-service/src/main/resources/` (`custom.js`, 
`custom-preload.js`,
+`custom.css`, `start-texera-jupyter.sh`) do nothing until the image is 
rebuilt. CI
+publishes it, but a local edit needs a local build under the same tag:
 
 ```sh
 docker build -f bin/dockerfiles/jupyter.dockerfile -t 
ghcr.io/apache/texera-jupyter:latest .
diff --git a/notebook-migration-service/src/main/resources/custom-preload.js 
b/notebook-migration-service/src/main/resources/custom-preload.js
new file mode 100644
index 0000000000..2211fa3ddb
--- /dev/null
+++ b/notebook-migration-service/src/main/resources/custom-preload.js
@@ -0,0 +1,51 @@
+/**
+ * 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.
+ */
+
+// Setup that must be in place before the notebook loads. main.js waits for 
this
+// module, while custom.js loads alongside the notebook and can miss its 
events.
+define([
+  "base/js/events",
+  "notebook/js/cell",
+  "notebook/js/keyboardmanager",
+  "notebook/js/kernelselector",
+  "notebook/js/textcell",
+], function (events, cell, keyboardmanager, kernelselector, textcell) {
+  // The embedded notebook is a read-only reference view of the generated 
workflow.
+  // Notebook re-derives each editor's readOnly from is_editable() on every 
focus.
+  cell.Cell.prototype.is_editable = function () {
+    return false;
+  };
+
+  // Focusing an editor re-enables the keyboard manager, which would bring back
+  // shortcuts such as Shift+Enter to run a cell.
+  keyboardmanager.KeyboardManager.prototype.enable = function () {};
+  events.on("notebook_loaded.Notebook", function () {
+    Jupyter.keyboard_manager.disable();
+  });
+
+  // The panel never runs cells, so a notebook whose kernel is not installed 
(an R
+  // notebook, for example) opens without one instead of prompting for a 
kernel.
+  kernelselector.KernelSelector.prototype._spec_not_found = function () {
+    this.events.trigger("no_kernel.Kernel");
+  };
+
+  // Keep markdown cells rendered: overriding unrender() stops a double-click 
(or
+  // Enter) from dropping a markdown cell into its editable source view.
+  textcell.MarkdownCell.prototype.unrender = function () {};
+});
diff --git a/notebook-migration-service/src/main/resources/custom.js 
b/notebook-migration-service/src/main/resources/custom.js
index 672104dfc0..ad432c0659 100644
--- a/notebook-migration-service/src/main/resources/custom.js
+++ b/notebook-migration-service/src/main/resources/custom.js
@@ -23,62 +23,20 @@
 // 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
-      );
-    });
-  });
-});
-
-// Read-only viewing: the embedded notebook is a reference view of the 
generated
-// workflow, not an editing surface. Make every code cell's editor read-only 
and
-// disable Jupyter's keyboard shortcuts so cells cannot be edited or executed.
-// (The chrome hidden via custom.css already removes the run/save/menu 
controls.)
-require(["base/js/events"], function (events) {
-  function makeReadOnly() {
-    if (!window.Jupyter || !Jupyter.notebook) {
-      return;
-    }
-    // readOnly "true" blocks editing while still letting the
-    // editor take focus, so a viewer can select and copy the generated code.
-    Jupyter.notebook.get_cells().forEach(function (cell) {
-      if (cell.code_mirror) {
-        cell.code_mirror.setOption("readOnly", true);
-      }
-    });
-    if (Jupyter.keyboard_manager) {
-      Jupyter.keyboard_manager.disable();
-    }
-  }
-
-  // notebook_loaded fires once cells (and their editors) exist; kernel_ready
-  // fires on every (re)start and re-applies in case the manager was 
re-enabled.
-  events.on("notebook_loaded.Notebook", makeReadOnly);
-  events.on("kernel_ready.Kernel", makeReadOnly);
-});
-
-// Keep markdown cells rendered: overriding unrender() stops a double-click (or
-// Enter) from dropping a markdown cell into its editable source view. The
-// prototype override applies to all existing and future markdown cells.
-require(["notebook/js/textcell"], function (textcell) {
-  textcell.MarkdownCell.prototype.unrender = function () {};
+// Delegated from document, so it can be bound before the notebook or a kernel 
exists.
+$(document).on("click", "#notebook-container .cell", function () {
+  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)

Reply via email to