This is an automated email from the ASF dual-hosted git repository.

zjffdu pushed a commit to branch web_angular
in repository https://gitbox.apache.org/repos/asf/zeppelin.git

commit d41431b35d637e6e9a9cc6d8ef127c0e51a6861d
Author: Hsuan Lee <hsua...@gmail.com>
AuthorDate: Wed Dec 4 15:08:11 2019 +0800

    [ZEPPELIN-4399] Add credential page
    
    ### What is this PR for?
    
    Add the credential page on the reworked with Angular project
    
    ### What type of PR is it?
    [Feature]
    
    ### What is the Jira issue?
    
    https://issues.apache.org/jira/browse/ZEPPELIN-4399
    
    ### How should this be tested?
    
    Not applicable
    
    ### Screenshots (if appropriate)
    
    
![credential](https://user-images.githubusercontent.com/22736418/70136543-2697b180-16c7-11ea-9aa3-0e44b3312b78.gif)
    
    ### Questions:
    * Does the licenses files need update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Author: Hsuan Lee <hsua...@gmail.com>
    
    Closes #3537 from hsuanxyz/feat/credential and squashes the following 
commits:
    
    9fc6a9d24 [Hsuan Lee] feat: add credential page
---
 .../interfaces/{public-api.ts => credential.ts}    |  21 ++-
 .../src/app/interfaces/public-api.ts               |   1 +
 .../credential/credential-routing.module.ts}       |  21 ++-
 .../workspace/credential/credential.component.html | 134 ++++++++++++++
 .../credential/credential.component.less}          |  32 +++-
 .../workspace/credential/credential.component.ts   | 201 +++++++++++++++++++++
 .../workspace/credential/credential.module.ts      |  56 ++++++
 .../pages/workspace/workspace-routing.module.ts    |   5 +
 .../src/app/services/credential.service.ts         |  43 +++++
 .../src/app/services/public-api.ts                 |   1 +
 10 files changed, 500 insertions(+), 15 deletions(-)

diff --git a/zeppelin-web-angular/src/app/interfaces/public-api.ts 
b/zeppelin-web-angular/src/app/interfaces/credential.ts
similarity index 67%
copy from zeppelin-web-angular/src/app/interfaces/public-api.ts
copy to zeppelin-web-angular/src/app/interfaces/credential.ts
index 1e07b8e..533b120 100644
--- a/zeppelin-web-angular/src/app/interfaces/public-api.ts
+++ b/zeppelin-web-angular/src/app/interfaces/credential.ts
@@ -10,8 +10,19 @@
  * limitations under the License.
  */
 
-export * from './ticket';
-export * from './trash-folder-id';
-export * from './interpreter';
-export * from './message-interceptor';
-export * from './security';
+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 1e07b8e..f00e442 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/interfaces/public-api.ts 
b/zeppelin-web-angular/src/app/pages/workspace/credential/credential-routing.module.ts
similarity index 59%
copy from zeppelin-web-angular/src/app/interfaces/public-api.ts
copy to 
zeppelin-web-angular/src/app/pages/workspace/credential/credential-routing.module.ts
index 1e07b8e..5624c7e 100644
--- a/zeppelin-web-angular/src/app/interfaces/public-api.ts
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/credential/credential-routing.module.ts
@@ -9,9 +9,20 @@
  * 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';
 
-export * from './ticket';
-export * from './trash-folder-id';
-export * from './interpreter';
-export * from './message-interceptor';
-export * from './security';
+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 0000000..51b0c84
--- /dev/null
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/credential/credential.component.html
@@ -0,0 +1,134 @@
+<!--
+  ~ 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.
+  -->
+
+<zeppelin-page-header
+  title="Credentials"
+  [extra]="headerExtra"
+  [description]="credentialsDescription">
+  <ng-template #credentialsDescription>
+    Manage your credentials. You can add new credential information. <a 
[href]="docsLink" target="_blank">
+    <i
+      nz-icon
+      nz-tooltip
+      nzTitle="Learn more"
+      nzType="question-circle"
+      nzTheme="outline"
+    ></i>
+  </a>
+
+  </ng-template>
+  <ng-template #headerExtra>
+    <button
+      class="repository-trigger"
+      nz-button
+      [nzType]="showAdd ? 'primary' : 'default'"
+      (click)="triggerAdd()">
+      <i nz-icon nzType="plus" nzTheme="outline"></i>
+      Add
+    </button>
+  </ng-template>
+  <div [@collapseMotion]="showAdd ? 'expanded' : 'collapsed' ">
+    <nz-divider nzType="horizontal"></nz-divider>
+    <h2>Add new credential</h2>
+    <form nz-form nzLayout="inline" [formGroup]="addForm" 
(ngSubmit)="submitForm()">
+      <div nz-row>
+        <div nz-col [nzSpan]="16">
+          <nz-form-item>
+            <nz-form-label>Entity</nz-form-label>
+            <nz-form-control nzErrorTip="Please input entity!">
+              <input [nzAutocomplete]="auto"
+                     (input)="onEntityInput($event)"
+                     formControlName="entity"
+                     nz-input
+                     placeholder="[Group].[Name]"/>
+              <nz-autocomplete nzBackfill #auto>
+                <nz-auto-option *ngFor="let option of 
interpreterFilteredNames" [nzValue]="option">
+                  {{ option }}
+                </nz-auto-option>
+              </nz-autocomplete>
+            </nz-form-control>
+          </nz-form-item>
+          <nz-form-item>
+            <nz-form-label>Username</nz-form-label>
+            <nz-form-control nzErrorTip="Please input username!">
+              <input formControlName="username" nz-input 
placeholder="Username"/>
+            </nz-form-control>
+          </nz-form-item>
+          <nz-form-item>
+            <nz-form-label>Password</nz-form-label>
+            <nz-form-control nzErrorTip="Please input Password!">
+              <input formControlName="password" nz-input type="password" 
placeholder="Password"/>
+            </nz-form-control>
+          </nz-form-item>
+        </div>
+        <div class="new-actions" nz-col [nzSpan]="8">
+          <nz-form-item>
+            <nz-form-control>
+              <button nz-button nzType="primary" [disabled]="!addForm.valid || 
adding">Save</button>
+              <button nz-button type="button" 
(click)="cancelAdd()">Cancel</button>
+            </nz-form-control>
+          </nz-form-item>
+        </div>
+      </div>
+    </form>
+  </div>
+</zeppelin-page-header>
+<div class="content">
+  <nz-table nzSize="small"
+            [nzData]="credentialControls"
+            [nzFrontPagination]="false"
+            [nzShowPagination]="false">
+    <thead>
+    <tr>
+      <th>Entity</th>
+      <th>Username</th>
+      <th>Password</th>
+      <th nzWidth="225px" class="actions-head">Actions</th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr *ngFor="let control of credentialControls">
+      <ng-container *ngIf="control.get('entity')?.value as entity" 
[formGroup]="control">
+        <td>{{entity}}</td>
+
+        <ng-container *ngIf="isEditing(control); else credentialDisplay">
+          <td><input nz-input formControlName="username"></td>
+          <td><input nz-input type="password" formControlName="password"></td>
+          <td class="credential-actions">
+            <button nz-button nzType="primary" 
(click)="saveCredential(control)"><i nz-icon nzType="save"
+                                                                               
     nzTheme="outline"></i> Save
+            </button>
+            <button nz-button (click)="unsetEditable(control)"><i nz-icon 
nzType="close" nzTheme="outline"></i> Cancel
+            </button>
+          </td>
+        </ng-container>
+
+        <ng-template #credentialDisplay>
+          <td>{{control.get('username')?.value}}</td>
+          <td><strong>**********</strong></td>
+          <td class="credential-actions">
+            <button nz-button (click)="setEditable(control)"><i nz-icon 
nzType="edit" nzTheme="outline"></i> Edit
+            </button>
+            <button nz-button
+                    nz-popconfirm
+                    nzPopconfirmTitle="Do you want to delete this credential 
information?"
+                    (nzOnConfirm)="removeCredential(control)">
+              <i nz-icon nzType="delete" nzTheme="outline"></i> Remove
+            </button>
+          </td>
+        </ng-template>
+      </ng-container>
+    </tr>
+
+    </tbody>
+  </nz-table>
+</div>
diff --git a/zeppelin-web-angular/src/app/interfaces/public-api.ts 
b/zeppelin-web-angular/src/app/pages/workspace/credential/credential.component.less
similarity index 56%
copy from zeppelin-web-angular/src/app/interfaces/public-api.ts
copy to 
zeppelin-web-angular/src/app/pages/workspace/credential/credential.component.less
index 1e07b8e..87649ca 100644
--- a/zeppelin-web-angular/src/app/interfaces/public-api.ts
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/credential/credential.component.less
@@ -10,8 +10,30 @@
  * limitations under the License.
  */
 
-export * from './ticket';
-export * from './trash-folder-id';
-export * from './interpreter';
-export * from './message-interceptor';
-export * from './security';
+@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 0000000..47150db
--- /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<string, CredentialForm> = 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 0000000..a126980
--- /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 1b6bedf..6815b80 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 0000000..0e08f7b
--- /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<Credential>(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 155a3b2..6c3dbc0 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';

Reply via email to