From bd971ab571c29843bc4c18fc673f533ae38d7f3c Mon Sep 17 00:00:00 2001 From: miinhho Date: Tue, 27 Oct 2026 02:16:02 +0900 Subject: [PATCH] [ZEPPELIN-6762] Fix truncated output alerts in the new UI --- .../renderers/HTMLRenderer.spec.tsx | 20 ++++++- .../src/components/renderers/HTMLRenderer.tsx | 8 +++ .../share/result/result.component.html | 1 + .../share/result/result.component.spec.ts | 23 ++++++++ .../share/result/result.component.ts | 14 +++++ zeppelin-web-angular/src/styles.less | 1 + .../src/styles/result-alert.less | 57 +++++++++++++++++++ 7 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 zeppelin-web-angular/src/styles/result-alert.less diff --git a/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.spec.tsx b/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.spec.tsx index 0911a9f15b9..71ce1fb4f89 100644 --- a/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.spec.tsx +++ b/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.spec.tsx @@ -10,7 +10,7 @@ * limitations under the License. */ -import { render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; import { HTMLRenderer } from './HTMLRenderer'; @@ -76,6 +76,24 @@ describe('HTMLRenderer', () => { expect(screen.getByText('second')).toBeTruthy(); }); + it('dismisses only result warnings, including after the HTML is replaced', () => { + const html = + ''; + const { rerender } = render(); + + fireEvent.click(screen.getByRole('button', { name: 'Ordinary output' })); + expect(screen.getByRole('alert').textContent).toContain('102400 bytes'); + fireEvent.click(screen.getByRole('button', { name: 'Close' }).firstChild!); + rerender(); + expect(screen.queryByRole('alert')).toBeNull(); + expect(screen.getByRole('button', { name: 'Ordinary output' })).toBeTruthy(); + + rerender(); + expect(screen.getByRole('alert').textContent).toContain('1000 rows'); + fireEvent.click(screen.getByRole('button', { name: 'Close' })); + expect(screen.queryByRole('alert')).toBeNull(); + }); + it('renders nothing visible for empty html', () => { const { container } = render(); diff --git a/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.tsx b/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.tsx index b01aeb12f9b..50a7cac7b83 100644 --- a/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.tsx +++ b/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.tsx @@ -28,6 +28,14 @@ export const HTMLRenderer = ({ html }: HTMLRendererProps) => { // to the DOM to execute them. container.innerHTML = html; + // ResultMessages HTML needs dismissal without loading Bootstrap's alert plugin. + container + .querySelectorAll('.result-alert > button.close[data-dismiss="alert"]') + .forEach(button => { + const alert = button.parentElement!; + button.addEventListener('click', () => alert.remove(), { once: true }); + }); + // Highlight code blocks (matches Angular: result.component.ts renderHTML) const codeEle = container.querySelector('pre code'); if (codeEle) { diff --git a/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.html b/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.html index 0d0804e814d..8b166d3b75c 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.html +++ b/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.html @@ -95,6 +95,7 @@ zeppelin-run-scripts [scriptsContent]="innerHTML" [innerHTML]="innerHTML" + (click)="dismissResultAlert($event)" > } @case (datasetType.TEXT) { diff --git a/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.spec.ts b/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.spec.ts index 67da4755a49..a15a7724768 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.spec.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.spec.ts @@ -363,3 +363,26 @@ describe('NotebookParagraphResultComponent HTML highlighting', () => { expect(output.querySelector('.hljs-keyword')?.textContent).toBe('const'); }); }); + +describe('HTML result alert dismissal', () => { + it('closes only the selected warning without changing the result data', () => { + const result = component(); + const html = ['102400 bytes', '1000 rows'] + .map( + limit => + `` + ) + .join(''); + result.result = { type: DatasetType.HTML, data: html }; + const container = document.createElement('div'); + container.innerHTML = `${html}`; + container.addEventListener('click', event => result.dismissResultAlert(event)); + + container.querySelector('button span')!.click(); + container.querySelector('div + button')!.click(); + + expect(container.querySelector('[role="alert"]')?.textContent).toBe('×1000 rows'); + expect(container.textContent).toContain('Ordinary output'); + expect(result.result.data).toBe(html); + }); +}); diff --git a/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.ts b/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.ts index e2a09068c2b..e1721472f7b 100644 --- a/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.ts +++ b/zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.ts @@ -326,6 +326,20 @@ export class NotebookParagraphResultComponent implements OnInit, AfterViewInit, this.configChange.emit(this.config); } + dismissResultAlert(event: MouseEvent): void { + const container = event.currentTarget; + const target = event.target; + if (!(container instanceof Element) || !(target instanceof Element)) { + return; + } + const button = target.closest('button.close[data-dismiss="alert"]'); + const alert = button?.parentElement; + // ResultMessages warnings are dismissed locally, without changing the saved output. + if (alert?.classList.contains('result-alert') && alert !== container && container.contains(alert)) { + alert.remove(); + } + } + updateResult(config: ParagraphConfigResult, result: ParagraphIResultsMsgItem) { this.config = config; this.result = result; diff --git a/zeppelin-web-angular/src/styles.less b/zeppelin-web-angular/src/styles.less index 72bf531420c..0452d43daad 100644 --- a/zeppelin-web-angular/src/styles.less +++ b/zeppelin-web-angular/src/styles.less @@ -17,5 +17,6 @@ @import './styles/font'; @import './styles/global'; @import './styles/rewrite'; +@import './styles/result-alert'; @import "../node_modules/ng-zorro-antd/resizable/style/entry.less"; @import "../node_modules/ng-zorro-antd/code-editor/style/entry.less"; diff --git a/zeppelin-web-angular/src/styles/result-alert.less b/zeppelin-web-angular/src/styles/result-alert.less new file mode 100644 index 00000000000..f5bec5a5a86 --- /dev/null +++ b/zeppelin-web-angular/src/styles/result-alert.less @@ -0,0 +1,57 @@ +/* + * 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. + */ + +// Shell-owned styles also cover the React remote and dynamically inserted HTML. +.inner-html .result-alert.alert-warning { + @import (reference, multiple) './theme/light/theme-light'; + position: relative; + padding: 12px 44px 12px 16px; + color: @text-color; + background: @alert-warning-bg-color; + border: 1px solid @alert-warning-border-color; + border-radius: @border-radius-base; + overflow-wrap: anywhere; + + > button.close[data-dismiss='alert'] { + position: absolute; + top: 8px; + right: 8px; + width: 24px; + height: 24px; + padding: 0; + border: 0; + background: transparent; + color: inherit; + font: inherit; + font-size: 20px; + line-height: 24px; + cursor: pointer; + opacity: 0.65; + + &:hover, + &:focus-visible { + opacity: 1; + } + + &:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; + } + } +} + +html.dark .inner-html .result-alert.alert-warning { + @import (reference, multiple) './theme/dark/theme-dark'; + color: @text-color; + background: mix(@warning-color, @component-background, 10%); + border-color: mix(@warning-color, @component-background, 30%); +}