This is an automated email from the ASF dual-hosted git repository.

tbonelee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new bd1ecc6dc4 [ZEPPELIN-6718] Carry every persisted field into a 
personalized user note
bd1ecc6dc4 is described below

commit bd1ecc6dc442c8b05124ae7afc1233006e21441d
Author: ChanHo Lee <[email protected]>
AuthorDate: Mon Sep 21 23:28:43 2026 +0900

    [ZEPPELIN-6718] Carry every persisted field into a personalized user note
    
    ### What is this PR for?
    
    Reading a personalized note does not return the note itself. 
`NotebookService.getNote` hands back a per-user copy built by 
`Note.getUserNote(user)`, and that copy is assembled by moving fields across 
one at a time, covering 5 of the 11 persisted fields.
    
    `path`, `defaultInterpreterGroup`, `version`, `info`, `noteParams` and 
`noteForms` are left null. Gson omits null fields, so those keys disappear from 
every response for a personalized note, over REST and WebSocket alike.
    
    `path` is the one that breaks the UI. It is not stored in the note file: 
the path comes from where the file sits and is reattached by `NoteManager` 
after load, so a copy that loses it has nowhere to recover it from. The front 
end reads it to detect the trash folder, `note.path.split('/')` throws on 
undefined, and the exception aborts change detection so the notebook action bar 
is never rendered. That includes the button that turns personalized mode back 
off, which means **a note switc [...]
    
    The server defect has been there since `getUserNote` was introduced in 
ZEPPELIN-1594. The classic UI guarded against the missing value:
    
    ```js
    // zeppelin-web/src/app/notebook/notebook.controller.js:267
    return note && note.path ? note.path.split('/')[1] === TRASH_FOLDER_ID : 
false;
    ```
    
    The Angular UI carries the same check without that guard, which is what 
finally exposed the server side.
    
    This PR copies the six missing fields. Because the copy is still written 
field by field, it also adds a regression test that walks every persisted field 
of `Note` by reflection and compares the copy against the original, so a field 
added to `Note` later and not copied here fails by name.
    
    The front end is left alone on purpose. `path` is a value the server must 
always send; typing it as optional or guarding at the call site would let the 
same class of server defect pass unnoticed again, which is exactly why this one 
went unnoticed for so long.
    
    ### What type of PR is it?
    Bug Fix
    
    ### Todos
    * [x] Copy the six fields `getUserNote` was dropping
    * [x] Add a reflection regression test that catches a future missing field
    
    ### What is the Jira issue?
    * [ZEPPELIN-6718](https://issues.apache.org/jira/browse/ZEPPELIN-6718)
    
    ### How should this be tested?
    
    ```
    ./mvnw test -pl zeppelin-server --am -Dtest=NoteTest
    ```
    
    10 tests pass. Reverting the change to `Note.java` while keeping the tests 
makes the two new ones fail and name the cause:
    
    ```
    [ERROR] Tests run: 10, Failures: 2, Errors: 0, Skipped: 0
    [ERROR]   NoteTest.userNoteKeepsEveryPersistedField:254 getUserNote dropped 
Note.defaultInterpreterGroup ==> expected: <spark> but was: <null>
    [ERROR]   NoteTest.userNoteKeepsThePath:233 expected: </folder/my note> but 
was: <null>
    ```
    
    Checked by hand as well. Create a note, switch it to personal mode, and 
open it. Before the change, `GET /api/notebook/{noteId}` comes back without 
`path` and the console repeats the `split` error while the action bar never 
renders. After it, `path` is back, no errors are logged, the action bar 
renders, and `Switch to collaboration mode` completes the switch 
(`personalizedMode: false`).
    
    ### Screenshots (if appropriate)
    N/A
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Closes #5494 from tbonelee/ZEPPELIN-6718.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 .../java/org/apache/zeppelin/notebook/Note.java    |  6 ++++
 .../org/apache/zeppelin/notebook/NoteTest.java     | 42 ++++++++++++++++++++++
 2 files changed, 48 insertions(+)

diff --git 
a/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/Note.java 
b/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/Note.java
index ccebad12dc..22c49dffb1 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/Note.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/notebook/Note.java
@@ -949,7 +949,13 @@ public class Note implements JsonSerializable {
     Note newNote = new Note();
     newNote.name = getName();
     newNote.id = getId();
+    newNote.path = path;
+    newNote.defaultInterpreterGroup = defaultInterpreterGroup;
+    newNote.version = version;
     newNote.setConfig(getConfig());
+    newNote.info = getInfo();
+    newNote.noteParams = getNoteParams();
+    newNote.noteForms = getNoteForms();
     newNote.angularObjects = getAngularObjects();
     newNote.setZeppelinConfiguration(zConf);
     newNote.setNoteParser(noteParser);
diff --git 
a/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/NoteTest.java 
b/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/NoteTest.java
index b7d1cb9ad7..1bdf9c71b4 100644
--- a/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/NoteTest.java
+++ b/zeppelin-server/src/test/java/org/apache/zeppelin/notebook/NoteTest.java
@@ -37,6 +37,8 @@ import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
 
 import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -221,4 +223,44 @@ class NoteTest {
     Note note2 = noteParser.fromJson(null, note.toJson());
     assertEquals(note2, note);
   }
+
+  @Test
+  void userNoteKeepsThePath() {
+    Note note = personalizedNote();
+
+    assertEquals("/folder/my note", note.getUserNote("user1").getPath());
+  }
+
+  @Test
+  void userNoteKeepsEveryPersistedField() throws IllegalAccessException {
+    Note note = personalizedNote();
+
+    Note userNote = note.getUserNote("user1");
+
+    for (Field field : Note.class.getDeclaredFields()) {
+      int modifiers = field.getModifiers();
+      if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) {
+        continue;
+      }
+      // The one field a user note is meant to differ in.
+      if ("paragraphs".equals(field.getName())) {
+        continue;
+      }
+      field.setAccessible(true);
+      assertEquals(field.get(note), field.get(userNote),
+          "getUserNote dropped Note." + field.getName());
+    }
+  }
+
+  private Note personalizedNote() {
+    Note note = new Note("/folder/my note", "spark", interpreterFactory, 
interpreterSettingManager,
+        paragraphJobListener, credentials, noteEventListener, zConf, 
noteParser);
+    note.setPersonalizedMode(true);
+    note.getConfig().put("config_1", "value_1");
+    note.getInfo().put("info_1", "value_1");
+    note.getNoteParams().put("param_1", "value_1");
+    note.getNoteForms().put("form_1", new TextBox("name", "default_name"));
+    note.addNewParagraph(AuthenticationInfo.ANONYMOUS);
+    return note;
+  }
 }

Reply via email to