Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions zeppelin-web-angular/e2e/models/editor-search-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
createTestNotebook,
PAGES,
performLoginIfRequired,
setParagraphEditorHidden,
setParagraphText,
skipWhenAuthenticationIsStillRequired,
waitForNotebookLinks,
waitForZeppelinReady
Expand Down Expand Up @@ -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);

Expand Down
30 changes: 30 additions & 0 deletions zeppelin-web-angular/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 => {
Expand All @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class NotebookParagraphCodeEditorComponent
private editor?: IStandaloneCodeEditor;
private monacoDisposables: IDisposable[] = [];
private highlightDecorations: DecorationIdentifier[] = [];
private searchTerm = '';
height = 18;
interpreterName?: string;

Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -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, []) || [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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);
}

Expand Down
Loading