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 + + + + + {{ option }} + + + + + + Username + + + + + + Password + + + + +
+
+ + + + + + +
+
+
+
+
+
+ + + + 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';