This is an automated email from the ASF dual-hosted git repository.
voidmatcha 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 3faf553a53 [ZEPPELIN-6533] Apply the notebook search term after the
paragraph views exist
3faf553a53 is described below
commit 3faf553a532df600f4d9b7544e833b3390d1c51c
Author: κΉμλ <[email protected]>
AuthorDate: Fri Aug 7 00:22:23 2026 +0900
[ZEPPELIN-6533] Apply the notebook search term after the paragraph views
exist
### What is this PR for?
The notebook component reads the `term` query param in `ngOnInit`, which
runs before `<at>ViewChildren` resolves and before the note arrives over the
WebSocket. The initial term was applied to a paragraph query list that did not
exist yet, and nothing re-applied it once the paragraphs rendered, so opening a
note through a `?term=...` deep link (for example clicking a notebook search
result) never highlighted the matching text.
This keeps the term on the notebook component and re-applies it in
`ngAfterViewInit` and whenever the paragraph query list changes. The code
editor keeps the term as well, because Monaco loads asynchronously and would
otherwise ignore a term that arrived before the editor was ready.
The guard suggested in the issue
(`listOfNotebookParagraphComponent?.forEach(...)`) is already on master, so
`onParagraphSearch` does not throw today. The access stays guarded here.
### What type of PR is it?
Bug Fix
### Todos
* [x] - Apply the search term once the paragraph views exist
* [x] - Apply the search term once the Monaco editor is ready
* [x] - Add an e2e regression test for the `term` deep link
### What is the Jira issue?
* https://issues.apache.org/jira/browse/ZEPPELIN-6533
### How should this be tested?
* Automated: `e2e/tests/notebook/search/editor-search.spec.ts` gains
"highlights the term carried by a deep link when the notebook opens". Run it
with `npm run e2e:fast -- tests/notebook/search/editor-search.spec.ts` in
`zeppelin-web-angular`. The new test fails on master (0 highlights) and passes
with this change; the rest of the spec and the notebook keyboard spec stay
green.
* Manual: create a note, put `alpha target beta target gamma target` in a
paragraph, then open `/#/notebook/<noteId>?term=target` coming from another
page. Every occurrence of `target` is highlighted. The same applies when
clicking a result on the notebook search page, which navigates with `paragraph`
and `term` query params.
### Screenshots (if appropriate)
Before: nothing is highlighted when the note opens through the deep link.
After: the three `target` occurrences are highlighted.
### Questions:
* Does the license files need to update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No
Closes #5378 from kimyenac/ZEPPELIN-6533.
Signed-off-by: YONGJAE LEE <[email protected]>
---
.../e2e/models/editor-search-page.ts | 24 ++++++++++++++
.../tests/notebook/search/editor-search.spec.ts | 37 ++++++++++++++++++++++
zeppelin-web-angular/e2e/utils.ts | 30 ++++++++++++++++++
.../pages/workspace/notebook/notebook.component.ts | 21 ++++++++++--
.../paragraph/code-editor/code-editor.component.ts | 4 +++
.../notebook/paragraph/paragraph.component.ts | 14 ++++++--
6 files changed, 126 insertions(+), 4 deletions(-)
diff --git a/zeppelin-web-angular/e2e/models/editor-search-page.ts
b/zeppelin-web-angular/e2e/models/editor-search-page.ts
index d47ba32fc4..a7fbc2d60c 100644
--- a/zeppelin-web-angular/e2e/models/editor-search-page.ts
+++ b/zeppelin-web-angular/e2e/models/editor-search-page.ts
@@ -22,6 +22,8 @@ export class EditorSearchPage extends BasePage {
readonly replaceInput: Locator;
readonly matchesCount: Locator;
readonly matchHighlights: Locator;
+ readonly termHighlights: Locator;
+ readonly showHideCodeButton: Locator;
readonly nextMatchButton: Locator;
readonly previousMatchButton: Locator;
readonly toggleReplaceButton: Locator;
@@ -42,6 +44,11 @@ export class EditorSearchPage extends BasePage {
this.matchesCount = this.findWidget.locator('.matchesCount').first();
// Monaco decorates every match with .findMatch and the active one with
.currentFindMatch.
this.matchHighlights = this.editor.locator('.findMatch,
.currentFindMatch');
+ // The `term` query param highlights through Zeppelin's own decoration
class, not Monaco's find widget.
+ this.termHighlights = this.editor.locator('.editor-search-highlight');
+ this.showHideCodeButton = page
+ .locator('zeppelin-notebook-paragraph-control
a[nzTooltipTitle="Show/hide the code"]')
+ .first();
this.nextMatchButton = this.findWidget.locator('.button.next,
[title^="Next Match"]').first();
this.previousMatchButton = this.findWidget.locator('.button.previous,
[title^="Previous Match"]').first();
this.toggleReplaceButton = this.findWidget.locator('.button.toggle,
[title^="Toggle Replace"]').first();
@@ -54,6 +61,23 @@ export class EditorSearchPage extends BasePage {
await expect(this.editor).toBeVisible({ timeout: 15000 });
}
+ async openNotebookWithSearchTerm(noteId: string, term: string):
Promise<void> {
+ await this.navigateToNotebookWithSearchTerm(noteId, term);
+ await expect(this.editor).toBeVisible({ timeout: 15000 });
+ }
+
+ // Separate from openNotebookWithSearchTerm: a paragraph whose editor starts
hidden renders no
+ // Monaco instance, so the caller cannot wait for the editor before acting.
+ async navigateToNotebookWithSearchTerm(noteId: string, term: string):
Promise<void> {
+ await
this.page.goto(`/#/notebook/${noteId}?term=${encodeURIComponent(term)}`);
+ await waitForZeppelinReady(this.page);
+ }
+
+ async showCode(): Promise<void> {
+ await this.showHideCodeButton.click();
+ await expect(this.editor).toBeVisible({ timeout: 15000 });
+ }
+
async setEditorContent(content: string): Promise<void> {
await this.editor.click();
// Key off the browser, not the host: Monaco follows the browser UA's
keymap, and
diff --git
a/zeppelin-web-angular/e2e/tests/notebook/search/editor-search.spec.ts
b/zeppelin-web-angular/e2e/tests/notebook/search/editor-search.spec.ts
index a7420914f1..3960559c1f 100644
--- a/zeppelin-web-angular/e2e/tests/notebook/search/editor-search.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/notebook/search/editor-search.spec.ts
@@ -17,6 +17,8 @@ import {
createTestNotebook,
PAGES,
performLoginIfRequired,
+ setParagraphEditorHidden,
+ setParagraphText,
skipWhenAuthenticationIsStillRequired,
waitForNotebookLinks,
waitForZeppelinReady
@@ -77,6 +79,41 @@ test.describe('Notebook editor search', () => {
await expect(editorSearchPage.matchHighlights).toHaveCount(3);
});
+ test('highlights the term carried by a deep link when the notebook opens',
async ({ page }) => {
+ const { noteId, paragraphId } = await createTestNotebook(page);
+
+ await test.step('Given a paragraph containing the term three times', async
() => {
+ await setParagraphText(page, noteId, paragraphId, 'alpha target beta
target gamma target');
+ });
+
+ await test.step('When the notebook is opened with the term in the query
string', async () => {
+ await editorSearchPage.openNotebookWithSearchTerm(noteId, 'target');
+ });
+
+ await test.step('Then every occurrence is highlighted', async () => {
+ await expect(editorSearchPage.termHighlights).toHaveCount(3);
+ });
+ });
+
+ test('highlights the term carried by a deep link when a hidden editor is
shown', async ({ page }) => {
+ const { noteId, paragraphId } = await createTestNotebook(page);
+
+ await test.step('Given a paragraph whose editor is hidden and contains the
term three times', async () => {
+ await setParagraphText(page, noteId, paragraphId, 'alpha target beta
target gamma target');
+ await setParagraphEditorHidden(page, noteId, paragraphId, true);
+ });
+
+ await test.step('When the notebook is opened with the term and the code is
shown again', async () => {
+ await editorSearchPage.navigateToNotebookWithSearchTerm(noteId,
'target');
+ await expect(editorSearchPage.editor).toHaveCount(0);
+ await editorSearchPage.showCode();
+ });
+
+ await test.step('Then every occurrence is highlighted', async () => {
+ await expect(editorSearchPage.termHighlights).toHaveCount(3);
+ });
+ });
+
test('replaces all matches in the editor search widget', async ({ page }) =>
{
const { noteId } = await createTestNotebook(page);
diff --git a/zeppelin-web-angular/e2e/utils.ts
b/zeppelin-web-angular/e2e/utils.ts
index cfc3c110e1..d4500d3f6e 100644
--- a/zeppelin-web-angular/e2e/utils.ts
+++ b/zeppelin-web-angular/e2e/utils.ts
@@ -482,6 +482,36 @@ const createNotebookViaRest = async (
return { noteId, paragraphId };
};
+export const setParagraphText = async (
+ page: Page,
+ noteId: string,
+ paragraphId: string,
+ text: string
+): Promise<void> => {
+ const response = await
page.request.put(`/api/notebook/${noteId}/paragraph/${paragraphId}`, {
+ data: { text },
+ failOnStatusCode: false
+ });
+ if (!response.ok()) {
+ throw new Error(`Update paragraph REST request failed:
${response.status()} ${await response.text()}`);
+ }
+};
+
+export const setParagraphEditorHidden = async (
+ page: Page,
+ noteId: string,
+ paragraphId: string,
+ editorHide: boolean
+): Promise<void> => {
+ const response = await
page.request.put(`/api/notebook/${noteId}/paragraph/${paragraphId}/config`, {
+ data: { editorHide },
+ failOnStatusCode: false
+ });
+ if (!response.ok()) {
+ throw new Error(`Update paragraph config REST request failed:
${response.status()} ${await response.text()}`);
+ }
+};
+
interface CreateTestNotebookWithNameOptions {
folderPath?: string | null;
namePrefix?: string;
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
index 408ca1af28..552e0f8c8d 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
+++
b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
@@ -11,6 +11,7 @@
*/
import {
+ AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
@@ -57,9 +58,10 @@ import { NotebookParagraphComponent } from
'./paragraph/paragraph.component';
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
})
-export class NotebookComponent extends MessageListenersManager implements
OnInit, OnDestroy {
+export class NotebookComponent extends MessageListenersManager implements
OnInit, AfterViewInit, OnDestroy {
@ViewChildren(NotebookParagraphComponent) listOfNotebookParagraphComponent!:
QueryList<NotebookParagraphComponent>;
private destroy$ = new Subject<void>();
+ private searchTerm = '';
note?: Exclude<Note['note'], undefined>;
permissions?: Permissions;
selectId: string | null = null;
@@ -272,7 +274,8 @@ export class NotebookComponent extends
MessageListenersManager implements OnInit
}
onParagraphSearch(term: string) {
- this.listOfNotebookParagraphComponent?.forEach(comp =>
comp.highlightMatches(term || ''));
+ this.searchTerm = term || '';
+ this.highlightSearchTerm();
}
saveParagraph(id: string) {
@@ -485,6 +488,13 @@ export class NotebookComponent extends
MessageListenersManager implements OnInit
});
}
+ ngAfterViewInit(): void {
+ this.highlightSearchTerm();
+
this.listOfNotebookParagraphComponent.changes.pipe(takeUntil(this.destroy$)).subscribe(()
=> {
+ this.highlightSearchTerm();
+ });
+ }
+
removeParagraphFromNgZ(): void {
if (this.note && Array.isArray(this.note.paragraphs)) {
this.note.paragraphs.forEach(p => {
@@ -501,4 +511,11 @@ export class NotebookComponent extends
MessageListenersManager implements OnInit
this.destroy$.complete();
this.titleService.setTitle('Zeppelin');
}
+
+ // The term can arrive before the paragraphs exist: the query param
subscription emits during
+ // ngOnInit, and the paragraphs themselves are only rendered once the note
arrives over the
+ // WebSocket. Keep the term and (re)apply it whenever the paragraph views
change.
+ private highlightSearchTerm(): void {
+ this.listOfNotebookParagraphComponent?.forEach(comp =>
comp.highlightMatches(this.searchTerm));
+ }
}
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
index 093a34e11c..ccb1a1669b 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
+++
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
@@ -66,6 +66,7 @@ export class NotebookParagraphCodeEditorComponent
private editor?: IStandaloneCodeEditor;
private monacoDisposables: IDisposable[] = [];
private highlightDecorations: DecorationIdentifier[] = [];
+ private searchTerm = '';
height = 18;
interpreterName?: string;
@@ -217,6 +218,8 @@ export class NotebookParagraphCodeEditorComponent
this.initEditorFocus();
this.initCompletionService(this.editor);
this.setEditorValue(this.editor);
+ // A term requested before Monaco finished loading was only stored, not
applied yet.
+ this.highlightMatches(this.searchTerm);
setTimeout(() => {
this.autoAdjustEditorHeight();
});
@@ -356,6 +359,7 @@ export class NotebookParagraphCodeEditorComponent
}
highlightMatches(term: string) {
+ this.searchTerm = term;
if (!this.editor || !term) {
// Remove previous highlights if term is empty
this.highlightDecorations =
this.editor?.deltaDecorations(this.highlightDecorations, []) || [];
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
index 8f72c2bbf3..186b4c595c 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
+++
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
@@ -84,8 +84,6 @@ export class NotebookParagraphComponent
implements OnInit, OnChanges, OnDestroy, AfterViewInit,
AngularKeyboardEventHandler
{
@HostBinding('attr.tabindex') tabindex = '-1';
- @ViewChild(NotebookParagraphCodeEditorComponent, { static: false })
- notebookParagraphCodeEditorComponent?: NotebookParagraphCodeEditorComponent;
@ViewChildren(NotebookParagraphResultComponent)
notebookParagraphResultComponents!:
QueryList<NotebookParagraphResultComponent>;
@Input() paragraph!: ParagraphItem;
@@ -145,9 +143,11 @@ export class NotebookParagraphComponent
@Output() readonly openSearchMenu = new EventEmitter();
private destroy$ = new Subject<void>();
+ private searchTerm = '';
private mode: Mode = 'command';
waitConfirmFromEdit = false;
+ notebookParagraphCodeEditorComponent?: NotebookParagraphCodeEditorComponent;
private keyBinderService: KeyBinder;
@@ -170,7 +170,17 @@ export class NotebookParagraphComponent
}
}
+ // The code editor sits behind an @if on `config.editorHide`, so it can
mount long after the
+ // search term arrived, and it mounts as a fresh instance that knows nothing
about the term.
+ // Setter injection replays the retained term the moment the editor becomes
available.
+ @ViewChild(NotebookParagraphCodeEditorComponent, { static: false })
+ set codeEditorComponent(component: NotebookParagraphCodeEditorComponent |
undefined) {
+ this.notebookParagraphCodeEditorComponent = component;
+ component?.highlightMatches(this.searchTerm);
+ }
+
highlightMatches(searchText: string) {
+ this.searchTerm = searchText;
this.notebookParagraphCodeEditorComponent?.highlightMatches(searchText);
}