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;
+ }
}