This is an automated email from the ASF dual-hosted git repository.
voidmatcha 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 883c9d6593 [ZEPPELIN-6676] Accept both Angular object removal payload
shapes
883c9d6593 is described below
commit 883c9d65932609d071b4593941ec8cd62aa5fac0
Author: Minho Jang <[email protected]>
AuthorDate: Thu Oct 8 19:00:54 2026 +0900
[ZEPPELIN-6676] Accept both Angular object removal payload shapes
### What is this PR for?
The server emits two `ANGULAR_OBJECT_REMOVE` payload shapes. Interpreter
removal supplies top-level `name`; client unbind supplies `angularObject.name`.
Represent both shapes and export a shared SDK helper using `data.name ??
data.angularObject?.name`. Update the existing Angular consumer to use this
normalized identifier while preserving paragraph isolation and avoiding
feedback messages.
Server evidence:
-
`zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java:1518-1528,2276-2291`
### What type of PR is it?
Bug Fix
### Todos
- [x] Represent and normalize both removal payloads.
- [x] Test both shapes, name precedence and paragraph isolation.
### What is the Jira issue?
https://issues.apache.org/jira/browse/ZEPPELIN-6676
Slice 3 of 3: Angular object removal compatibility. Slice 1 merged in #5539
and slice 2 merged in #5544.
### How should this be tested?
Run from the repository root after installing frontend dependencies:
```sh
cd zeppelin-web-angular
npm run typecheck:sdk-contracts
npm run test:shell
npm run check:websocket-contract
```
### Questions:
- Does the license files need to update? No.
- Is there breaking changes for older versions? Consumers must normalize
the identifier instead of assuming top-level `name` is always present. Both
existing server shapes are accepted. No server wire-format change.
- Does this needs documentation? The SDK helper and regression tests show
both payload shapes and normalization.
Closes #5557 from miinhho/ZEPPELIN-6676-upstream-angular-object-compat.
Signed-off-by: YONGJAE LEE <[email protected]>
---
.../zeppelin-sdk/src/angular-object.spec.ts | 41 ++++++++++++++++++++++
.../src/{public-api.ts => angular-object.ts} | 8 +++--
.../src/interfaces/message-paragraph.interface.ts | 6 ++--
.../projects/zeppelin-sdk/src/public-api.ts | 1 +
.../app/core/paragraph-base/paragraph-base.spec.ts | 24 +++++++++++--
.../src/app/core/paragraph-base/paragraph-base.ts | 6 +++-
6 files changed, 78 insertions(+), 8 deletions(-)
diff --git
a/zeppelin-web-angular/projects/zeppelin-sdk/src/angular-object.spec.ts
b/zeppelin-web-angular/projects/zeppelin-sdk/src/angular-object.spec.ts
new file mode 100644
index 0000000000..299449c456
--- /dev/null
+++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/angular-object.spec.ts
@@ -0,0 +1,41 @@
+/*
+ * 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, describe, expect, it } from 'vitest';
+
+import { getAngularObjectRemovalName } from './angular-object';
+import { AngularObjectRemove } from './interfaces/message-paragraph.interface';
+
+describe('Angular object removal compatibility', () => {
+ const context = { noteId: 'note', paragraphId: 'paragraph' };
+
+ it('reads the interpreter removal name', () => {
+ const data = { ...context, name: 'value' } satisfies AngularObjectRemove;
+ expect(getAngularObjectRemovalName(data)).toBe('value');
+ });
+
+ it('reads the client-unbind object name without a top-level name', () => {
+ const data = { ...context, angularObject: { name: 'value', object: 42 },
interpreterGroupId: 'group' };
+ assertType<AngularObjectRemove>(data);
+ expect(getAngularObjectRemovalName(data)).toBe('value');
+ });
+
+ it('prefers the top-level name when both are supplied', () => {
+ expect(
+ getAngularObjectRemovalName({
+ ...context,
+ name: 'top',
+ angularObject: { name: 'nested', object: null }
+ })
+ ).toBe('top');
+ });
+});
diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts
b/zeppelin-web-angular/projects/zeppelin-sdk/src/angular-object.ts
similarity index 64%
copy from zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts
copy to zeppelin-web-angular/projects/zeppelin-sdk/src/angular-object.ts
index 2db821a15a..de91b0e39b 100644
--- a/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts
+++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/angular-object.ts
@@ -10,6 +10,8 @@
* limitations under the License.
*/
-export * from './interfaces/public-api';
-export * from './message';
-export * from './paragraph-state';
+import { AngularObjectRemove } from './interfaces/message-paragraph.interface';
+
+// NotebookServer emits a name for interpreter removal and an object for
client unbind.
+export const getAngularObjectRemovalName = (data: AngularObjectRemove): string
| undefined =>
+ data.name ?? data.angularObject?.name;
diff --git
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts
index 90e1312581..9d0d703008 100644
---
a/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts
+++
b/zeppelin-web-angular/projects/zeppelin-sdk/src/interfaces/message-paragraph.interface.ts
@@ -240,10 +240,9 @@ export interface AngularObjectUpdated {
interpreterGroupId: string;
}
-export interface AngularObjectRemove {
+interface AngularObjectRemoveContext {
noteId: string;
paragraphId: string;
- name: string;
angularObject?: {
name: string;
object: unknown;
@@ -254,6 +253,9 @@ export interface AngularObjectRemove {
interpreterGroupId?: string;
}
+export type AngularObjectRemove = AngularObjectRemoveContext &
+ ({ name: string } | { name?: string; angularObject:
NonNullable<AngularObjectRemoveContext['angularObject']> });
+
export interface AngularObjectUpdate {
noteId: string;
paragraphId: string;
diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts
b/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts
index 2db821a15a..ba14a13c7e 100644
--- a/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts
+++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts
@@ -10,6 +10,7 @@
* limitations under the License.
*/
+export * from './angular-object';
export * from './interfaces/public-api';
export * from './message';
export * from './paragraph-state';
diff --git
a/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.spec.ts
b/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.spec.ts
index fbfc695f8a..ca3ec2c922 100644
--- a/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.spec.ts
+++ b/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.spec.ts
@@ -11,7 +11,7 @@
*/
import { ChangeDetectorRef } from '@angular/core';
-import { DatasetType, Message, OP, ParagraphItem, ParagraphState } from
'@zeppelin/sdk';
+import { AngularObjectRemove, DatasetType, Message, OP, ParagraphItem,
ParagraphState } from '@zeppelin/sdk';
import { EMPTY } from 'rxjs';
import { afterEach, describe, expect, it, vi } from 'vitest';
@@ -22,6 +22,7 @@ class TestParagraph extends ParagraphBase {
changeColWidth = vi.fn();
updateParagraphResult = vi.fn();
protected currentNoteId = 'note';
+ unsetContextValue = vi.fn();
hydrate(snapshot: ParagraphItem) {
this.setParagraphSnapshot(snapshot);
@@ -31,7 +32,7 @@ class TestParagraph extends ParagraphBase {
super(
{ receive: () => EMPTY, receiveEnvelope: () => EMPTY } as unknown as
Message,
{ isParagraphRunning: item => item.status === 'RUNNING',
isEntireNoteRunning: () => false },
- {} as AngularContextManager,
+ { unsetContextValue: (...args) => this.unsetContextValue(...args) } as
AngularContextManager,
{ markForCheck: vi.fn() } as unknown as ChangeDetectorRef
);
this.paragraph = paragraph;
@@ -73,6 +74,25 @@ const beginOutput = (component: TestParagraph) =>
const appendOutput = (component: TestParagraph) =>
component.onParagraphAppendOutput({ noteId: 'note', paragraphId: 'A', index:
0, data: 'second\n' });
+describe('ParagraphBase Angular object removal', () => {
+ const payloads: AngularObjectRemove[] = [
+ { noteId: 'note', paragraphId: 'A', name: 'bound' },
+ { noteId: 'note', paragraphId: 'A', angularObject: { name: 'bound',
object: 42 }, interpreterGroupId: 'group' }
+ ];
+
+ it.each(payloads)('removes the binding for %j', data => {
+ const component = new TestParagraph(paragraph('A'));
+ component.angularObjectRemove(data);
+ expect(component.unsetContextValue).toHaveBeenCalledWith('bound', 'A',
false);
+ });
+
+ it('does not remove a binding belonging to another paragraph', () => {
+ const component = new TestParagraph(paragraph('A'));
+ component.angularObjectRemove({ noteId: 'note', paragraphId: 'other',
name: 'bound' });
+ expect(component.unsetContextValue).not.toHaveBeenCalled();
+ });
+});
+
describe('ParagraphBase streaming state isolation', () => {
it.each(['append', 'update'])('ignores %s from another note with the same
paragraph ID', kind => {
const component = new TestParagraph(paragraph('A'));
diff --git a/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.ts
b/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.ts
index 5645b6b743..7b87aea983 100644
--- a/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.ts
+++ b/zeppelin-web-angular/src/app/core/paragraph-base/paragraph-base.ts
@@ -24,6 +24,7 @@ import {
ParagraphEditorSetting,
ParagraphSettings,
ParagraphStates,
+ getAngularObjectRemovalName,
isTerminalParagraphState,
ParagraphItem,
ParagraphIResultsMsgItem,
@@ -228,7 +229,10 @@ export abstract class ParagraphBase extends
MessageListenersManager {
throw new Error('paragraph is not defined');
}
if (data.paragraphId === this.paragraph.id) {
- this.angularContextManager.unsetContextValue(data.name,
data.paragraphId, false);
+ const name = getAngularObjectRemovalName(data);
+ if (name !== undefined) {
+ this.angularContextManager.unsetContextValue(name, data.paragraphId,
false);
+ }
}
}