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 =
+ '
102400 bytes
';
+ 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 =>
+ `${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%);
+}