Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@

export interface ImageRendererProps {
imageData: string;
format?: 'png' | 'svg';
}

export const ImageRenderer = ({ imageData }: ImageRendererProps) => {
const imgSrc = `data:image/png;base64,${imageData}`;
export const ImageRenderer = ({ imageData, format = 'png' }: ImageRendererProps) => {
const imgSrc =
format === 'svg'
? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(imageData)}`
: `data:image/png;base64,${imageData}`;

return <img src={imgSrc} alt="Result" style={{ maxWidth: '100%', height: 'auto' }} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,40 @@ describe('SingleResultRenderer', () => {
expect(screen.getByRole('img').getAttribute('src')).toBe('data:image/png;base64,QUJD');
});

it('renders SVG as an encoded image without inserting its markup into the page', () => {
const svg = '<svg xmlns="http://www.w3.org/2000/svg"><text>서울 # &</text><script>ignored()</script></svg>';
const { container } = render(<SingleResultRenderer index={0} result={result(DatasetType.SVG, svg)} />);

expect(screen.getByRole('img').getAttribute('src')).toBe(
`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
);
expect(container.querySelector('svg')).toBeNull();
expect(container.querySelector('script')).toBeNull();
});

it('updates the image format when an IMG result becomes SVG and back', () => {
const svg = '<svg xmlns="http://www.w3.org/2000/svg" />';
const view = render(<SingleResultRenderer index={0} result={result(DatasetType.IMG, 'QUJD')} />);

view.rerender(<SingleResultRenderer index={0} result={result(DatasetType.SVG, svg)} />);
expect(screen.getByRole('img').getAttribute('src')).toBe(
`data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
);

view.rerender(<SingleResultRenderer index={0} result={result(DatasetType.IMG, 'REVG')} />);
expect(screen.getByRole('img').getAttribute('src')).toBe('data:image/png;base64,REVG');
});

it('clears the previous image when the result becomes NULL', () => {
const view = render(<SingleResultRenderer index={0} result={result(DatasetType.SVG, '<svg />')} />);
expect(screen.getByRole('img')).toBeTruthy();

view.rerender(<SingleResultRenderer index={0} result={result(DatasetType.NULL, 'ignored')} />);

expect(screen.queryByRole('img')).toBeNull();
expect(view.container.innerHTML).toBe('');
});

it('renders HTML as markup rather than as text', () => {
render(<SingleResultRenderer index={0} result={result(DatasetType.HTML, '<p>markup output</p>')} />);

Expand All @@ -76,8 +110,7 @@ describe('SingleResultRenderer', () => {
expect(screen.getByText(/not supported in React environment/)).toBeTruthy();
});

it('renders nothing for a type it has no renderer for', () => {
// NETWORK is declared by the SDK and reaches the default arm.
it('renders nothing for NETWORK, which has no renderer', () => {
const { container } = render(<SingleResultRenderer index={0} result={result(DatasetType.NETWORK, 'graph')} />);

expect(container.innerHTML).toBe('');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export const SingleResultRenderer = ({ result, index, config }: SingleResultRend
return <TextRenderer text={checkAndReplaceCarriageReturn(result.data)} />;
case DatasetType.IMG:
return <ImageRenderer imageData={result.data} />;
case DatasetType.SVG:
return <ImageRenderer imageData={result.data} format="svg" />;
case DatasetType.ANGULAR:
return (
<Alert
Expand All @@ -42,7 +44,12 @@ export const SingleResultRenderer = ({ result, index, config }: SingleResultRend
showIcon
/>
);
default:
case DatasetType.NULL:
case DatasetType.NETWORK:
return null;
default: {
const _unhandled: never = result.type;
return null;
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,33 @@ export interface ParagraphConfig {
export interface ParagraphResults {
[index: number]: Record<string, unknown>;

code?: string;
code?: ParagraphResultCode;
msg?: ParagraphIResultsMsgItem[];
}

export type ParagraphResultCode = 'SUCCESS' | 'INCOMPLETE' | 'ERROR' | 'KEEP_PREVIOUS_RESULT';

export const ParagraphStates = {
UNKNOWN: 'UNKNOWN',
READY: 'READY',
PENDING: 'PENDING',
RUNNING: 'RUNNING',
FINISHED: 'FINISHED',
ERROR: 'ERROR',
ABORT: 'ABORT'
} as const;

export type ParagraphState = (typeof ParagraphStates)[keyof typeof ParagraphStates];

export enum DatasetType {
NETWORK = 'NETWORK',
TABLE = 'TABLE',
HTML = 'HTML',
TEXT = 'TEXT',
ANGULAR = 'ANGULAR',
IMG = 'IMG'
IMG = 'IMG',
SVG = 'SVG',
NULL = 'NULL'
}

export class ParagraphIResultsMsgItem {
Expand Down Expand Up @@ -148,7 +164,7 @@ export interface ParagraphItem {
dateFinished?: string;
errorMessage?: string;
runtimeInfos?: RuntimeInfos;
status: string;
status: ParagraphState;
title?: string;
focus?: boolean;
// TODO(hsuanxyz): define proper type
Expand Down Expand Up @@ -348,7 +364,7 @@ export interface Progress {

export interface ParagraphStatus {
id: string;
status: string;
status: ParagraphState;
}

interface GraphConfigSetting {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 { assertType, expect, expectTypeOf, it } from 'vitest';

import {
DatasetType,
ParagraphItem,
ParagraphResultCode,
ParagraphState,
ParagraphStatus
} from './message-paragraph.interface';

it('matches the finite values in Job.Status and InterpreterResult', () => {
expectTypeOf<ParagraphItem['status']>().toEqualTypeOf<
'UNKNOWN' | 'READY' | 'PENDING' | 'RUNNING' | 'FINISHED' | 'ERROR' | 'ABORT'
>();
expectTypeOf<ParagraphStatus['status']>().toEqualTypeOf<ParagraphState>();
expectTypeOf<NonNullable<ParagraphItem['results']>['code']>().toEqualTypeOf<ParagraphResultCode | undefined>();
expectTypeOf<ParagraphResultCode>().toEqualTypeOf<'SUCCESS' | 'INCOMPLETE' | 'ERROR' | 'KEEP_PREVIOUS_RESULT'>();
assertType<DatasetType>(DatasetType.SVG);
assertType<DatasetType>(DatasetType.NULL);
});

it('declares all eight InterpreterResult.Type wire values', () => {
expect(Object.values(DatasetType).sort()).toEqual([
'ANGULAR',
'HTML',
'IMG',
'NETWORK',
'NULL',
'SVG',
'TABLE',
'TEXT'
]);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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, it } from 'vitest';

import { ParagraphStates } from './interfaces/message-paragraph.interface';
import { isTerminalParagraphState } from './paragraph-state';

it.each(Object.values(ParagraphStates))('classifies %s using Job.Status.isCompleted', status => {
expect(isTerminalParagraphState(status)).toBe(['FINISHED', 'ERROR', 'ABORT'].includes(status));
});

it('does not treat a missing paragraph as a completed run', () => {
expect(isTerminalParagraphState(undefined)).toBe(false);
});
27 changes: 27 additions & 0 deletions zeppelin-web-angular/projects/zeppelin-sdk/src/paragraph-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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 { ParagraphState } from './interfaces/message-paragraph.interface';

// Job.Status.isCompleted: FINISHED, ERROR and ABORT terminate a run.
const terminalStates: Record<ParagraphState, boolean> = {
UNKNOWN: false,
READY: false,
PENDING: false,
RUNNING: false,
FINISHED: true,
ERROR: true,
ABORT: true
};

export const isTerminalParagraphState = (status: ParagraphState | undefined): boolean =>
status !== undefined && terminalStates[status];
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@

export * from './interfaces/public-api';
export * from './message';
export * from './paragraph-state';
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import { ChangeDetectorRef } from '@angular/core';
import { DatasetType, Message, OP, ParagraphItem } from '@zeppelin/sdk';
import { DatasetType, Message, OP, ParagraphItem, ParagraphState } from '@zeppelin/sdk';
import { EMPTY } from 'rxjs';
import { afterEach, describe, expect, it, vi } from 'vitest';

Expand All @@ -38,7 +38,11 @@ class TestParagraph extends ParagraphBase {
}
}

const paragraph = (id: string, status = 'RUNNING', dateStarted = '2026-01-01T00:00:00Z'): ParagraphItem => ({
const paragraph = (
id: string,
status: ParagraphState = 'RUNNING',
dateStarted = '2026-01-01T00:00:00Z'
): ParagraphItem => ({
id,
status,
dateStarted,
Expand Down
22 changes: 7 additions & 15 deletions zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
ParagraphConfigResults,
ParagraphEditorSetting,
ParagraphItem,
ParagraphStates,
isTerminalParagraphState,
ParagraphIResultsMsgItem,
WebSocketMessage
} from '@zeppelin/sdk';
Expand All @@ -39,17 +41,7 @@ import { AngularContextManager } from './angular-context-manager';
import { NoteStatus } from './note-status';
import { ParagraphOutputState } from './paragraph-output-state';

export const ParagraphStatus = {
READY: 'READY',
PENDING: 'PENDING',
RUNNING: 'RUNNING',
FINISHED: 'FINISHED',
ABORT: 'ABORT',
ERROR: 'ERROR'
};

const isTerminalParagraphStatus = (status?: string): boolean =>
status === ParagraphStatus.FINISHED || status === ParagraphStatus.ABORT || status === ParagraphStatus.ERROR;
export const ParagraphStatus = ParagraphStates;

export abstract class ParagraphBase extends MessageListenersManager {
paragraph?: ParagraphItem;
Expand Down Expand Up @@ -173,7 +165,7 @@ export abstract class ParagraphBase extends MessageListenersManager {
this.outputState.reset();
}
// Close the stream before publishing the terminal snapshot.
if (isTerminalParagraphStatus(newPara.status)) {
if (isTerminalParagraphState(newPara.status)) {
this.outputState.finish(newPara.results?.msg);
}
if (this.isUpdateRequired(oldPara, newPara)) {
Expand Down Expand Up @@ -247,7 +239,7 @@ export abstract class ParagraphBase extends MessageListenersManager {

private initializeOutputState(): void {
if (!this.outputState.isInitialized) {
this.outputState.reset(this.results, isTerminalParagraphStatus(this.paragraph?.status));
this.outputState.reset(this.results, isTerminalParagraphState(this.paragraph?.status));
}
}

Expand Down Expand Up @@ -312,7 +304,7 @@ export abstract class ParagraphBase extends MessageListenersManager {
newPara.jobName !== oldPara.jobName ||
newPara.title !== oldPara.title ||
isEmpty(newPara.results) !== isEmpty(oldPara.results) ||
(isTerminalParagraphStatus(newPara.status) && !isEqual(newPara.results?.msg, oldPara.results?.msg)) ||
(isTerminalParagraphState(newPara.status) && !isEqual(newPara.results?.msg, oldPara.results?.msg)) ||
newPara.errorMessage !== oldPara.errorMessage ||
!isEqual(newPara.settings, oldPara.settings) ||
!isEqual(newPara.config, oldPara.config) ||
Expand Down Expand Up @@ -481,7 +473,7 @@ export abstract class ParagraphBase extends MessageListenersManager {
if (paragraph) {
this.setResults(paragraph);
}
const terminal = isTerminalParagraphStatus(paragraph?.status);
const terminal = isTerminalParagraphState(paragraph?.status);
this.outputState.reset(this.results, terminal);
this.cdr.markForCheck();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import { QueryList } from '@angular/core';
import { convertToParamMap } from '@angular/router';
import { DatasetType, Note, ParagraphItem } from '@zeppelin/sdk';
import { DatasetType, Note, ParagraphItem, ParagraphState } from '@zeppelin/sdk';
import { BehaviorSubject, EMPTY, of } from 'rxjs';
import { afterEach, describe, expect, it, vi } from 'vitest';

Expand All @@ -32,7 +32,7 @@ vi.mock('../../share/result/result.component', () => ({ NotebookParagraphResultC

import { PublishedParagraphComponent } from './paragraph.component';

const snapshot = (noteId: string, status: string, text: string): Note => ({
const snapshot = (noteId: string, status: ParagraphState, text: string): Note => ({
note: {
id: noteId,
name: noteId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,22 @@
}
<ng-template cdkPortalOutlet></ng-template>
<!-- Transformation setting container for classic visualizations -->
@if (getCurrentVisualization()?.isClassic) {
@if (result.type !== datasetType.SVG && result.type !== datasetType.NULL && getCurrentVisualization()?.isClassic) {
<div
[style.display]="config?.graph?.optionOpen ? 'block' : 'none'"
[id]="'trsetting' + id + '_' + config?.graph?.mode"
class="transformation-setting"
></div>
}
<!-- Visualization setting container for classic visualizations -->
@if (getCurrentVisualization()?.isClassic) {
@if (result.type !== datasetType.SVG && result.type !== datasetType.NULL && getCurrentVisualization()?.isClassic) {
<div
[style.display]="config?.graph?.optionOpen ? 'block' : 'none'"
[id]="'vizsetting' + id + '_' + config?.graph?.mode"
class="visualization-setting"
></div>
}
@if (getCurrentVisualization()?.isClassic) {
@if (result.type !== datasetType.SVG && result.type !== datasetType.NULL && getCurrentVisualization()?.isClassic) {
<div [id]="'p' + id + '_' + config?.graph?.mode" class="classic-visualization-container"></div>
}
@switch (result?.type) {
Expand All @@ -103,6 +103,9 @@
@case (datasetType.IMG) {
<div class="img"><img [src]="imgData" alt="img" /></div>
}
@case (datasetType.SVG) {
<div class="img"><img [src]="imgData" alt="svg" /></div>
}
}
@if (frontEndError) {
<div class="text-plain">
Expand Down
Loading
Loading