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
The following commit(s) were added to refs/heads/web_angular by this push: new cc45a8b [ZEPPELIN-4399] Add credential page cc45a8b is described below commit cc45a8b640d4f7f9fa0cc58ff5d82e428df1f240 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';