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

chanholee 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 f1f671394d [ZEPPELIN-6330] Redirect to home with error if shared 
paragraph(or note) doesn’t exist
f1f671394d is described below

commit f1f671394dc742ec35ede32be039850cdf8a1145
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]>
---
 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>

Reply via email to