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 237b06765a [ZEPPELIN-6645] Handle empty interpreter settings in note
creation
237b06765a is described below
commit 237b06765af603791d8c869cbcd6bc3794fb845a
Author: Minho Jang <[email protected]>
AuthorDate: Sun Nov 1 10:44:13 2026 +0900
[ZEPPELIN-6645] Handle empty interpreter settings in note creation
### What is this PR for?
The server filters interpreter settings by the current user's permissions,
so an empty list is a valid response. The New Note and Clone Note dialogs share
a handler that unconditionally reads the first setting's name, causing a
`TypeError` when no settings are available.
Use optional chaining to leave the default interpreter unset for an empty
response while preserving first-setting selection for nonempty responses. An
empty response also clears any previous selection and allows change detection
to complete.
Add component tests for interpreter selection transitions and existing
new/clone note naming behavior.
### What type of PR is it?
Bug Fix
### Todos
* [x] Handle an empty interpreter settings response.
* [x] Add regression tests and existing note naming coverage.
* [x] Verify component tests and browser modal behavior.
### What is the Jira issue?
* https://issues.apache.org/jira/browse/ZEPPELIN-6645
### How should this be tested?
From `zeppelin-web-angular`:
```sh
npm run test:shell --
src/app/share/note-create/note-create.component.spec.ts
```
All 9 component tests pass. Coverage includes:
* Empty and nonempty interpreter lists.
* Selection clearing and recovery across responses.
* New note naming at root and nested paths.
* Maximum note numbering and clone numbering.
* Literal handling of regex metacharacters in source names.
For server-backed reproduction:
1. Configure permissions on all interpreter settings so a test user is
authorized for none.
2. As that user, open New Note and Clone Note for a readable note.
3. Confirm neither dialog reports the `TypeError` and New Note leaves the
default interpreter unset.
4. Check normal first-interpreter selection with an authorized user.
### Questions:
* Does the license files need to update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No
Closes #5599 from miinhho/ZEPPELIN-6645-empty-interpreter-settings.
Signed-off-by: YONGJAE LEE <[email protected]>
---
.../note-create/note-create.component.spec.ts | 160 +++++++++++++++++++++
.../app/share/note-create/note-create.component.ts | 2 +-
2 files changed, 161 insertions(+), 1 deletion(-)
diff --git
a/zeppelin-web-angular/src/app/share/note-create/note-create.component.spec.ts
b/zeppelin-web-angular/src/app/share/note-create/note-create.component.spec.ts
new file mode 100644
index 0000000000..80855fe7c0
--- /dev/null
+++
b/zeppelin-web-angular/src/app/share/note-create/note-create.component.spec.ts
@@ -0,0 +1,160 @@
+/*
+ * 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 { ChangeDetectorRef } from '@angular/core';
+import { NzModalRef } from 'ng-zorro-antd/modal';
+import { EMPTY } from 'rxjs';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { InterpreterItem, Note } from '@zeppelin/sdk';
+import { MessageService, NoteListService } from '@zeppelin/services';
+
+import { NoteCreateComponent } from './note-create.component';
+
+const interpreter = (name: string): InterpreterItem => ({
+ id: name,
+ name,
+ group: name,
+ properties: {},
+ status: 'READY',
+ interpreterGroup: [],
+ dependencies: [],
+ option: {
+ remote: true,
+ port: -1,
+ isExistingProcess: false,
+ setPermission: false,
+ owners: [],
+ isUserImpersonate: false
+ }
+});
+
+const sourceNote = (name: string): NonNullable<Note['note']> => ({
+ id: 'source-note',
+ name,
+ path: `/${name}`,
+ paragraphs: [],
+ defaultInterpreterGroup: 'spark',
+ noteParams: {},
+ noteForms: {},
+ angularObjects: {},
+ config: {
+ releaseresource: false,
+ isZeppelinNotebookCronEnable: false,
+ looknfeel: 'default',
+ personalizedMode: 'false'
+ },
+ info: {}
+});
+
+const createComponent = (notePaths: string[] = [], data:
ConstructorParameters<typeof NoteCreateComponent>[4] = {}) => {
+ const markForCheck = vi.fn();
+ const component = new NoteCreateComponent(
+ { receive: () => EMPTY, getInterpreterSettings: vi.fn() } as unknown as
MessageService,
+ { markForCheck } as unknown as ChangeDetectorRef,
+ { notes: { flatList: notePaths.map(path => ({ path })) } } as unknown as
NoteListService,
+ {} as NzModalRef,
+ data
+ );
+ return { component, markForCheck };
+};
+
+describe('NoteCreateComponent', () => {
+ let component: NoteCreateComponent;
+
+ afterEach(() => component?.ngOnDestroy());
+
+ it('leaves the default unset when no interpreters are available', () => {
+ const fixture = createComponent();
+ component = fixture.component;
+
+ component.getInterpreterSettings({ interpreterSettings: [] });
+
+ expect(component.defaultInterpreter).toBeUndefined();
+ expect(fixture.markForCheck).toHaveBeenCalled();
+ });
+
+ it('selects the first available interpreter', () => {
+ ({ component } = createComponent());
+
+ component.getInterpreterSettings({ interpreterSettings:
[interpreter('spark'), interpreter('python')] });
+
+ expect(component.defaultInterpreter).toBe('spark');
+ });
+
+ it('clears an unavailable selection and selects from a later nonempty
response', () => {
+ ({ component } = createComponent());
+ component.getInterpreterSettings({ interpreterSettings:
[interpreter('spark')] });
+
+ component.getInterpreterSettings({ interpreterSettings: [] });
+
+ expect(component.defaultInterpreter).toBeUndefined();
+
+ component.getInterpreterSettings({ interpreterSettings:
[interpreter('python')] });
+
+ expect(component.defaultInterpreter).toBe('python');
+ });
+
+ describe('new note naming', () => {
+ it.each([
+ { path: '', expected: 'Untitled Note 1' },
+ { path: '/Projects/Reports', expected: '/Projects/Reports/Untitled Note
1' }
+ ])('initializes the note name for path "$path"', ({ path, expected }) => {
+ ({ component } = createComponent([], { path }));
+
+ component.ngOnInit();
+
+ expect(component.noteName).toBe(expected);
+ });
+
+ it('uses the highest existing note number regardless of list order', () =>
{
+ ({ component } = createComponent(
+ ['/Untitled Note 8', '/Untitled Note 2', '/Untitled Note 5', '/Other
99', '/Untitled Note 20 draft'],
+ { path: '' }
+ ));
+
+ component.ngOnInit();
+
+ expect(component.noteName).toBe('Untitled Note 9');
+ });
+ });
+
+ describe('clone note naming', () => {
+ it('adds a copy number to an unnumbered source name', () => {
+ ({ component } = createComponent(['Sales Report', 'Other 1'], {
cloneNote: sourceNote('Sales Report') }));
+
+ component.ngOnInit();
+
+ expect(component.noteName).toBe('Sales Report 1');
+ });
+
+ it('increments past existing copies without appending a second number', ()
=> {
+ ({ component } = createComponent(['Sales Report 7', 'Sales Report 2',
'Sales Report 4', 'Other 99'], {
+ cloneNote: sourceNote('Sales Report 2')
+ }));
+
+ component.ngOnInit();
+
+ expect(component.noteName).toBe('Sales Report 8');
+ });
+
+ it('matches regex metacharacters in source names literally', () => {
+ ({ component } = createComponent(['Revenue (USD) 2', 'Revenue (USD) 4',
'Revenue USD 9'], {
+ cloneNote: sourceNote('Revenue (USD) 2')
+ }));
+
+ component.ngOnInit();
+
+ expect(component.noteName).toBe('Revenue (USD) 5');
+ });
+ });
+});
diff --git
a/zeppelin-web-angular/src/app/share/note-create/note-create.component.ts
b/zeppelin-web-angular/src/app/share/note-create/note-create.component.ts
index 96f510ffd4..8590a90a5e 100644
--- a/zeppelin-web-angular/src/app/share/note-create/note-create.component.ts
+++ b/zeppelin-web-angular/src/app/share/note-create/note-create.component.ts
@@ -35,7 +35,7 @@ export class NoteCreateComponent extends
MessageListenersManager implements OnIn
@MessageListener(OP.INTERPRETER_SETTINGS)
getInterpreterSettings(data:
MessageReceiveDataTypeMap[OP.INTERPRETER_SETTINGS]) {
this.listOfInterpreter = data.interpreterSettings;
- this.defaultInterpreter = data.interpreterSettings[0].name;
+ this.defaultInterpreter = data.interpreterSettings[0]?.name;
this.cdr.markForCheck();
}