diff --git a/zeppelin-web-angular/src/app/interfaces/credential.ts b/zeppelin-web-angular/src/app/interfaces/credential.ts
new file mode 100644
index 00000000000..533b1206819
--- /dev/null
+++ b/zeppelin-web-angular/src/app/interfaces/credential.ts
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+export interface Credential {
+ userCredentials: {
+ [key: string]: CredentialItem;
+ };
+}
+
+export interface CredentialItem {
+ username: string;
+ password: string;
+}
+
+export interface CredentialForm {
+ entity: string;
+ password: string;
+ username: string;
+}
diff --git a/zeppelin-web-angular/src/app/interfaces/public-api.ts b/zeppelin-web-angular/src/app/interfaces/public-api.ts
index 1e07b8e496a..f00e442594e 100644
--- a/zeppelin-web-angular/src/app/interfaces/public-api.ts
+++ b/zeppelin-web-angular/src/app/interfaces/public-api.ts
@@ -15,3 +15,4 @@ export * from './trash-folder-id';
export * from './interpreter';
export * from './message-interceptor';
export * from './security';
+export * from './credential';
diff --git a/zeppelin-web-angular/src/app/pages/workspace/credential/credential-routing.module.ts b/zeppelin-web-angular/src/app/pages/workspace/credential/credential-routing.module.ts
new file mode 100644
index 00000000000..5624c7e28ba
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/credential/credential-routing.module.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+import { CredentialComponent } from './credential.component';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: CredentialComponent
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class CredentialRoutingModule {}
diff --git a/zeppelin-web-angular/src/app/pages/workspace/credential/credential.component.html b/zeppelin-web-angular/src/app/pages/workspace/credential/credential.component.html
new file mode 100644
index 00000000000..51b0c84c208
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/credential/credential.component.html
@@ -0,0 +1,134 @@
+
+
+
+
+ Manage your credentials. You can add new credential information.
+
+
+
+
+
+
+
+
+
+
Add new credential
+
+
+
+
+
+
+
+
Entity
+
Username
+
Password
+
Actions
+
+
+
+
+
+
{{entity}}
+
+
+
+
+
+
+
+
+
+
+
+
{{control.get('username')?.value}}
+
**********
+
+
+
+
+
+
+
+
+
+
+
diff --git a/zeppelin-web-angular/src/app/pages/workspace/credential/credential.component.less b/zeppelin-web-angular/src/app/pages/workspace/credential/credential.component.less
new file mode 100644
index 00000000000..87649caf0a5
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/credential/credential.component.less
@@ -0,0 +1,39 @@
+/*
+ * 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 'theme-mixin';
+
+.themeMixin({
+ ::ng-deep {
+ .ant-form-inline .ant-form-item-with-help {
+ margin-bottom: 0;
+ }
+
+ .credential-actions, .new-actions {
+ text-align: right;
+ button+button {
+ margin-left: 8px;
+ }
+ }
+
+ .actions-head {
+ text-align: right;
+ }
+ }
+
+ .content {
+ padding: @card-padding-base / 2;
+ nz-table {
+ background: @card-background;
+ }
+ }
+});
diff --git a/zeppelin-web-angular/src/app/pages/workspace/credential/credential.component.ts b/zeppelin-web-angular/src/app/pages/workspace/credential/credential.component.ts
new file mode 100644
index 00000000000..47150dbd69f
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/credential/credential.component.ts
@@ -0,0 +1,201 @@
+/*
+ * 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 { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { collapseMotion, NzMessageService } from 'ng-zorro-antd';
+
+import { finalize } from 'rxjs/operators';
+
+import { CredentialForm } from '@zeppelin/interfaces';
+import { CredentialService, InterpreterService, TicketService } from '@zeppelin/services';
+
+@Component({
+ selector: 'zeppelin-credential',
+ templateUrl: './credential.component.html',
+ styleUrls: ['./credential.component.less'],
+ animations: [collapseMotion],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class CredentialComponent implements OnInit {
+ addForm: FormGroup;
+ showAdd = false;
+ adding = false;
+ interpreterNames: string[] = [];
+ interpreterFilteredNames: string[] = [];
+ editFlags: Map = new Map();
+ credentialFormArray: FormArray = this.fb.array([]);
+ docsLink: string;
+
+ get credentialControls(): FormGroup[] {
+ return this.credentialFormArray.controls as FormGroup[];
+ }
+
+ constructor(
+ private cdr: ChangeDetectorRef,
+ private fb: FormBuilder,
+ private nzMessageService: NzMessageService,
+ private interpreterService: InterpreterService,
+ private credentialService: CredentialService,
+ private ticketService: TicketService
+ ) {
+ this.setDocsLink();
+ }
+
+ setDocsLink() {
+ const version = this.ticketService.version;
+ this.docsLink = `https://zeppelin.apache.org/docs/${version}/setup/security/datasource_authorization.html`;
+ }
+
+ onEntityInput(event: Event) {
+ const input = event.target as HTMLInputElement;
+ if (input && input.value) {
+ this.interpreterFilteredNames = this.interpreterNames
+ .filter(e => e.indexOf(input.value.trim()) !== -1)
+ .slice(0, 10);
+ } else {
+ this.interpreterFilteredNames = this.interpreterNames.slice(0, 10);
+ }
+ }
+
+ getEntityFromForm(form: FormGroup): string {
+ return form.get('entity') && form.get('entity').value;
+ }
+
+ isEditing(form: FormGroup): boolean {
+ const entity = this.getEntityFromForm(form);
+ return entity && this.editFlags.has(entity);
+ }
+
+ setEditable(form: FormGroup) {
+ const entity = this.getEntityFromForm(form);
+ if (entity) {
+ this.editFlags.set(entity, form.getRawValue());
+ }
+ this.cdr.markForCheck();
+ }
+
+ unsetEditable(form: FormGroup, reset = true) {
+ const entity = this.getEntityFromForm(form);
+ if (reset && entity && this.editFlags.has(entity)) {
+ form.reset(this.editFlags.get(entity));
+ }
+ this.editFlags.delete(entity);
+ this.cdr.markForCheck();
+ }
+
+ submitForm(): void {
+ for (const i in this.addForm.controls) {
+ this.addForm.controls[i].markAsDirty();
+ this.addForm.controls[i].updateValueAndValidity();
+ }
+ if (this.addForm.valid) {
+ const data = this.addForm.getRawValue() as CredentialForm;
+ this.addCredential(data);
+ }
+ }
+
+ saveCredential(form: FormGroup) {
+ for (const i in form.controls) {
+ form.controls[i].markAsDirty();
+ form.controls[i].updateValueAndValidity();
+ }
+ if (form.valid) {
+ this.credentialService.updateCredential(form.getRawValue()).subscribe(() => {
+ this.nzMessageService.success('Successfully saved credentials.');
+ this.unsetEditable(form, false);
+ });
+ }
+ }
+
+ removeCredential(form: FormGroup) {
+ const entity = this.getEntityFromForm(form);
+ if (entity) {
+ this.credentialService.removeCredential(entity).subscribe(() => {
+ this.getCredentials();
+ });
+ }
+ }
+
+ triggerAdd(): void {
+ this.showAdd = !this.showAdd;
+ this.cdr.markForCheck();
+ }
+
+ cancelAdd() {
+ this.showAdd = false;
+ this.resetAddForm();
+ this.cdr.markForCheck();
+ }
+
+ getCredentials() {
+ this.credentialService.getCredentials().subscribe(data => {
+ const controls = [...Object.entries(data.userCredentials)].map(e => {
+ const entity = e[0];
+ const { username, password } = e[1];
+ return this.fb.group({
+ entity: [entity, [Validators.required]],
+ username: [username, [Validators.required]],
+ password: [password, [Validators.required]]
+ });
+ });
+ this.credentialFormArray = this.fb.array(controls);
+ this.cdr.markForCheck();
+ });
+ }
+
+ getInterpreterNames() {
+ this.interpreterService.getInterpretersSetting().subscribe(data => {
+ this.interpreterNames = data.map(e => `${e.group}.${e.name}`);
+ this.interpreterFilteredNames = this.interpreterNames.slice(0, 10);
+ this.cdr.markForCheck();
+ });
+ }
+
+ addCredential(data: CredentialForm) {
+ this.adding = true;
+ this.cdr.markForCheck();
+ this.credentialService
+ .addCredential(data)
+ .pipe(
+ finalize(() => {
+ this.adding = false;
+ this.cdr.markForCheck();
+ })
+ )
+ .subscribe(() => {
+ this.nzMessageService.success('Successfully saved credentials.');
+ this.getCredentials();
+ this.resetAddForm();
+ this.cdr.markForCheck();
+ });
+ }
+
+ resetAddForm() {
+ this.addForm.reset({
+ entity: null,
+ username: null,
+ password: null
+ });
+ this.cdr.markForCheck();
+ }
+
+ ngOnInit(): void {
+ this.getCredentials();
+ this.getInterpreterNames();
+ this.addForm = this.fb.group({
+ entity: [null, [Validators.required]],
+ username: [null, [Validators.required]],
+ password: [null, [Validators.required]]
+ });
+ }
+}
diff --git a/zeppelin-web-angular/src/app/pages/workspace/credential/credential.module.ts b/zeppelin-web-angular/src/app/pages/workspace/credential/credential.module.ts
new file mode 100644
index 00000000000..a1269804cdd
--- /dev/null
+++ b/zeppelin-web-angular/src/app/pages/workspace/credential/credential.module.ts
@@ -0,0 +1,56 @@
+/*
+ * 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 { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { ShareModule } from '@zeppelin/share';
+import {
+ NzAutocompleteModule,
+ NzButtonModule,
+ NzCardModule,
+ NzDividerModule,
+ NzFormModule,
+ NzGridModule,
+ NzIconModule,
+ NzInputModule,
+ NzMessageModule,
+ NzPopconfirmModule,
+ NzTableModule,
+ NzToolTipModule
+} from 'ng-zorro-antd';
+import { CredentialRoutingModule } from './credential-routing.module';
+import { CredentialComponent } from './credential.component';
+
+@NgModule({
+ declarations: [CredentialComponent],
+ imports: [
+ CommonModule,
+ CredentialRoutingModule,
+ FormsModule,
+ ShareModule,
+ ReactiveFormsModule,
+ NzFormModule,
+ NzAutocompleteModule,
+ NzButtonModule,
+ NzCardModule,
+ NzIconModule,
+ NzDividerModule,
+ NzInputModule,
+ NzMessageModule,
+ NzTableModule,
+ NzPopconfirmModule,
+ NzGridModule,
+ NzToolTipModule
+ ]
+})
+export class CredentialModule {}
diff --git a/zeppelin-web-angular/src/app/pages/workspace/workspace-routing.module.ts b/zeppelin-web-angular/src/app/pages/workspace/workspace-routing.module.ts
index 1b6bedf03ea..6815b8097c2 100644
--- a/zeppelin-web-angular/src/app/pages/workspace/workspace-routing.module.ts
+++ b/zeppelin-web-angular/src/app/pages/workspace/workspace-routing.module.ts
@@ -48,6 +48,11 @@ const routes: Routes = [
path: 'configuration',
loadChildren: () =>
import('@zeppelin/pages/workspace/configuration/configuration.module').then(m => m.ConfigurationModule)
+ },
+ {
+ path: 'credential',
+ loadChildren: () =>
+ import('@zeppelin/pages/workspace/credential/credential.module').then(m => m.CredentialModule)
}
]
}
diff --git a/zeppelin-web-angular/src/app/services/credential.service.ts b/zeppelin-web-angular/src/app/services/credential.service.ts
new file mode 100644
index 00000000000..0e08f7b2d96
--- /dev/null
+++ b/zeppelin-web-angular/src/app/services/credential.service.ts
@@ -0,0 +1,43 @@
+/*
+ * 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 { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Credential, CredentialForm } from '@zeppelin/interfaces';
+import { BaseRest } from './base-rest';
+import { BaseUrlService } from './base-url.service';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CredentialService extends BaseRest {
+ constructor(baseUrlService: BaseUrlService, private http: HttpClient) {
+ super(baseUrlService);
+ }
+
+ getCredentials() {
+ return this.http.get(this.restUrl`/credential`);
+ }
+
+ addCredential(data: CredentialForm) {
+ return this.http.put(this.restUrl`/credential`, data);
+ }
+
+ updateCredential(data: CredentialForm) {
+ return this.http.put(this.restUrl`/credential`, data);
+ }
+
+ removeCredential(entity: string) {
+ return this.http.delete(this.restUrl`/credential/${entity}`);
+ }
+}
diff --git a/zeppelin-web-angular/src/app/services/public-api.ts b/zeppelin-web-angular/src/app/services/public-api.ts
index 155a3b2e80f..6c3dbc07ea5 100644
--- a/zeppelin-web-angular/src/app/services/public-api.ts
+++ b/zeppelin-web-angular/src/app/services/public-api.ts
@@ -28,3 +28,4 @@ export * from './note-list.service';
export * from './runtime-compiler.service';
export * from './shortcut.service';
export * from './configuration.service';
+export * from './credential.service';