This is an automated email from the ASF dual-hosted git repository.
tbonelee 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 3d92e924f2 [ZEPPELIN-6427] Convert interpreter setting form to typed
reactive forms
3d92e924f2 is described below
commit 3d92e924f2a4cc1926d544e885da6167c1dcbf9a
Author: ChanHo Lee <[email protected]>
AuthorDate: Wed Jun 17 00:52:20 2026 +0900
[ZEPPELIN-6427] Convert interpreter setting form to typed reactive forms
### What is this PR for?
Numeric interpreter properties edited in the Angular UI are sent as JSON
numbers, which Gson deserializes as `Double` (`60000` → `"60000.0"`), breaking
`Long`/`Integer` parsing in interpreters such as JDBC (ZEPPELIN-6395). The
server-side workaround from ZEPPELIN-6131 only covers the update path, not
create.
This PR fixes it on the client by converting `InterpreterItemComponent`
from `UntypedFormBuilder` to typed reactive forms:
- Non-checkbox values are sent as strings (like the classic UI); checkbox
values stay real booleans, and `"true"`/`"false"` strings from corrupted data
are normalized back on save.
- New request DTOs mirror the fields `InterpreterOption.java` actually
reads; the UI-only `session`/`process` fields (dead since ZEPPELIN-1210) are no
longer sent.
- Fixes wrong `Properties.value`/`type` interface types; response option
fields that Gson omits when null are now optional.
Alternative to #5147, which stringifies checkbox booleans too — persisting
`"false"` makes an unchecked checkbox render as checked on reload. Credit to
<at>kevinjmh for the original diagnosis.
### What type of PR is it?
Improvement
### What is the Jira issue?
https://issues.apache.org/jira/browse/ZEPPELIN-6427 (fixes ZEPPELIN-6395)
### How should this be tested?
No unit test infra exists in `zeppelin-web-angular` (Playwright e2e only),
so verified by `ng build` (strict, 0 errors), lint/prettier, and manually:
numeric property saves as `"60000"` in `interpreter.json` (create and update),
JDBC paragraph runs without `NumberFormatException`, unchecked checkbox stays
unchecked after reload, and a regression pass over create/edit/cancel, property
and dependency CRUD, and interpreter binding mode options.
### Screenshots (if appropriate)
N/A
### Questions:
* Does the license files need to update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No
Closes #5265 from tbonelee/ZEPPELIN-6427-typed-interpreter-forms.
Signed-off-by: ChanHo Lee <[email protected]>
---
.../src/app/interfaces/interpreter.ts | 54 +++-
.../workspace/interpreter/interpreter.component.ts | 11 +-
.../workspace/interpreter/item/item.component.html | 3 +-
.../workspace/interpreter/item/item.component.ts | 313 ++++++++++++---------
.../src/app/services/interpreter.service.ts | 15 +-
5 files changed, 254 insertions(+), 142 deletions(-)
diff --git a/zeppelin-web-angular/src/app/interfaces/interpreter.ts
b/zeppelin-web-angular/src/app/interfaces/interpreter.ts
index 76a6e00ad0..4b289987ba 100644
--- a/zeppelin-web-angular/src/app/interfaces/interpreter.ts
+++ b/zeppelin-web-angular/src/app/interfaces/interpreter.ts
@@ -65,13 +65,52 @@ interface SnapshotPolicy {
interface Properties {
[key: string]: {
name: string;
- value: boolean;
- type: string;
- defaultValue?: string;
+ // The server serializes every property value as a string or boolean.
+ // `type: 'number'` props are still sent as quoted strings (e.g. "1000") —
+ // the type is only a UI hint, not the JSON type. null is never sent
either:
+ // Gson omits null values, so an unset value arrives as undefined (key
absent).
+ value: string | boolean;
+ type: InterpreterPropertyTypes;
+ defaultValue?: string | boolean;
description?: string;
};
}
+export interface InterpreterPropertyValue {
+ name: string;
+ value: string | boolean;
+ type: InterpreterPropertyTypes;
+}
+
+/**
+ * Request shape for creating/updating an interpreter setting.
+ * Mirrors the fields the server actually reads (InterpreterOption.java) —
+ * UI-only state such as `session`/`process` must not be sent.
+ */
+export interface InterpreterSettingOption {
+ isExistingProcess: boolean;
+ isUserImpersonate: boolean;
+ owners: string[];
+ perNote: string;
+ perUser: string;
+ /** null is accepted by the server and kept as the default -1 (unset) */
+ port: number | null;
+ host: string;
+ remote: boolean;
+ setPermission: boolean;
+}
+
+export interface InterpreterSettingRequest {
+ name: string;
+ group: string;
+ option: InterpreterSettingOption;
+ properties: Record<string, InterpreterPropertyValue>;
+ dependencies: Array<{
+ groupArtifactVersion: string;
+ exclusions: string[];
+ }>;
+}
+
interface InterpreterGroupItem {
name: string;
class: string;
@@ -91,14 +130,19 @@ interface DependenciesItem {
exclusions: string[];
}
+/**
+ * Response shape of InterpreterOption.java serialized by Gson.
+ * Primitive boolean/int fields are always present; String/List fields
+ * are omitted when null (e.g. fresh option templates from GET /interpreter).
+ */
interface Option {
remote: boolean;
port: number;
isExistingProcess: boolean;
setPermission: boolean;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- owners: any[];
isUserImpersonate: boolean;
+ host?: string;
+ owners?: string[];
perNote?: string;
perUser?: string;
}
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/interpreter/interpreter.component.ts
b/zeppelin-web-angular/src/app/pages/workspace/interpreter/interpreter.component.ts
index 1c6a7a3e86..0c31e2a752 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/interpreter/interpreter.component.ts
+++
b/zeppelin-web-angular/src/app/pages/workspace/interpreter/interpreter.component.ts
@@ -17,7 +17,12 @@ import { debounceTime } from 'rxjs/operators';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzModalService } from 'ng-zorro-antd/modal';
-import { Interpreter, InterpreterPropertyTypes, InterpreterRepository } from
'@zeppelin/interfaces';
+import {
+ Interpreter,
+ InterpreterPropertyTypes,
+ InterpreterRepository,
+ InterpreterSettingRequest
+} from '@zeppelin/interfaces';
import { InterpreterService } from '@zeppelin/services';
import { InterpreterCreateRepositoryModalComponent } from
'./create-repository-modal/create-repository-modal.component';
@@ -75,7 +80,7 @@ export class InterpreterComponent implements OnInit,
OnDestroy {
});
}
- addInterpreterSetting(data: Interpreter): void {
+ addInterpreterSetting(data: InterpreterSettingRequest): void {
this.interpreterService.addInterpreterSetting(data).subscribe(res => {
this.interpreterSettings.push(res);
this.showCreateSetting = false;
@@ -84,7 +89,7 @@ export class InterpreterComponent implements OnInit,
OnDestroy {
});
}
- updateInterpreter(data: Interpreter): void {
+ updateInterpreter(data: InterpreterSettingRequest): void {
this.interpreterService.updateInterpreter(data).subscribe(res => {
const current = this.interpreterSettings.find(e => e.name === res.name);
if (current) {
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/interpreter/item/item.component.html
b/zeppelin-web-angular/src/app/pages/workspace/interpreter/item/item.component.html
index 5d8b81b914..f38c936342 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/interpreter/item/item.component.html
+++
b/zeppelin-web-angular/src/app/pages/workspace/interpreter/item/item.component.html
@@ -257,6 +257,7 @@
<nz-form-control>
<input
nz-input
+ type="number"
formControlName="port"
placeholder=""
pattern="^()([1-9]|[1-5]?[0-9]{2,4}|6[1-4][0-9]{3}|65[1-4][0-9]{2}|655[1-2][0-9]|6553[1-5])$"
@@ -355,7 +356,7 @@
******
}
@case ('url') {
- <a [href]="control.get('value')?.value || ''"
target="_blank">
+ <a [href]="(control.get('value')?.value ??
'').toString()" target="_blank">
{{ control.get('value')?.value || '' }}
</a>
}
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/interpreter/item/item.component.ts
b/zeppelin-web-angular/src/app/pages/workspace/interpreter/item/item.component.ts
index 15e10e7e12..06634bff48 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/interpreter/item/item.component.ts
+++
b/zeppelin-web-angular/src/app/pages/workspace/interpreter/item/item.component.ts
@@ -11,22 +11,58 @@
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input,
OnDestroy, OnInit } from '@angular/core';
-import {
- AbstractControl,
- UntypedFormArray,
- UntypedFormBuilder,
- UntypedFormGroup,
- ValidationErrors,
- Validators,
- ValidatorFn
-} from '@angular/forms';
+import { AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors,
Validators } from '@angular/forms';
import { DestroyHookComponent } from '@zeppelin/core';
-import { Interpreter } from '@zeppelin/interfaces';
+import {
+ Interpreter,
+ InterpreterPropertyTypes,
+ InterpreterPropertyValue,
+ InterpreterSettingRequest
+} from '@zeppelin/interfaces';
import { InterpreterService, SecurityService, TicketService } from
'@zeppelin/services';
import { BehaviorSubject, Observable } from 'rxjs';
import { debounceTime, filter, map, switchMap, takeUntil, tap } from
'rxjs/operators';
import { InterpreterComponent } from '../interpreter.component';
+type PropertyValue = string | number | boolean | null;
+
+interface PropertyFormGroup {
+ key: FormControl<string>;
+ value: FormControl<PropertyValue>;
+ description: FormControl<string | null>;
+ type: FormControl<InterpreterPropertyTypes>;
+}
+
+interface DependencyFormGroup {
+ groupArtifactVersion: FormControl<string>;
+ exclusions: FormControl<string>;
+}
+
+interface OptionFormGroup {
+ isExistingProcess: FormControl<boolean>;
+ isUserImpersonate: FormControl<boolean>;
+ owners: FormControl<string[]>;
+ perNote: FormControl<string>;
+ perUser: FormControl<string>;
+ port: FormControl<number | null>;
+ host: FormControl<string>;
+ remote: FormControl<boolean>;
+ setPermission: FormControl<boolean>;
+ // TODO: `session`/`process` are write-only leftovers from the pre-0.7
boolean isolation
+ // model, superseded by the perNote/perUser modes in ZEPPELIN-1210. They are
never read
+ // by the template, never sent to the server, and should be removed in a
follow-up.
+ session: FormControl<boolean>;
+ process: FormControl<boolean>;
+}
+
+interface InterpreterFormGroup {
+ name: FormControl<string>;
+ group: FormControl<string>;
+ option: FormGroup<OptionFormGroup>;
+ properties: FormArray<FormGroup<PropertyFormGroup>>;
+ dependencies: FormArray<FormGroup<DependencyFormGroup>>;
+}
+
@Component({
selector: 'zeppelin-interpreter-item',
templateUrl: './item.component.html',
@@ -38,12 +74,12 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
@Input() mode: 'create' | 'view' | 'edit' = 'view';
@Input() interpreter?: Interpreter;
- formGroup!: UntypedFormGroup;
- optionFormGroup!: UntypedFormGroup;
- editingPropertiesFormGroup?: UntypedFormGroup;
- editingDependenceFormGroup?: UntypedFormGroup;
- propertiesFormArray!: UntypedFormArray;
- dependenciesFormArray!: UntypedFormArray;
+ formGroup!: FormGroup<InterpreterFormGroup>;
+ optionFormGroup!: FormGroup<OptionFormGroup>;
+ editingPropertiesFormGroup?: FormGroup<PropertyFormGroup>;
+ editingDependenceFormGroup?: FormGroup<DependencyFormGroup>;
+ propertiesFormArray!: FormArray<FormGroup<PropertyFormGroup>>;
+ dependenciesFormArray!: FormArray<FormGroup<DependencyFormGroup>>;
userList$?: Observable<string[]>;
userSearchChange$: BehaviorSubject<string> | null = new BehaviorSubject('');
runningOptionMap = {
@@ -86,31 +122,41 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
this.addProperties();
this.addDependence();
const formData = this.formGroup.getRawValue();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const properties: Record<any, any> = {};
-
- formData.properties
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- .sort((e: any) => e.key)
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- .forEach((e: any) => {
- const { key, value, type } = e;
- properties[key] = {
- value,
- type,
- name: key
- };
- });
- formData.properties = properties;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- formData.dependencies.forEach((e: any) => {
- e.exclusions = e.exclusions.split(',').filter((s: string) => s !== '');
+ const properties: Record<string, InterpreterPropertyValue> = {};
+
+ formData.properties.forEach(({ key, value, type }) => {
+ properties[key] = {
+ // Numeric inputs hold JS numbers, which Gson would deserialize as
Double
+ // (e.g. 60000 -> "60000.0") and break Long/Integer parsing
(ZEPPELIN-6395),
+ // so send them as strings. Checkboxes stay real booleans, as in the
classic UI.
+ value: type === 'checkbox' ? value === true || value === 'true' :
String(value ?? ''),
+ type,
+ name: key
+ };
});
+ // session/process are UI-only state derived from perNote/perUser;
+ // the server-side InterpreterOption has no such fields, so they are not
sent.
+ const { isExistingProcess, isUserImpersonate, owners, perNote, perUser,
port, host, remote, setPermission } =
+ formData.option;
+ const setting: InterpreterSettingRequest = {
+ name: formData.name,
+ group: formData.group,
+ option: { isExistingProcess, isUserImpersonate, owners, perNote,
perUser, port, host, remote, setPermission },
+ properties,
+ dependencies: formData.dependencies.map(({ groupArtifactVersion,
exclusions }) => ({
+ groupArtifactVersion,
+ exclusions: exclusions
+ .split(',')
+ .map(s => s.trim())
+ .filter(s => s !== '')
+ }))
+ };
+
if (this.mode === 'create') {
- this.parent.addInterpreterSetting(formData);
+ this.parent.addInterpreterSetting(setting);
} else {
- this.parent.updateInterpreter(formData);
+ this.parent.updateInterpreter(setting);
this.mode = 'view';
}
}
@@ -146,11 +192,11 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
this.cdr.markForCheck();
}
- onTypeChange(type: string) {
+ onTypeChange(type: InterpreterPropertyTypes) {
if (!this.editingPropertiesFormGroup) {
throw new Error("'editingPropertiesFormGroup' is not defined. Please
check if it is initialized properly.");
}
- let valueSet: string | boolean | number;
+ let valueSet: PropertyValue;
switch (type) {
case 'number':
valueSet = 0;
@@ -161,7 +207,7 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
default:
valueSet = '';
}
- this.editingPropertiesFormGroup.get('value')!.setValue(valueSet);
+ this.editingPropertiesFormGroup.controls.value.setValue(valueSet);
}
addDependence(): void {
@@ -172,17 +218,12 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
if (this.editingDependenceFormGroup.valid) {
const data = this.editingDependenceFormGroup.getRawValue();
const current = this.dependenciesFormArray.controls.find(
- control => control.get('groupArtifactVersion')!.value ===
data.groupArtifactVersion
+ control => control.controls.groupArtifactVersion.value ===
data.groupArtifactVersion
);
if (current) {
- current.get('exclusions')!.setValue(data.exclusions);
+ current.controls.exclusions.setValue(data.exclusions);
} else {
- this.dependenciesFormArray.push(
- this.formBuilder.group({
- groupArtifactVersion: [data.groupArtifactVersion,
[Validators.required]],
- exclusions: data.exclusions
- })
- );
+ this.dependenciesFormArray.push(this.createDependencyFormGroup(data));
}
this.editingDependenceFormGroup.reset({
exclusions: '',
@@ -199,19 +240,12 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
if (this.editingPropertiesFormGroup.valid) {
const data = this.editingPropertiesFormGroup.getRawValue();
- const current = this.propertiesFormArray.controls.find(control =>
control.get('key')!.value === data.key);
+ const current = this.propertiesFormArray.controls.find(control =>
control.controls.key.value === data.key);
if (current) {
- current.get('value')!.setValue(data.value);
- current.get('type')!.setValue(data.type);
+ current.controls.value.setValue(data.value);
+ current.controls.type.setValue(data.type);
} else {
- this.propertiesFormArray.push(
- this.formBuilder.group({
- key: [data.key, [Validators.required]],
- value: data.value || '',
- description: null,
- type: data.type
- })
- );
+ this.propertiesFormArray.push(this.createPropertyFormGroup({ ...data,
value: data.value ?? '' }));
}
this.editingPropertiesFormGroup.reset({
key: '',
@@ -225,8 +259,8 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
setInterpreterRunningOption(perNote: string, perUser: string) {
const { sharedModeName, globallyModeName, perNoteModeName, perUserModeName
} = this.runningOptionMap;
- this.optionFormGroup.get('perNote')!.setValue(perNote);
- this.optionFormGroup.get('perUser')!.setValue(perUser);
+ this.optionFormGroup.controls.perNote.setValue(perNote);
+ this.optionFormGroup.controls.perUser.setValue(perUser);
// Globally == shared_perNote + shared_perUser
if (perNote === sharedModeName && perUser === sharedModeName) {
@@ -252,34 +286,34 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
}
}
- this.optionFormGroup.get('perNote')!.setValue(sharedModeName);
- this.optionFormGroup.get('perUser')!.setValue(sharedModeName);
+ this.optionFormGroup.controls.perNote.setValue(sharedModeName);
+ this.optionFormGroup.controls.perUser.setValue(sharedModeName);
this.interpreterRunningOption = globallyModeName;
}
setPerNoteOrUserOption(type: 'perNote' | 'perUser', value: string) {
- this.optionFormGroup.get(type)!.setValue(value);
+ this.optionFormGroup.controls[type].setValue(value);
switch (value) {
case this.sessionOptionMap.isolated:
- this.optionFormGroup.get('session')!.setValue(false);
- this.optionFormGroup.get('process')!.setValue(true);
+ this.optionFormGroup.controls.session.setValue(false);
+ this.optionFormGroup.controls.process.setValue(true);
break;
case this.sessionOptionMap.scoped:
- this.optionFormGroup.get('session')!.setValue(true);
- this.optionFormGroup.get('process')!.setValue(false);
+ this.optionFormGroup.controls.session.setValue(true);
+ this.optionFormGroup.controls.process.setValue(false);
break;
case this.sessionOptionMap.shared:
- this.optionFormGroup.get('session')!.setValue(false);
- this.optionFormGroup.get('process')!.setValue(false);
+ this.optionFormGroup.controls.session.setValue(false);
+ this.optionFormGroup.controls.process.setValue(false);
break;
}
}
- nameValidator(control: AbstractControl): ValidationErrors | null {
+ nameValidator(control: AbstractControl<string>): ValidationErrors | null {
if (this.mode !== 'create') {
return null;
}
- const name = (control.value as string).trim();
+ const name = control.value.trim();
const exist = this.parent.interpreterSettings.find(e => e.name === name);
if (exist) {
return { exist: true, message: `Name '${name}' already exists` };
@@ -291,25 +325,24 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
buildForm(): void {
let name = '';
let group = '';
- this.optionFormGroup = this.formBuilder.group({
- isExistingProcess: false,
- isUserImpersonate: false,
- owners: [[]],
- perNote: '',
- perUser: '',
- port: [
- null,
-
[Validators.pattern('^()([1-9]|[1-5]?[0-9]{2,4}|6[1-4][0-9]{3}|65[1-4][0-9]{2}|655[1-2][0-9]|6553[1-5])$')]
- ],
- host: '',
- remote: true,
- setPermission: false,
- session: false,
- process: false
+ this.optionFormGroup = new FormGroup<OptionFormGroup>({
+ isExistingProcess: new FormControl(false, { nonNullable: true }),
+ isUserImpersonate: new FormControl(false, { nonNullable: true }),
+ owners: new FormControl<string[]>([], { nonNullable: true }),
+ perNote: new FormControl('', { nonNullable: true }),
+ perUser: new FormControl('', { nonNullable: true }),
+ port: new FormControl<number | null>(null, [
+
Validators.pattern('^()([1-9]|[1-5]?[0-9]{2,4}|6[1-4][0-9]{3}|65[1-4][0-9]{2}|655[1-2][0-9]|6553[1-5])$')
+ ]),
+ host: new FormControl('', { nonNullable: true }),
+ remote: new FormControl(true, { nonNullable: true }),
+ setPermission: new FormControl(false, { nonNullable: true }),
+ session: new FormControl(false, { nonNullable: true }),
+ process: new FormControl(false, { nonNullable: true })
});
- this.propertiesFormArray = this.formBuilder.array([]);
- this.dependenciesFormArray = this.formBuilder.array([]);
+ this.propertiesFormArray = new FormArray<FormGroup<PropertyFormGroup>>([]);
+ this.dependenciesFormArray = new
FormArray<FormGroup<DependencyFormGroup>>([]);
if (this.mode === 'view' && this.interpreter) {
name = this.interpreter.name;
@@ -324,18 +357,18 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
// set dependencies fields
this.interpreter.dependencies.forEach(e => {
const exclusions = Array.isArray(e.exclusions) ? e.exclusions : [];
- this.dependenciesFormArray!.push(
- this.formBuilder.group({
- exclusions: [exclusions.join(',')],
- groupArtifactVersion: [e.groupArtifactVersion,
[Validators.required]]
+ this.dependenciesFormArray.push(
+ this.createDependencyFormGroup({
+ exclusions: exclusions.join(','),
+ groupArtifactVersion: e.groupArtifactVersion
})
);
});
// set properties fields
Object.entries(this.interpreter.properties).forEach(([key, item]) => {
- this.propertiesFormArray!.push(
- this.formBuilder.group({
+ this.propertiesFormArray.push(
+ this.createPropertyFormGroup({
key,
value: item.value,
description: null,
@@ -345,9 +378,12 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
});
}
- this.formGroup = this.formBuilder.group({
- name: [name, [Validators.required, (c: Parameters<ValidatorFn>[0]) =>
this.nameValidator(c)]],
- group: [group, [Validators.required]],
+ this.formGroup = new FormGroup<InterpreterFormGroup>({
+ name: new FormControl(name, {
+ nonNullable: true,
+ validators: [Validators.required, control =>
this.nameValidator(control)]
+ }),
+ group: new FormControl(group, { nonNullable: true, validators:
[Validators.required] }),
option: this.optionFormGroup,
properties: this.propertiesFormArray,
dependencies: this.dependenciesFormArray
@@ -368,43 +404,40 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
})
);
- this.editingPropertiesFormGroup = this.formBuilder.group({
- key: ['', [Validators.required]],
+ this.editingPropertiesFormGroup = this.createPropertyFormGroup({
+ key: '',
value: '',
description: null,
type: 'string'
});
- this.editingDependenceFormGroup = this.formBuilder.group({
- groupArtifactVersion: ['', [Validators.required]],
- exclusions: ['']
+ this.editingDependenceFormGroup = this.createDependencyFormGroup({
+ groupArtifactVersion: '',
+ exclusions: ''
});
if (this.mode === 'create') {
- this.formGroup
- .get('group')!
- .valueChanges.pipe(takeUntil(this.destroy$))
- .subscribe(value => {
- // remove all controls
- while (this.propertiesFormArray!.length) {
- this.propertiesFormArray!.removeAt(0);
- }
-
- const interpreters = this.parent.availableInterpreters.filter(e =>
e.group === value);
- interpreters.forEach(interpreter => {
- Object.entries(interpreter.properties).forEach(([key, item]) => {
- this.propertiesFormArray!.push(
- this.formBuilder.group({
- key: [key, [Validators.required]],
- value: item.defaultValue,
- description: item.description,
- type: item.type
- })
- );
- });
+
this.formGroup.controls.group.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value
=> {
+ // remove all controls
+ while (this.propertiesFormArray.length) {
+ this.propertiesFormArray.removeAt(0);
+ }
+
+ const interpreters = this.parent.availableInterpreters.filter(e =>
e.group === value);
+ interpreters.forEach(interpreter => {
+ Object.entries(interpreter.properties).forEach(([key, item]) => {
+ this.propertiesFormArray.push(
+ this.createPropertyFormGroup({
+ key,
+ value: item.defaultValue ?? '',
+ description: item.description ?? null,
+ type: item.type
+ })
+ );
});
- this.cdr.markForCheck();
});
+ this.cdr.markForCheck();
+ });
}
}
@@ -413,7 +446,6 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
public ticketService: TicketService,
private securityService: SecurityService,
private interpreterService: InterpreterService,
- private formBuilder: UntypedFormBuilder,
private cdr: ChangeDetectorRef
) {
super();
@@ -421,7 +453,7 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
ngOnInit() {
this.buildForm();
- const option = this.optionFormGroup!.getRawValue();
+ const option = this.optionFormGroup.getRawValue();
this.setInterpreterRunningOption(option.perNote, option.perUser);
if (this.mode !== 'view') {
@@ -437,4 +469,31 @@ export class InterpreterItemComponent extends
DestroyHookComponent implements On
this.userSearchChange$ = null;
super.ngOnDestroy();
}
+
+ private createPropertyFormGroup(property: {
+ key: string;
+ value: PropertyValue;
+ description: string | null;
+ type: InterpreterPropertyTypes;
+ }): FormGroup<PropertyFormGroup> {
+ return new FormGroup<PropertyFormGroup>({
+ key: new FormControl(property.key, { nonNullable: true, validators:
[Validators.required] }),
+ value: new FormControl<PropertyValue>(property.value),
+ description: new FormControl(property.description),
+ type: new FormControl(property.type, { nonNullable: true })
+ });
+ }
+
+ private createDependencyFormGroup(dependency: {
+ groupArtifactVersion: string;
+ exclusions: string;
+ }): FormGroup<DependencyFormGroup> {
+ return new FormGroup<DependencyFormGroup>({
+ groupArtifactVersion: new FormControl(dependency.groupArtifactVersion, {
+ nonNullable: true,
+ validators: [Validators.required]
+ }),
+ exclusions: new FormControl(dependency.exclusions, { nonNullable: true })
+ });
+ }
}
diff --git a/zeppelin-web-angular/src/app/services/interpreter.service.ts
b/zeppelin-web-angular/src/app/services/interpreter.service.ts
index e0f60bf836..0deef5b7e5 100644
--- a/zeppelin-web-angular/src/app/services/interpreter.service.ts
+++ b/zeppelin-web-angular/src/app/services/interpreter.service.ts
@@ -18,7 +18,8 @@ import {
Interpreter,
InterpreterMap,
InterpreterPropertyTypes,
- InterpreterRepository
+ InterpreterRepository,
+ InterpreterSettingRequest
} from '@zeppelin/interfaces';
import { InterpreterItem } from '@zeppelin/sdk';
@@ -60,13 +61,15 @@ export class InterpreterService extends BaseRest {
return
this.http.get<InterpreterPropertyTypes[]>(this.restUrl`/interpreter/property/types`);
}
- addInterpreterSetting(interpreter: Interpreter) {
- return this.http.post<Interpreter>(this.restUrl`/interpreter/setting`,
interpreter);
+ addInterpreterSetting(setting: InterpreterSettingRequest) {
+ return this.http.post<Interpreter>(this.restUrl`/interpreter/setting`,
setting);
}
- updateInterpreter(interpreter: Interpreter) {
- const { option, properties, dependencies } = interpreter;
- return
this.http.put<Interpreter>(this.restUrl`/interpreter/setting/${interpreter.name}`,
{
+ updateInterpreter(setting: InterpreterSettingRequest) {
+ // PUT only accepts option/properties/dependencies; name is used as the
path id
+ // and group is immutable after creation (see
UpdateInterpreterSettingRequest.java).
+ const { option, properties, dependencies } = setting;
+ return
this.http.put<Interpreter>(this.restUrl`/interpreter/setting/${setting.name}`, {
option,
properties,
dependencies