This is an automated email from the ASF dual-hosted git repository.
chanholee pushed a commit to branch branch-0.12
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/branch-0.12 by this push:
new b41f1c6c61 [ZEPPELIN-6330] Redirect to home with error if shared
paragraph(or note) doesn’t exist
b41f1c6c61 is described below
commit b41f1c6c610150da89cc67e8a37a85a2db72ebc4
Author: YONGJAE LEE(이용재) <[email protected]>
AuthorDate: Thu Oct 9 16:35:37 2025 +0900
[ZEPPELIN-6330] Redirect to home with error if shared paragraph(or note)
doesn’t exist
### What is this PR for?
If a user accesses a shared link (via **“Link this paragraph”**) for a
paragraph that does not exist, it now **redirects to the home screen** and
**displays an error modal** indicating that the paragraph does not exist. Added
tests to cover this behavior.
---
**Correct button text in Create Note modal** - db87e51
```
<nz-form-label>
<ng-container *ngIf="cloneNote; else importTpl">Clone
Note</ng-container>
<ng-template #importTpl>Note Name</ng-template>
</nz-form-label>
...
<button nz-button nzType="primary" (click)="createNote()">
<ng-container *ngIf="cloneNote; else importTpl">Clone</ng-container>
<ng-template #importTpl>Create</ng-template>
</button>
```
- There was an issue where importTpl was duplicated, causing the "Note
Name" to be overridden on the "Create" button. Renamed button's importTpl to
importTplBtn to fix this.
---
**App component related E2E test updates** - daf6045
- Updated tests for the App component to ensure verify correct behavior
under various scenarios.
---
**Add create new note precedure when it's empty** - 94933a4
Added a notebook test utility for creating a new note and a common utility
that creates a note if none exists, which can be used in multiple places.
- zeppelin-web-angular/e2e/models/notebook.util.ts
- zeppelin-web-angular/e2e/utils.ts
### What type of PR is it?
Bug Fix
Improvement
### Todos
### What is the Jira issue?
ZEPPELIN-6330
### How should this be tested?
### Screenshots (if appropriate)
### Questions:
* Does the license files need to update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No
Closes #5098 from dididy/fix/link-paragraph-invalid-url.
Signed-off-by: Chan Lee <[email protected]>
(cherry picked from commit f1f671394dc742ec35ede32be039850cdf8a1145)
Signed-off-by: Chan Lee <[email protected]>
---
zeppelin-web-angular/e2e/models/notebook.util.ts | 44 +++++
.../e2e/models/published-paragraph-page.ts | 73 +++++++
.../e2e/models/published-paragraph-page.util.ts | 213 +++++++++++++++++++++
zeppelin-web-angular/e2e/tests/app.spec.ts | 25 ++-
.../notebook/published/published-paragraph.spec.ts | 103 ++++++++++
zeppelin-web-angular/e2e/utils.ts | 25 ++-
.../workspace/notebook/notebook.component.html | 1 +
.../published/paragraph/paragraph.component.ts | 35 +++-
.../share/note-create/note-create.component.html | 4 +-
9 files changed, 504 insertions(+), 19 deletions(-)
diff --git a/zeppelin-web-angular/e2e/models/notebook.util.ts
b/zeppelin-web-angular/e2e/models/notebook.util.ts
new file mode 100644
index 0000000000..5495a1dfef
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/notebook.util.ts
@@ -0,0 +1,44 @@
+/*
+ * Licensed 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.
+ */
+
+import { expect, Page } from '@playwright/test';
+import { BasePage } from './base-page';
+import { HomePage } from './home-page';
+
+export class NotebookUtil extends BasePage {
+ private homePage: HomePage;
+
+ constructor(page: Page) {
+ super(page);
+ this.homePage = new HomePage(page);
+ }
+
+ async createNotebook(notebookName: string): Promise<void> {
+ await this.homePage.navigateToHome();
+ await this.homePage.createNewNoteButton.click();
+
+ // Wait for the modal to appear and fill the notebook name
+ const notebookNameInput = this.page.locator('input[name="noteName"]');
+ await expect(notebookNameInput).toBeVisible({ timeout: 10000 });
+
+ // Fill notebook name
+ await notebookNameInput.fill(notebookName);
+
+ // Click the 'Create' button in the modal
+ const createButton = this.page.locator('button', { hasText: 'Create' });
+ await createButton.click();
+
+ // Wait for the notebook to be created and navigate to it
+ await this.page.waitForURL(url => url.toString().includes('/notebook/'), {
timeout: 30000 });
+ await this.waitForPageLoad();
+ }
+}
diff --git a/zeppelin-web-angular/e2e/models/published-paragraph-page.ts
b/zeppelin-web-angular/e2e/models/published-paragraph-page.ts
new file mode 100644
index 0000000000..7b40667c26
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/published-paragraph-page.ts
@@ -0,0 +1,73 @@
+/*
+ * Licensed 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.
+ */
+
+import { Locator, Page } from '@playwright/test';
+import { BasePage } from './base-page';
+
+export class PublishedParagraphPage extends BasePage {
+ readonly publishedParagraphContainer: Locator;
+ readonly dynamicForms: Locator;
+ readonly paragraphResult: Locator;
+ readonly errorModal: Locator;
+ readonly errorModalTitle: Locator;
+ readonly errorModalContent: Locator;
+ readonly errorModalOkButton: Locator;
+
+ constructor(page: Page) {
+ super(page);
+ this.publishedParagraphContainer =
page.locator('zeppelin-publish-paragraph');
+ this.dynamicForms =
page.locator('zeppelin-notebook-paragraph-dynamic-forms');
+ this.paragraphResult = page.locator('zeppelin-notebook-paragraph-result');
+ this.errorModal = page.locator('.ant-modal');
+ this.errorModalTitle = page.locator('.ant-modal-title');
+ this.errorModalContent = this.page.locator('.ant-modal-body', { hasText:
'Paragraph Not Found' }).last();
+ this.errorModalOkButton = page.getByRole('button', { name: 'OK' }).last();
+ }
+
+ async navigateToPublishedParagraph(noteId: string, paragraphId: string):
Promise<void> {
+ await this.page.goto(`/#/notebook/${noteId}/paragraph/${paragraphId}`);
+ await this.waitForPageLoad();
+ }
+
+ async isPublishedParagraphVisible(): Promise<boolean> {
+ return await this.publishedParagraphContainer.isVisible();
+ }
+
+ async getErrorModalTitle(): Promise<string> {
+ return (await this.errorModalTitle.textContent()) || '';
+ }
+
+ async getErrorModalContent(): Promise<string> {
+ return (await this.errorModalContent.textContent()) || '';
+ }
+
+ async clickErrorModalOk(): Promise<void> {
+ await this.errorModalOkButton.click();
+ }
+
+ async isDynamicFormsVisible(): Promise<boolean> {
+ return await this.dynamicForms.isVisible();
+ }
+
+ async isResultVisible(): Promise<boolean> {
+ return await this.paragraphResult.isVisible();
+ }
+
+ async getCurrentUrl(): Promise<string> {
+ return this.page.url();
+ }
+
+ async isOnHomePage(): Promise<boolean> {
+ const url = await this.getCurrentUrl();
+ return url.includes('/#/') && !url.includes('/notebook/');
+ }
+}
diff --git a/zeppelin-web-angular/e2e/models/published-paragraph-page.util.ts
b/zeppelin-web-angular/e2e/models/published-paragraph-page.util.ts
new file mode 100644
index 0000000000..4e45469390
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/published-paragraph-page.util.ts
@@ -0,0 +1,213 @@
+/*
+ * Licensed 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.
+ */
+
+import { expect, Page } from '@playwright/test';
+import { NotebookUtil } from './notebook.util';
+import { PublishedParagraphPage } from './published-paragraph-page';
+
+export class PublishedParagraphTestUtil {
+ private page: Page;
+ private publishedParagraphPage: PublishedParagraphPage;
+ private notebookUtil: NotebookUtil;
+
+ constructor(page: Page) {
+ this.page = page;
+ this.publishedParagraphPage = new PublishedParagraphPage(page);
+ this.notebookUtil = new NotebookUtil(page);
+ }
+
+ async verifyNonExistentParagraphError(validNoteId: string,
invalidParagraphId: string): Promise<void> {
+ await
this.publishedParagraphPage.navigateToPublishedParagraph(validNoteId,
invalidParagraphId);
+
+ const modal = this.page.locator('.ant-modal', { hasText: 'Paragraph Not
Found' }).last();
+ await expect(modal).toBeVisible({ timeout: 10000 });
+
+ await expect(modal).toContainText('Paragraph Not Found');
+
+ const content = await this.publishedParagraphPage.getErrorModalContent();
+ expect(content).toContain(invalidParagraphId);
+ expect(content).toContain('does not exist in notebook');
+ expect(content).toContain('redirected to the home page');
+
+ await this.publishedParagraphPage.clickErrorModalOk();
+
+ await expect(this.publishedParagraphPage.errorModal).toBeHidden();
+
+ expect(await this.publishedParagraphPage.isOnHomePage()).toBe(true);
+ }
+
+ async verifyClickLinkThisParagraphBehavior(noteId: string, paragraphId:
string): Promise<void> {
+ // 1. Navigate to the normal notebook view
+ await this.page.goto(`/#/notebook/${noteId}`);
+ await this.page.waitForLoadState('networkidle');
+
+ // 2. Find the correct paragraph result element and go up to the parent
paragraph container
+ const paragraphElement =
this.page.locator(`zeppelin-notebook-paragraph[data-testid="${paragraphId}"]`);
+ await expect(paragraphElement).toBeVisible();
+
+ // 3. Click the settings button to open the dropdown
+ const settingsButton = paragraphElement.locator('a[nz-dropdown]');
+ await settingsButton.click();
+
+ // 4. Click "Link this paragraph" in the dropdown menu
+ const linkParagraphButton = this.page.locator('li.list-item:has-text("Link
this paragraph")');
+ await expect(linkParagraphButton).toBeVisible();
+
+ // 5. Handle the new page/tab that opens
+ const [newPage] = await Promise.all([this.page.waitForEvent('popup'),
linkParagraphButton.click()]);
+ await newPage.waitForLoadState();
+
+ // 6. Verify the new page URL shows published paragraph (not redirected)
+ await expect(newPage).toHaveURL(new
RegExp(`/notebook/${noteId}/paragraph/${paragraphId}`), { timeout: 10000 });
+
+ const codeEditor =
newPage.locator('zeppelin-notebook-paragraph-code-editor');
+ await expect(codeEditor).toBeHidden();
+
+ const controlPanel =
newPage.locator('zeppelin-notebook-paragraph-control');
+ await expect(controlPanel).toBeHidden();
+ }
+
+ async openFirstNotebook(): Promise<{ noteId: string; paragraphId: string }> {
+ await this.page.goto('/');
+ await this.page.waitForLoadState('networkidle');
+
+ const treeContainer = this.page.locator('nz-tree.ant-tree');
+ await this.page.waitForLoadState('networkidle');
+ await treeContainer.waitFor({ state: 'attached', timeout: 15000 });
+
+ const firstNode = treeContainer.locator('nz-tree-node').first();
+ await firstNode.waitFor({ state: 'attached', timeout: 15000 });
+ await expect(firstNode).toBeVisible();
+
+ // Check if the first node is a closed folder and expand it
+ const switcher = firstNode.locator('.ant-tree-switcher').first();
+ if ((await switcher.isVisible()) && (await
switcher.getAttribute('class'))?.includes('ant-tree-switcher_close')) {
+ await switcher.click();
+ await expect(switcher).toHaveClass(/ant-tree-switcher_open/);
+ }
+
+ // After potentially expanding the first folder, find the first notebook
and click its link.
+ const firstNotebookNode =
treeContainer.locator('nz-tree-node:has(.ant-tree-switcher-noop)').first();
+ await expect(firstNotebookNode).toBeVisible();
+
+ const notebookLink =
firstNotebookNode.locator('a[href*="/notebook/"]').first();
+ await notebookLink.click();
+
+ // Wait for navigation to the notebook
+ await this.page.waitForURL(/\/notebook\//, { timeout: 10000 });
+ await this.page.waitForLoadState('networkidle');
+
+ // Extract notebook ID from URL
+ const url = this.page.url();
+ const noteIdMatch = url.match(/\/notebook\/([^\/\?]+)/);
+ if (!noteIdMatch) {
+ throw new Error('Failed to extract notebook ID from URL: ' + url);
+ }
+ const noteId = noteIdMatch[1];
+
+ // Get the first paragraph ID from the page
+ await
expect(this.page.locator('zeppelin-notebook-paragraph-result').first()).toBeVisible({
timeout: 10000 });
+ const paragraphContainer =
this.page.locator('zeppelin-notebook-paragraph').first(); // 첫 번째 paragraph
+ const dropdownTrigger = paragraphContainer.locator('a[nz-dropdown]');
+ await dropdownTrigger.click();
+
+ const paragraphLink = this.page.locator('li.paragraph-id a').first();
+ await paragraphLink.waitFor({ state: 'attached', timeout: 5000 });
+
+ const paragraphId = await paragraphLink.textContent();
+
+ if (!paragraphId || !paragraphId.startsWith('paragraph_')) {
+ throw new Error(`Failed to find a valid paragraph ID. Found:
${paragraphId}`);
+ }
+
+ await this.page.goto('/');
+ await this.page.waitForLoadState('networkidle');
+ await this.page.waitForSelector('text=Welcome to Zeppelin!', { timeout:
5000 });
+
+ return { noteId, paragraphId };
+ }
+
+ async createTestNotebook(): Promise<{ noteId: string; paragraphId: string }>
{
+ const notebookName = `Test Notebook ${Date.now()}`;
+
+ // Use existing NotebookUtil to create notebook
+ await this.notebookUtil.createNotebook(notebookName);
+
+ // Extract noteId from URL
+ const url = this.page.url();
+ const noteIdMatch = url.match(/\/notebook\/([^\/\?]+)/);
+ if (!noteIdMatch) {
+ throw new Error('Failed to extract notebook ID from URL: ' + url);
+ }
+ const noteId = noteIdMatch[1];
+
+ // Get first paragraph ID
+ await this.page
+ .locator('zeppelin-notebook-paragraph')
+ .first()
+ .waitFor({ state: 'visible', timeout: 10000 });
+ const paragraphContainer =
this.page.locator('zeppelin-notebook-paragraph').first();
+ const dropdownTrigger = paragraphContainer.locator('a[nz-dropdown]');
+ await dropdownTrigger.click();
+
+ const paragraphLink = this.page.locator('li.paragraph-id a').first();
+ await paragraphLink.waitFor({ state: 'attached', timeout: 5000 });
+
+ const paragraphId = await paragraphLink.textContent();
+
+ if (!paragraphId || !paragraphId.startsWith('paragraph_')) {
+ throw new Error(`Failed to find a valid paragraph ID. Found:
${paragraphId}`);
+ }
+
+ // Navigate back to home
+ await this.page.goto('/');
+ await this.page.waitForLoadState('networkidle');
+ await this.page.waitForSelector('text=Welcome to Zeppelin!', { timeout:
5000 });
+
+ return { noteId, paragraphId };
+ }
+
+ async deleteTestNotebook(noteId: string): Promise<void> {
+ // Navigate to home page
+ await this.page.goto('/');
+ await this.page.waitForLoadState('networkidle');
+
+ // Find the notebook in the tree by noteId and get its parent tree node
+ const notebookLink = this.page.locator(`a[href*="/notebook/${noteId}"]`);
+
+ if ((await notebookLink.count()) > 0) {
+ // Hover over the tree node to make delete button visible
+ const treeNode = notebookLink.locator('xpath=ancestor::nz-tree-node[1]');
+ await treeNode.hover();
+
+ // Find and click the delete button
+ const deleteButton = treeNode.locator('a[nz-tooltip]
i[nztype="delete"]');
+ await expect(deleteButton).toBeVisible();
+ await deleteButton.click();
+
+ // Confirm deletion in popconfirm
+ const confirmButton = this.page.locator('button:has-text("OK")');
+ await confirmButton.click();
+
+ // Wait for the notebook to be removed
+ await expect(treeNode).toBeHidden();
+ }
+ }
+
+ generateNonExistentIds(): { noteId: string; paragraphId: string } {
+ const timestamp = Date.now();
+ return {
+ noteId: `NON_EXISTENT_NOTEBOOK_${timestamp}`,
+ paragraphId: `NON_EXISTENT_PARAGRAPH_${timestamp}`
+ };
+ }
+}
diff --git a/zeppelin-web-angular/e2e/tests/app.spec.ts
b/zeppelin-web-angular/e2e/tests/app.spec.ts
index 9231eca79e..5a02c87f38 100644
--- a/zeppelin-web-angular/e2e/tests/app.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/app.spec.ts
@@ -32,6 +32,8 @@ test.describe('Zeppelin App Component', () => {
const zeppelinRoot = page.locator('zeppelin-root');
await expect(zeppelinRoot).toBeAttached();
+ await waitForZeppelinReady(page);
+
// Verify router-outlet is inside zeppelin-root (use first to avoid
multiple elements)
const routerOutlet = zeppelinRoot.locator('router-outlet').first();
await expect(routerOutlet).toBeAttached();
@@ -142,30 +144,25 @@ test.describe('Zeppelin App Component', () => {
await waitForZeppelinReady(page);
const zeppelinRoot = page.locator('zeppelin-root');
+ const routerOutlet = zeppelinRoot.locator('router-outlet').first();
// Navigate to different pages and ensure component remains intact
- const testPaths = ['/notebook', '/jobmanager', '/configuration'];
+ const testPaths = ['/#/notebook', '/#/jobmanager', '/#/configuration'];
for (const path of testPaths) {
- try {
- await page.goto(path, { waitUntil: 'load', timeout: 5000 });
-
- // Component should still be attached
- await expect(zeppelinRoot).toBeAttached();
+ await page.goto(path, { waitUntil: 'load', timeout: 10000 });
+ await waitForZeppelinReady(page);
- // Router outlet should still be present
- const routerOutlet = zeppelinRoot.locator('router-outlet');
- await expect(routerOutlet).toBeAttached();
+ // Component should still be attached
+ await expect(zeppelinRoot).toBeAttached();
- await waitForZeppelinReady(page);
- } catch (error) {
- // Skip paths that don't exist or are not accessible
- console.log(`Skipping path ${path}: ${error}`);
- }
+ // Router outlet should still be present
+ await expect(routerOutlet).toBeAttached();
}
// Return to home
await page.goto('/', { waitUntil: 'load' });
+ await waitForZeppelinReady(page);
await expect(zeppelinRoot).toBeAttached();
});
diff --git
a/zeppelin-web-angular/e2e/tests/notebook/published/published-paragraph.spec.ts
b/zeppelin-web-angular/e2e/tests/notebook/published/published-paragraph.spec.ts
new file mode 100644
index 0000000000..04eb056321
--- /dev/null
+++
b/zeppelin-web-angular/e2e/tests/notebook/published/published-paragraph.spec.ts
@@ -0,0 +1,103 @@
+/*
+ * Licensed 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.
+ */
+
+import { expect, test } from '@playwright/test';
+import { PublishedParagraphTestUtil } from
'../../../models/published-paragraph-page.util';
+import {
+ addPageAnnotationBeforeEach,
+ createNotebookIfListEmpty,
+ performLoginIfRequired,
+ waitForZeppelinReady,
+ PAGES
+} from '../../../utils';
+
+test.describe('Published Paragraph', () => {
+ addPageAnnotationBeforeEach(PAGES.WORKSPACE.PUBLISHED_PARAGRAPH);
+
+ let testUtil: PublishedParagraphTestUtil;
+ let testNotebook: { noteId: string; paragraphId: string };
+
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/');
+ await waitForZeppelinReady(page);
+ await performLoginIfRequired(page);
+ await createNotebookIfListEmpty(page);
+
+ testUtil = new PublishedParagraphTestUtil(page);
+ testNotebook = await testUtil.createTestNotebook();
+ });
+
+ test.afterEach(async () => {
+ if (testNotebook?.noteId) {
+ await testUtil.deleteTestNotebook(testNotebook.noteId);
+ }
+ });
+
+ test.describe('Error Handling', () => {
+ test('should show error modal when notebook does not exist', async ({ page
}) => {
+ const nonExistentIds = testUtil.generateNonExistentIds();
+
+ await
page.goto(`/#/notebook/${nonExistentIds.noteId}/paragraph/${nonExistentIds.paragraphId}`);
+ await page.waitForLoadState('networkidle');
+
+ const modal = page.locator('.ant-modal');
+ const isModalVisible = await modal.isVisible({ timeout: 10000 });
+
+ if (isModalVisible) {
+ const modalContent = await modal.textContent();
+ expect(modalContent?.toLowerCase()).toContain('not found');
+ } else {
+ await expect(page).toHaveURL(/\/#\/$/, { timeout: 5000 });
+ }
+ });
+
+ test('should show error modal when paragraph does not exist in valid
notebook', async () => {
+ const validNoteId = testNotebook.noteId;
+ const nonExistentParagraphId =
testUtil.generateNonExistentIds().paragraphId;
+
+ await testUtil.verifyNonExistentParagraphError(validNoteId,
nonExistentParagraphId);
+ });
+
+ test('should redirect to home page after error modal dismissal', async ({
page }) => {
+ const nonExistentIds = testUtil.generateNonExistentIds();
+
+ await
page.goto(`/#/notebook/${nonExistentIds.noteId}/paragraph/${nonExistentIds.paragraphId}`);
+ await page.waitForLoadState('networkidle');
+
+ const modal = page.locator('.ant-modal', { hasText: 'Paragraph Not
Found' }).last();
+ const isModalVisible = await modal.isVisible();
+
+ if (isModalVisible) {
+ const okButton = page.locator('button:has-text("OK"),
button:has-text("확인"), [role="button"]:has-text("OK")');
+ await okButton.click();
+
+ await expect(page).toHaveURL(/\/#\/$/, { timeout: 10000 });
+ } else {
+ await expect(page).toHaveURL(/\/#\/$/, { timeout: 5000 });
+ }
+ });
+ });
+
+ test.describe('Valid Paragraph Display', () => {
+ test('should enter published paragraph by clicking', async () => {
+ await testUtil.verifyClickLinkThisParagraphBehavior(testNotebook.noteId,
testNotebook.paragraphId);
+ });
+
+ test('should enter published paragraph by URL', async ({ page }) => {
+ await
page.goto(`/#/notebook/${testNotebook.noteId}/paragraph/${testNotebook.paragraphId}`);
+ await page.waitForLoadState('networkidle');
+ await
expect(page).toHaveURL(`/#/notebook/${testNotebook.noteId}/paragraph/${testNotebook.paragraphId}`,
{
+ timeout: 10000
+ });
+ });
+ });
+});
diff --git a/zeppelin-web-angular/e2e/utils.ts
b/zeppelin-web-angular/e2e/utils.ts
index 9f6193d083..ca18204a71 100644
--- a/zeppelin-web-angular/e2e/utils.ts
+++ b/zeppelin-web-angular/e2e/utils.ts
@@ -10,8 +10,9 @@
* limitations under the License.
*/
-import { test, Page, TestInfo } from '@playwright/test';
+import { expect, test, Page, TestInfo } from '@playwright/test';
import { LoginTestUtil } from './models/login-page.util';
+import { NotebookUtil } from './models/notebook.util';
export const PAGES = {
// Main App
@@ -188,7 +189,7 @@ export async function performLoginIfRequired(page: Page):
Promise<boolean> {
await passwordInput.fill(testUser.password);
await loginButton.click();
- await page.waitForSelector('zeppelin-workspace', { timeout: 10000 });
+ await page.waitForSelector('text=Welcome to Zeppelin!', { timeout: 5000 });
return true;
}
@@ -214,3 +215,23 @@ export async function waitForZeppelinReady(page: Page):
Promise<void> {
throw error instanceof Error ? error : new Error(`Zeppelin loading failed:
${String(error)}`);
}
}
+
+export async function createNotebookIfListEmpty(page: Page): Promise<void> {
+ const notebookName = `My Test Notebook ${Date.now()}`;
+
+ // Check if any notebooks are listed
+ const notebookItems = page.locator('a[href*="#/notebook/"]'); // Assuming
notebooks are links with #/notebook/ in their href
+ const notebookCount = await notebookItems.count();
+
+ if (notebookCount === 0) {
+ console.log('No notebooks found, creating a new one...');
+ const notebookUtil = new NotebookUtil(page);
+ await notebookUtil.createNotebook(notebookName);
+ await expect(page.locator(`text=${notebookName}`)).toBeVisible();
+ console.log(`Notebook '${notebookName}' created successfully.`);
+ } else {
+ console.log(`${notebookCount} notebooks already exist. Skipping
creation.`);
+ }
+ await page.goto('/');
+ await page.waitForSelector('text=Welcome to Zeppelin!', { timeout: 5000 });
+}
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
index f75ff5104b..ed0bddd2f2 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
+++
b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
@@ -85,6 +85,7 @@
[revisionView]="revisionView"
[first]="first"
[last]="last"
+ [attr.data-testid]="p.id"
(selectAtIndex)="onSelectAtIndex($event)"
(selected)="onParagraphSelect($event)"
(triggerSaveParagraph)="saveParagraph($event)"
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.ts
b/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.ts
index ca7f2b7aeb..367d478bfb 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.ts
+++
b/zeppelin-web-angular/src/app/pages/workspace/published/paragraph/paragraph.component.ts
@@ -10,7 +10,7 @@
* limitations under the License.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit,
QueryList, ViewChildren } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
import { publishedSymbol, MessageListener, ParagraphBase, Published } from
'@zeppelin/core';
import {
MessageReceiveDataTypeMap,
@@ -22,6 +22,7 @@ import {
import { HeliumService, MessageService, NgZService, NoteStatusService } from
'@zeppelin/services';
import { SpellResult } from '@zeppelin/spell';
import { isNil } from 'lodash';
+import { NzModalService } from 'ng-zorro-antd/modal';
import { NotebookParagraphResultComponent } from
'../../share/result/result.component';
@Component({
@@ -44,6 +45,8 @@ export class PublishedParagraphComponent extends
ParagraphBase implements Publis
public messageService: MessageService,
private activatedRoute: ActivatedRoute,
private heliumService: HeliumService,
+ private router: Router,
+ private nzModalService: NzModalService,
noteStatusService: NoteStatusService,
ngZService: NgZService,
cdr: ChangeDetectorRef
@@ -70,11 +73,21 @@ export class PublishedParagraphComponent extends
ParagraphBase implements Publis
this.setResults(this.paragraph);
this.originalText = this.paragraph.text;
this.initializeDefault(this.paragraph.config, this.paragraph.settings);
+ } else {
+ this.handleParagraphNotFound(note.name || this.noteId!,
this.paragraphId!);
+ return;
}
}
this.cdr.markForCheck();
}
+ @MessageListener(OP.ERROR_INFO)
+ handleError(data: MessageReceiveDataTypeMap[OP.ERROR_INFO]) {
+ if (data.info && data.info.includes('404')) {
+ this.handleNoteNotFound(this.noteId!);
+ }
+ }
+
trackByIndexFn(index: number) {
return index;
}
@@ -114,4 +127,24 @@ export class PublishedParagraphComponent extends
ParagraphBase implements Publis
resultComponent.updateResult(config, result);
}
}
+
+ private handleParagraphNotFound(noteName: string, paragraphId: string): void
{
+ this.router.navigate(['/']).then(() => {
+ this.nzModalService.error({
+ nzTitle: 'Paragraph Not Found',
+ nzContent: `The paragraph "${paragraphId}" does not exist in notebook
"${noteName}". You have been redirected to the home page.`,
+ nzOkText: 'OK'
+ });
+ });
+ }
+
+ private handleNoteNotFound(noteId: string): void {
+ this.router.navigate(['/']).then(() => {
+ this.nzModalService.error({
+ nzTitle: 'Notebook Not Found',
+ nzContent: `The notebook "${noteId}" does not exist or you don't have
permission to access it. You have been redirected to the home page.`,
+ nzOkText: 'OK'
+ });
+ });
+ }
}
diff --git
a/zeppelin-web-angular/src/app/share/note-create/note-create.component.html
b/zeppelin-web-angular/src/app/share/note-create/note-create.component.html
index b9da5f0a04..7400a0590c 100644
--- a/zeppelin-web-angular/src/app/share/note-create/note-create.component.html
+++ b/zeppelin-web-angular/src/app/share/note-create/note-create.component.html
@@ -41,8 +41,8 @@
<div class="modal-footer ant-modal-footer">
<button nz-button nzType="primary" (click)="createNote()">
- <ng-container *ngIf="cloneNote; else importTpl">Clone</ng-container>
- <ng-template #importTpl>Create</ng-template>
+ <ng-container *ngIf="cloneNote; else importTplBtn">Clone</ng-container>
+ <ng-template #importTplBtn>Create</ng-template>
</button>
</div>
</form>