Skip to content

Commit 9799b26

Browse files
authored
[ZEPPELIN-6762] Fix truncated output alerts in the new UI
### What is this PR for? Fix the unstyled “Output is truncated” alert and its non-functional close button in the new UI. The alert HTML generated by the server relies on Bootstrap styles and dismissal behavior, which the new UI does not provide. This PR adds light/dark theme styling and close-button handling for both Angular and React. ### What type of PR is it? Bug Fix ### What is the Jira issue? * https://issues.apache.org/jira/browse/ZEPPELIN-6762 ### How should this be tested? Automated tests - Run the following commands from the repository root: ```bash cd zeppelin-web-angular npm run test:shell -- src/app/pages/workspace/share/result/result.component.spec.ts --maxWorkers=1 npm --prefix projects/zeppelin-react test -- src/components/renderers/HTMLRenderer.spec.tsx --maxWorkers=1 ``` Manual verification 1. Create a notebook using the sh interpreter and run: ```sh %sh for i in $(seq 1 20000); do echo "line_$i"; done ``` 2. Verify that the “Output is truncated” alert below the output has a background, border, and close button. 3. Click × and verify that only the alert disappears; the output text should remain. 4. Run the paragraph again. Verify that the alert reappears and can also be dismissed by focusing its close button and pressing Enter. 5. Repeat in both light and dark themes. 6. Open the same paragraph in the React Published view and repeat: ```text /#/notebook/<noteId>/paragraph/<paragraphId>?react=true ``` To verify the row-limit alert with a PostgreSQL JDBC interpreter, set zeppelin.jdbc.maxResult to 1000 and run: ```sql %jdbc SELECT generate_series(1, 2000); ``` Verify that the row-limit alert following the table is styled correctly and can be dismissed. ### Screenshots (if appropriate) **Angular dark theme** <img width="1280" height="420" alt="zeppelin-6762-angular-dark" src="https://github.com/user-attachments/assets/22343a9e-9aa1-47d8-86a6-6320b3f1d675" /> **Angular light theme** <img width="1280" height="420" alt="zeppelin-6762-angular-light" src="https://github.com/user-attachments/assets/3498ac84-d76f-4ffe-a831-12d9c7e6e5e6" /> **React dark theme** <img width="1600" height="525" alt="zeppelin-6762-react-dark" src="https://github.com/user-attachments/assets/d7f85139-e1bf-425e-b189-ffba5de510c5" /> **React light theme** <img width="1280" height="420" alt="zeppelin-6762-react-light" src="https://github.com/user-attachments/assets/243408ef-3ae0-411f-83e0-5839d123deff" /> ### Questions: * Does the license files need to update? No. * Is there breaking changes for older versions? No. * Does this needs documentation? No. Closes #5591 from miinhho/ZEPPELIN-6762-truncated-output-alert. Signed-off-by: YONGJAE LEE <dev.yongjaelee@gmail.com>
1 parent 2d14bb9 commit 9799b26

7 files changed

Lines changed: 123 additions & 1 deletion

File tree

‎zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.spec.tsx‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* limitations under the License.
1111
*/
1212

13-
import { render, screen, waitFor } from '@testing-library/react';
13+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
1414
import { describe, expect, it } from 'vitest';
1515
import { HTMLRenderer } from './HTMLRenderer';
1616

@@ -76,6 +76,24 @@ describe('HTMLRenderer', () => {
7676
expect(screen.getByText('second')).toBeTruthy();
7777
});
7878

79+
it('dismisses only result warnings, including after the HTML is replaced', () => {
80+
const html =
81+
'<div class="result-alert alert-warning" role="alert"><button class="close" data-dismiss="alert" aria-label="Close"><span>&times;</span></button>102400 bytes</div><button class="close" data-dismiss="alert">Ordinary output</button>';
82+
const { rerender } = render(<HTMLRenderer html={html} />);
83+
84+
fireEvent.click(screen.getByRole('button', { name: 'Ordinary output' }));
85+
expect(screen.getByRole('alert').textContent).toContain('102400 bytes');
86+
fireEvent.click(screen.getByRole('button', { name: 'Close' }).firstChild!);
87+
rerender(<HTMLRenderer html={html} />);
88+
expect(screen.queryByRole('alert')).toBeNull();
89+
expect(screen.getByRole('button', { name: 'Ordinary output' })).toBeTruthy();
90+
91+
rerender(<HTMLRenderer html={html.replace('102400 bytes', '1000 rows')} />);
92+
expect(screen.getByRole('alert').textContent).toContain('1000 rows');
93+
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
94+
expect(screen.queryByRole('alert')).toBeNull();
95+
});
96+
7997
it('renders nothing visible for empty html', () => {
8098
const { container } = render(<HTMLRenderer html="" />);
8199

‎zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/HTMLRenderer.tsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ export const HTMLRenderer = ({ html }: HTMLRendererProps) => {
2828
// to the DOM to execute them.
2929
container.innerHTML = html;
3030

31+
// ResultMessages HTML needs dismissal without loading Bootstrap's alert plugin.
32+
container
33+
.querySelectorAll<HTMLButtonElement>('.result-alert > button.close[data-dismiss="alert"]')
34+
.forEach(button => {
35+
const alert = button.parentElement!;
36+
button.addEventListener('click', () => alert.remove(), { once: true });
37+
});
38+
3139
// Highlight code blocks (matches Angular: result.component.ts renderHTML)
3240
const codeEle = container.querySelector<HTMLElement>('pre code');
3341
if (codeEle) {

‎zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
zeppelin-run-scripts
9696
[scriptsContent]="innerHTML"
9797
[innerHTML]="innerHTML"
98+
(click)="dismissResultAlert($event)"
9899
></div>
99100
}
100101
@case (datasetType.TEXT) {

‎zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.spec.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,26 @@ describe('NotebookParagraphResultComponent HTML highlighting', () => {
363363
expect(output.querySelector('.hljs-keyword')?.textContent).toBe('const');
364364
});
365365
});
366+
367+
describe('HTML result alert dismissal', () => {
368+
it('closes only the selected warning without changing the result data', () => {
369+
const result = component();
370+
const html = ['102400 bytes', '1000 rows']
371+
.map(
372+
limit =>
373+
`<div class="result-alert alert-warning" role="alert"><button class="close" data-dismiss="alert"><span>&times;</span></button>${limit}</div>`
374+
)
375+
.join('');
376+
result.result = { type: DatasetType.HTML, data: html };
377+
const container = document.createElement('div');
378+
container.innerHTML = `${html}<button class="close" data-dismiss="alert">Ordinary output</button>`;
379+
container.addEventListener('click', event => result.dismissResultAlert(event));
380+
381+
container.querySelector<HTMLElement>('button span')!.click();
382+
container.querySelector<HTMLButtonElement>('div + button')!.click();
383+
384+
expect(container.querySelector('[role="alert"]')?.textContent).toBe('×1000 rows');
385+
expect(container.textContent).toContain('Ordinary output');
386+
expect(result.result.data).toBe(html);
387+
});
388+
});

‎zeppelin-web-angular/src/app/pages/workspace/share/result/result.component.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,20 @@ export class NotebookParagraphResultComponent implements OnInit, AfterViewInit,
326326
this.configChange.emit(this.config);
327327
}
328328

329+
dismissResultAlert(event: MouseEvent): void {
330+
const container = event.currentTarget;
331+
const target = event.target;
332+
if (!(container instanceof Element) || !(target instanceof Element)) {
333+
return;
334+
}
335+
const button = target.closest('button.close[data-dismiss="alert"]');
336+
const alert = button?.parentElement;
337+
// ResultMessages warnings are dismissed locally, without changing the saved output.
338+
if (alert?.classList.contains('result-alert') && alert !== container && container.contains(alert)) {
339+
alert.remove();
340+
}
341+
}
342+
329343
updateResult(config: ParagraphConfigResult, result: ParagraphIResultsMsgItem) {
330344
this.config = config;
331345
this.result = result;

‎zeppelin-web-angular/src/styles.less‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717
@import './styles/font';
1818
@import './styles/global';
1919
@import './styles/rewrite';
20+
@import './styles/result-alert';
2021
@import "../node_modules/ng-zorro-antd/resizable/style/entry.less";
2122
@import "../node_modules/ng-zorro-antd/code-editor/style/entry.less";
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
* Unless required by applicable law or agreed to in writing, software
7+
* distributed under the License is distributed on an "AS IS" BASIS,
8+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
* See the License for the specific language governing permissions and
10+
* limitations under the License.
11+
*/
12+
13+
// Shell-owned styles also cover the React remote and dynamically inserted HTML.
14+
.inner-html .result-alert.alert-warning {
15+
@import (reference, multiple) './theme/light/theme-light';
16+
position: relative;
17+
padding: 12px 44px 12px 16px;
18+
color: @text-color;
19+
background: @alert-warning-bg-color;
20+
border: 1px solid @alert-warning-border-color;
21+
border-radius: @border-radius-base;
22+
overflow-wrap: anywhere;
23+
24+
> button.close[data-dismiss='alert'] {
25+
position: absolute;
26+
top: 8px;
27+
right: 8px;
28+
width: 24px;
29+
height: 24px;
30+
padding: 0;
31+
border: 0;
32+
background: transparent;
33+
color: inherit;
34+
font: inherit;
35+
font-size: 20px;
36+
line-height: 24px;
37+
cursor: pointer;
38+
opacity: 0.65;
39+
40+
&:hover,
41+
&:focus-visible {
42+
opacity: 1;
43+
}
44+
45+
&:focus-visible {
46+
outline: 2px solid currentColor;
47+
outline-offset: 2px;
48+
}
49+
}
50+
}
51+
52+
html.dark .inner-html .result-alert.alert-warning {
53+
@import (reference, multiple) './theme/dark/theme-dark';
54+
color: @text-color;
55+
background: mix(@warning-color, @component-background, 10%);
56+
border-color: mix(@warning-color, @component-background, 30%);
57+
}

0 commit comments

Comments
 (0)