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();
   }
 

Reply via email to