diff options
Diffstat (limited to 'portal-FE-common/src/app/pages/users')
8 files changed, 1217 insertions, 0 deletions
diff --git a/portal-FE-common/src/app/pages/users/bulk-user/bulk-user.component.html b/portal-FE-common/src/app/pages/users/bulk-user/bulk-user.component.html new file mode 100644 index 00000000..e988c317 --- /dev/null +++ b/portal-FE-common/src/app/pages/users/bulk-user/bulk-user.component.html @@ -0,0 +1,130 @@ +<!-- + ============LICENSE_START========================================== + ONAP Portal + =================================================================== + Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + =================================================================== + + Unless otherwise specified, all software contained herein is licensed + under the Apache License, Version 2.0 (the "License"); + you may not use this software 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. + + Unless otherwise specified, all documentation contained herein is licensed + under the Creative Commons License, Attribution 4.0 Intl. (the "License"); + you may not use this documentation except in compliance with the License. + You may obtain a copy of the License at + + https://creativecommons.org/licenses/by/4.0/ + + Unless required by applicable law or agreed to in writing, documentation + 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. + + ============LICENSE_END============================================ + + + --> + +<div class="container"> + <div class="modal-header"> + <h4 class="modal-title">{{title}}</h4> + <button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross')"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <div *ngIf="dialogState===1"> + <mat-form-field> + <mat-label> Select Application </mat-label> + <mat-select [disabled]='adminsAppsData.length === 0'> + <mat-option [value]="select-application" (click)="changeSelectApp('select-application')">Select Application + </mat-option> + <mat-option *ngFor="let app of adminsAppsData" (click)="changeSelectApp(app)" [value]="app.value"> + {{app.title}}</mat-option> + </mat-select> + </mat-form-field> + <span class="onap-spinner" *ngIf="adminsAppsData.length === 0"></span> + </div> + <div *ngIf="dialogState===2"> + <div class="upload-instructions">Select Upload File:</div> + <!-- input type=file is difficult to style. + Instead use a label styled as a button. --> + <label class="file-label"> + <input type="file" (change)="onFileSelect($event.target)" accept="text/plain,.csv" /> + </label>{{selectedFile}} + <div class="upload-instructions">File must be .csv or .txt and have one entry per line with this format: + <pre>orgUserId, role name</pre> + </div> + </div> + <div class="bulk-upload" *ngIf="dialogState===3"> + <!-- progress indicator --> + <div class="upload-instructions" [hidden]="!isProcessing"> + {{progressMsg}} + <br> + <br> + <span class="onap-spinner"></span> + </div> + + <!-- progress indicator --> + <div class="upload-instructions" [hidden]="!isProcessedRecords"> + {{conformMsg}} + </div> + <div [hidden]="isProcessing || isProcessedRecords"> + <div class="upload-instructions"> + Click OK to upload the valid requests. + Invalid requests will be ignored.</div> + + <table mat-table [dataSource]="uploadFileDataSource"> + <!-- Search Result Column--> + <ng-container matColumnDef="line"> + <th id="rowheader-result" mat-header-cell *matHeaderCellDef> Line + <td id="table-data-{{i}}" mat-cell *matCellDef="let element; let i = index;">{{element.line}} + </td> + </ng-container> + <ng-container matColumnDef="orgUserId"> + <th id="rowheader-result" mat-header-cell *matHeaderCellDef> OrgUserID + <td id="rowheader_t1_{{i}}-roles" mat-cell *matCellDef="let element; let i=index;"> + {{element.orgUserId}} + </td> + </ng-container> + <ng-container matColumnDef="appRole"> + <th id="rowheader-result" mat-header-cell *matHeaderCellDef> App Role + <td id="table-data-{{i}}" mat-cell *matCellDef="let element; let i = index;"> + {{element.role}} + </td> + </ng-container> + <ng-container matColumnDef="status"> + <th id="rowheader-result" mat-header-cell *matHeaderCellDef> Status + <td id="table-data-{{i}}" mat-cell *matCellDef="let element; let i = index;"> + {{element.status}} + </td> + </ng-container> + + <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr> + <tr mat-row id="table-row-{{i}}" *matRowDef="let row; columns: displayedColumns; let i = index;"></tr> + </table> + </div> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-primary" *ngIf="dialogState === 2" (click)="navigateBack()">Back</button> + + <button type="submit" id="dialog1Button" class="btn btn-primary" [disabled]="selectApp" *ngIf="dialogState === 1" + (click)="uploadFileDialog()">Next</button> + <button type="button" class="btn btn-primary" *ngIf="dialogState !== 3" + (click)="activeModal.close('Close')">Close</button> + <button type="submit" class="btn btn-primary" *ngIf="dialogState === 3" (click)="updateDB()">Ok</button> + <button type="button" class="btn btn-primary" *ngIf="dialogState === 3" (click)="navigateDialog2()">Cancel</button> + </div> +</div>
\ No newline at end of file diff --git a/portal-FE-common/src/app/pages/users/bulk-user/bulk-user.component.scss b/portal-FE-common/src/app/pages/users/bulk-user/bulk-user.component.scss new file mode 100644 index 00000000..3c8cd756 --- /dev/null +++ b/portal-FE-common/src/app/pages/users/bulk-user/bulk-user.component.scss @@ -0,0 +1,45 @@ +/*- + * ============LICENSE_START========================================== + * ONAP Portal + * =================================================================== + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * =================================================================== + * + * Unless otherwise specified, all software contained herein is licensed + * under the Apache License, Version 2.0 (the "License"); + * you may not use this software 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. + * + * Unless otherwise specified, all documentation contained herein is licensed + * under the Creative Commons License, Attribution 4.0 Intl. (the "License"); + * you may not use this documentation except in compliance with the License. + * You may obtain a copy of the License at + * + * https://creativecommons.org/licenses/by/4.0/ + * + * Unless required by applicable law or agreed to in writing, documentation + * 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. + * + * ============LICENSE_END============================================ + * + * + */ +.mat-column-orgUserId { + padding: 10px; +} + +.container.bulk-upload { + overflow-y: auto; + height: 250px; +} diff --git a/portal-FE-common/src/app/pages/users/bulk-user/bulk-user.component.spec.ts b/portal-FE-common/src/app/pages/users/bulk-user/bulk-user.component.spec.ts new file mode 100644 index 00000000..05b04a96 --- /dev/null +++ b/portal-FE-common/src/app/pages/users/bulk-user/bulk-user.component.spec.ts @@ -0,0 +1,62 @@ +/*- + * ============LICENSE_START========================================== + * ONAP Portal + * =================================================================== + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * =================================================================== + * + * Unless otherwise specified, all software contained herein is licensed + * under the Apache License, Version 2.0 (the "License"); + * you may not use this software 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. + * + * Unless otherwise specified, all documentation contained herein is licensed + * under the Creative Commons License, Attribution 4.0 Intl. (the "License"); + * you may not use this documentation except in compliance with the License. + * You may obtain a copy of the License at + * + * https://creativecommons.org/licenses/by/4.0/ + * + * Unless required by applicable law or agreed to in writing, documentation + * 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. + * + * ============LICENSE_END============================================ + * + * + */ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BulkUserComponent } from './bulk-user.component'; + +describe('BulkUserComponent', () => { + let component: BulkUserComponent; + let fixture: ComponentFixture<BulkUserComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ BulkUserComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BulkUserComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-FE-common/src/app/pages/users/bulk-user/bulk-user.component.ts b/portal-FE-common/src/app/pages/users/bulk-user/bulk-user.component.ts new file mode 100644 index 00000000..70072a68 --- /dev/null +++ b/portal-FE-common/src/app/pages/users/bulk-user/bulk-user.component.ts @@ -0,0 +1,497 @@ +/*- + * ============LICENSE_START========================================== + * ONAP Portal + * =================================================================== + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * =================================================================== + * + * Unless otherwise specified, all software contained herein is licensed + * under the Apache License, Version 2.0 (the "License"); + * you may not use this software 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. + * + * Unless otherwise specified, all documentation contained herein is licensed + * under the Creative Commons License, Attribution 4.0 Intl. (the "License"); + * you may not use this documentation except in compliance with the License. + * You may obtain a copy of the License at + * + * https://creativecommons.org/licenses/by/4.0/ + * + * Unless required by applicable law or agreed to in writing, documentation + * 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. + * + * ============LICENSE_END============================================ + * + * + */ +import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; +import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { UsersService, ApplicationsService, FunctionalMenuService } from 'src/app/shared/services'; +import { ConfirmationModalComponent } from 'src/app/modals/confirmation-modal/confirmation-modal.component'; +import { MatTableDataSource } from '@angular/material'; + +@Component({ + selector: 'app-bulk-user', + templateUrl: './bulk-user.component.html', + styleUrls: ['./bulk-user.component.scss'] +}) +export class BulkUserComponent implements OnInit { + + @Input() title: string; + @Input() adminsAppsData: any; + @Output() passBackBulkUserPopup: EventEmitter<any> = new EventEmitter(); + adminApps: any; + // Roles fetched from app service + appRolesResult: any; + // Users fetched from user service + userCheckResult: any; + // Requests for user-role assignment built by validator + appUserRolesRequest: any; + fileSelected: boolean; + isProcessing: boolean; + isProcessedRecords: boolean; + dialogState: number; + selectedFile: any; + fileModel: any; + selectApp: boolean; + fileToRead: any; + selectedAppValue: any; + progressMsg: string; + conformMsg: string; + uploadFile: any; + uploadCheck: boolean; + displayedColumns: string[] = ['line', 'orgUserId', 'appRole', 'status']; + uploadFileDataSource = new MatTableDataSource(this.uploadFile); + constructor(public ngbModal: NgbModal, public activeModal: NgbActiveModal, private applicationsService: ApplicationsService, private usersService: UsersService, private functionalMenuService: FunctionalMenuService) { } + + ngOnInit() { + this.selectApp = true; + this.fileSelected = false; + this.uploadCheck = false; + // Flag that indicates background work is proceeding + this.isProcessing = true; + this.isProcessedRecords = false; + this.dialogState = 1; + } + + changeSelectApp(val: any) { + if (val === 'select-application') + this.selectApp = true; + else + this.selectApp = false; + this.selectedAppValue = val; + } + + // Answers a function that compares properties with the specified name. + getSortOrder = (prop, foldCase) => { + return function (a, b) { + let aProp = foldCase ? a[prop].toLowerCase() : a[prop]; + let bProp = foldCase ? b[prop].toLowerCase() : b[prop]; + if (aProp > bProp) + return 1; + else if (aProp < bProp) + return -1; + else + return 0; + } + } + + onFileLoad(fileLoadedEvent) { + const textFromFileLoaded = fileLoadedEvent.target.result; + let lines = textFromFileLoaded.split('\n'); + // this.uploadFile = lines; + let result = []; + var len, i, line, o; + + // Need 1-based index below + for (len = lines.length, i = 1; i <= len; ++i) { + // Use 0-based index for array + line = lines[i - 1].trim(); + if (line.length == 0) { + result.push({ + line: i, + orgUserId: '', + role: '', + status: 'Blank line' + }); + continue; + } + o = line.split(','); + if (o.length !== 2) { + // other lengths not valid for upload + result.push({ + line: i, + orgUserId: line, + role: '', + status: 'Failed to find 2 comma-separated values' + }); + } + else { + let entry = { + line: i, + orgUserId: o[0], + role: o[1] + // leave status undefined, this could be valid. + }; + if (o[0].toLowerCase() === 'orgUserId') { + // not valid for upload, so set status + entry['status'] = 'Header'; + } + else if (o[0].trim() == '' || o[1].trim() == '') { + // defend against line with only a single comma etc. + entry['status'] = 'Failed to find 2 non-empty values'; + } + result.push(entry); + } // len 2 + } // for + return result; + } + + onFileSelect(input: HTMLInputElement) { + var validExts = new Array(".csv", ".txt"); + var fileExt = input.value; + fileExt = fileExt.substring(fileExt.lastIndexOf('.')); + if (validExts.indexOf(fileExt) < 0) { + const modalFileErrorRef = this.ngbModal.open(ConfirmationModalComponent); + modalFileErrorRef.componentInstance.title = 'Confirmation'; + modalFileErrorRef.componentInstance.message = 'Invalid file selected, valid files are of ' + + validExts.toString() + ' types.' + this.uploadCheck = false; + return false; + } + else { + const files = input.files; + this.isProcessing = true; + this.conformMsg = ''; + this.isProcessedRecords = true; + this.progressMsg = 'Reading upload file..'; + if (files && files.length) { + this.uploadCheck = true; + const fileToRead = files[0]; + const fileReader = new FileReader(); + fileReader.readAsText(fileToRead, "UTF-8"); + fileReader.onloadend = (e) => { + this.uploadFile = this.onFileLoad(e); + this.uploadFile.sort(this.getSortOrder('orgUserId', true)); + let appId = this.selectedAppValue.id; + this.progressMsg = 'Fetching application roles..'; + this.functionalMenuService.getManagedRolesMenu(appId).toPromise().then((rolesObj) => { + this.appRolesResult = rolesObj; + this.progressMsg = 'Validating application roles..'; + this.verifyAppRoles(this.appRolesResult); + this.progressMsg = 'Validating Org Users..'; + let userPromises = this.buildUserChecks(); + Promise.all(userPromises).then(userPromise => { + this.evalUserCheckResults(); + let appPromises = this.buildAppRoleChecks(); + this.progressMsg = 'Querying application for user roles..'; + Promise.all(appPromises).then(() => { + this.evalAppRoleCheckResults(); + // Re sort by line for the confirmation dialog + this.uploadFile.sort(this.getSortOrder('line', false)); + // We're done, confirm box may show the table + this.progressMsg = 'Done.'; + this.isProcessing = false; + this.isProcessedRecords = false; + }, + function (error) { + this.isProcessing = false; + this.isProcessedRecords = false; + } + ); // then of app promises + }, + function (_error) { + this.isProcessing = false; + this.isProcessedRecords = false; + } + ); // then of user promises + }, + function (error) { + this.isProcessing = false; + this.isProcessedRecords = false; + } + ); + this.uploadFileDataSource = new MatTableDataSource(this.uploadFile); + this.dialogState = 3; + }; + } + } + } + + /** + * Evaluates the result set returned by the app role service. + * Sets an uploadFile array element status if a role is not defined. + * Reads and writes scope variable uploadFile. + * Reads closure variable appRolesResult. + */ + verifyAppRoles(appRolesResult: any) { + // check roles in upload file against defined app roles + this.uploadFile.forEach(function (uploadRow) { + // skip rows that already have a defined status: headers etc. + if (uploadRow.status) { + return; + } + uploadRow.role = uploadRow.role.trim(); + var foundRole = false; + for (var i = 0; i < appRolesResult.length; i++) { + if (uploadRow.role.toUpperCase() === appRolesResult[i].rolename.trim().toUpperCase()) { + foundRole = true; + break; + } + }; + if (!foundRole) { + uploadRow.status = 'Invalid role'; + }; + }); // foreach + }; // verifyRoles + + /** + * Builds and returns an array of promises to invoke the + * searchUsers service for each unique Org User UID in the input. + * Reads and writes scope variable uploadFile, which must be sorted by Org User UID. + * The promise function writes to closure variable userCheckResult + */ + buildUserChecks() { + // if (debug) + // $log.debug('BulkUserModalCtrl::buildUserChecks: uploadFile length is ' + $scope.uploadFile.length); + this.userCheckResult = []; + let promises = []; + let prevRow = null; + this.uploadFile.forEach((uploadRow) => { + if (uploadRow.status) { + // if (debug) + // $log.debug('BulkUserModalCtrl::buildUserChecks: skip row ' + uploadRow.line); + return; + }; + // detect repeated UIDs + if (prevRow == null || prevRow.orgUserId.toLowerCase() !== uploadRow.orgUserId.toLowerCase()) { + // if (debug) + // $log.debug('BulkUserModalCtrl::buildUserChecks: create request for orgUserId ' + uploadRow.orgUserId); + let userPromise = this.usersService.searchUsers(uploadRow.orgUserId).toPromise().then((usersList) => { + if (typeof usersList[0] !== "undefined") { + this.userCheckResult.push({ + orgUserId: usersList[0].orgUserId, + firstName: usersList[0].firstName, + lastName: usersList[0].lastName, + jobTitle: usersList[0].jobTitle + }); + } + else { + // User not found. + // if (debug) + // $log.debug('BulkUserModalCtrl::buildUserChecks: searchUsers returned null'); + } + }, function (error) { + // $log.error('BulkUserModalCtrl::buildUserChecks: searchUsers failed ' + JSON.stringify(error)); + }); + promises.push(userPromise); + } + else { + // if (debug) + // $log.debug('BulkUserModalCtrl::buildUserChecks: skip repeated orgUserId ' + uploadRow.orgUserId); + } + prevRow = uploadRow; + }); // foreach + return promises; + }; // buildUserChecks + + /** + * Evaluates the result set returned by the user service to set + * the uploadFile array element status if the user was not found. + * Reads and writes scope variable uploadFile. + * Reads closure variable userCheckResult. + */ + evalUserCheckResults = () => { + // if (debug) + // $log.debug('BulkUserModalCtrl::evalUserCheckResult: uploadFile length is ' + $scope.uploadFile.length); + this.uploadFile.forEach((uploadRow) => { + if (uploadRow.status) { + // if (debug) + // $log.debug('BulkUserModalCtrl::evalUserCheckResults: skip row ' + uploadRow.line); + return; + }; + let foundorgUserId = false; + this.userCheckResult.forEach(function (userItem) { + if (uploadRow.orgUserId.toLowerCase() === userItem.orgUserId.toLowerCase()) { + // if (debug) + // $log.debug('BulkUserModalCtrl::evalUserCheckResults: found orgUserId ' + uploadRow.orgUserId); + foundorgUserId = true; + }; + }); + if (!foundorgUserId) { + // if (debug) + // $log.debug('BulkUserModalCtrl::evalUserCheckResults: NO match on orgUserId ' + uploadRow.orgUserId); + uploadRow.status = 'Invalid orgUserId'; + } + }); // foreach + }; // evalUserCheckResults + + /** + * Builds and returns an array of promises to invoke the getUserAppRoles + * service for each unique Org User in the input file. + * Each promise creates an update to be sent to the remote application + * with all role names. + * Reads scope variable uploadFile, which must be sorted by Org User. + * The promise function writes to closure variable appUserRolesRequest + */ + buildAppRoleChecks() { + this.appUserRolesRequest = []; + let appId = this.selectedAppValue.id; + let promises = []; + let prevRow = null; + this.uploadFile.forEach((uploadRow) => { + if (uploadRow.status) { + return; + } + // Because the input is sorted, generate only one request for each Org User + if (prevRow == null || prevRow.orgUserId.toLowerCase() !== uploadRow.orgUserId.toLowerCase()) { + let appPromise = this.usersService.getUserAppRoles(appId, uploadRow.orgUserId, true, false).toPromise().then((userAppRolesResult) => { + // Reply for unknown user has all defined roles with isApplied=false on each. + if (typeof userAppRolesResult[0] !== "undefined") { + this.appUserRolesRequest.push({ + orgUserId: uploadRow.orgUserId, + userAppRoles: userAppRolesResult + }); + } else { + // $log.error('BulkUserModalCtrl::buildAppRoleChecks: getUserAppRoles returned ' + JSON.stringify(userAppRolesResult)); + }; + }, function (error) { + // $log.error('BulkUserModalCtrl::buildAppRoleChecks: getUserAppRoles failed ', error); + }); + promises.push(appPromise); + } else { + // if (debug) + // $log.debug('BulkUserModalCtrl::buildAppRoleChecks: duplicate orgUserId, skip: '+ uploadRow.orgUserId); + } + prevRow = uploadRow; + }); // foreach + return promises; + }; // buildAppRoleChecks + + /** + * Evaluates the result set returned by the app service and adjusts + * the list of updates to be sent to the remote application by setting + * isApplied=true for each role name found in the upload file. + * Reads and writes scope variable uploadFile. + * Reads closure variable appUserRolesRequest. + */ + evalAppRoleCheckResults() { + this.uploadFile.forEach((uploadRow) => { + if (uploadRow.status) { + return; + } + // Search for the match in the app-user-roles array + this.appUserRolesRequest.forEach((appUserRoleObj) => { + if (uploadRow.orgUserId.toLowerCase() === appUserRoleObj.orgUserId.toLowerCase()) { + let roles = appUserRoleObj.userAppRoles; + roles.forEach(function (appRoleItem) { + //if (debug) + // $log.debug('BulkUserModalCtrl::evalAppRoleCheckResults: checking uploadRow.role=' + // + uploadRow.role + ', appRoleItem.roleName= ' + appRoleItem.roleName); + if (uploadRow.role === appRoleItem.roleName) { + if (appRoleItem.isApplied) { + uploadRow.status = 'Role exists'; + } + else { + // After much back-and-forth I decided a clear indicator + // is better than blank in the table status column. + uploadRow.status = 'OK'; + appRoleItem.isApplied = true; + } + // This count is not especially interesting. + // numberUserRolesSucceeded++; + } + }); // for each role + } + }); // for each result + }); // for each row + }; // evalAppRoleCheckResults + + // Sets the variable that hides/reveals the user controls + uploadFileDialog() { + this.fileSelected = false; + this.selectedFile = null; + this.fileModel = null; + this.dialogState = 2; + } + + // Navigate between dialog screens using number: 1,2,3 + navigateBack() { + this.selectApp = true; + this.dialogState = 1; + this.fileSelected = false; + }; + + // Navigate between dialog screens using number: 1,2,3 + navigateDialog2() { + this.dialogState = 2; + }; + + /** + * Sends requests to Portal requesting user role assignment. + * That endpoint handles creation of the user at the remote app if necessary. + * Reads closure variable appUserRolesRequest. + * Invoked by the Next button on the confirmation dialog. + */ + updateDB() { + this.isProcessing = true; + this.conformMsg = ''; + this.isProcessedRecords = true; + this.progressMsg = 'Sending requests to application..'; + // if (debug) + // $log.debug('BulkUserModalCtrl::updateDB: request length is ' + appUserRolesRequest.length); + var numberUsersSucceeded = 0; + let promises = []; + this.appUserRolesRequest.forEach(appUserRoleObj => { + // if (debug) + // $log.debug('BulkUserModalCtrl::updateDB: appUserRoleObj is ' + JSON.stringify(appUserRoleObj)); + let updateRequest = { + orgUserId: appUserRoleObj.orgUserId, + appId: this.selectedAppValue.id, + appRoles: appUserRoleObj.userAppRoles + }; + // if (debug) + // $log.debug('BulkUserModalCtrl::updateDB: updateRequest is ' + JSON.stringify(updateRequest)); + let updatePromise = this.usersService.updateUserAppRoles(updateRequest).toPromise().then(res => { + // if (debug) + // $log.debug('BulkUserModalCtrl::updateDB: updated successfully: ' + JSON.stringify(res)); + numberUsersSucceeded++; + }).catch(err => { + // What to do if one of many fails?? + // $log.error('BulkUserModalCtrl::updateDB failed: ', err); + const modelErrorRef = this.ngbModal.open(ConfirmationModalComponent); + modelErrorRef.componentInstance.title = 'Error'; + modelErrorRef.componentInstance.message = 'Failed to update the user application roles. ' + + 'Error: ' + err.status; + }).finally(() => { + // $log.debug('BulkUserModalCtrl::updateDB: finally()'); + }); + promises.push(updatePromise); + }); // for each + + // Run all the promises + Promise.all(promises).then(() => { + + this.conformMsg = 'Processed ' + numberUsersSucceeded + ' users.'; + const modelRef = this.ngbModal.open(ConfirmationModalComponent); + modelRef.componentInstance.title = 'Confirmation'; + modelRef.componentInstance.message = this.conformMsg + this.isProcessing = false; + this.isProcessedRecords = true; + this.uploadFile = []; + this.dialogState = 2; + }); + }; // updateDb + +}
\ No newline at end of file diff --git a/portal-FE-common/src/app/pages/users/users.component.html b/portal-FE-common/src/app/pages/users/users.component.html new file mode 100644 index 00000000..8f01deab --- /dev/null +++ b/portal-FE-common/src/app/pages/users/users.component.html @@ -0,0 +1,121 @@ +<!-- + ============LICENSE_START========================================== + ONAP Portal + =================================================================== + Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + =================================================================== + + Unless otherwise specified, all software contained herein is licensed + under the Apache License, Version 2.0 (the "License"); + you may not use this software 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. + + Unless otherwise specified, all documentation contained herein is licensed + under the Creative Commons License, Attribution 4.0 Intl. (the "License"); + you may not use this documentation except in compliance with the License. + You may obtain a copy of the License at + + https://creativecommons.org/licenses/by/4.0/ + + Unless required by applicable law or agreed to in writing, documentation + 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. + + ============LICENSE_END============================================ + + + --> + +<div class="container"> + <div class="onap-main-view-title"> + <h1 class="heading-page">Users</h1> + </div> + <span *ngIf="showSpinner" class="onap-spinner"></span> + <mat-form-field> + <mat-label> Select Application </mat-label> + <mat-select [disabled]='adminApps.length === 0'> + <mat-option [value]="select-application" (click)="applyDropdownFilter('select-application')">Select Application + </mat-option> + <mat-option *ngFor="let app of adminApps" [value]="app.value" (click)="applyDropdownFilter(app)"> + {{app.title}}</mat-option> + </mat-select> + </mat-form-field> + + <mat-form-field> + <input matInput type="text" (keyup)="applyFilter($event.target.value)" placeholder="Search in entire table"> + </mat-form-field> + <button type="button" class="btn btn-primary" (click)="openBulkUserUploadModal()"><i + class="icon ion-md-cloud-upload"></i> + Bulk Upload</button> + <button type="button" class="btn btn-primary" (click)="openAddNewUserModal()"><i class="icon ion-md-person-add"></i> + Add </button> + <div class="error-text" id="div-select-app" [hidden]="!noAppSelected || adminApps.length === 0"> + <p class="error-help">Use the 'Select application' dropdown to see users.</p> + </div> + <div class="error-text" id="div-error-no-users" [hidden]="!noUsersInApp"> + <p> </p> + <p class="error-help"> + No users found. Select "Add User" to add a User to the application. + </p> + </div> + <div class="error-text" id="div-error-app-down" [hidden]="!appsIsDown"> + <p> </p> + <p class="error-help"> + Failed to communicate with the application. + Please try again later or contact a system administrator. + </p> + </div> + <div class="error-text" id="div-error-403" [hidden]="!adminAppsIsNull"> + <h1>Attention:</h1> + <p> </p> + <p class="error-help">It appears that you have not been added as an admin yet to an application.</p> + <p> </p> + <p class="error-help">Click on the Admins link to the left and check and see if you are listed as an admin for an + application. + If not, you can add yourself to the appropriate application.</p> + </div> + <table mat-table [dataSource]="adminsDataSource" matSort> + <!-- First Name Column --> + <ng-container matColumnDef="firstName"> + <th id="col1" mat-header-cell *matHeaderCellDef mat-sort-header> First Name </th> + <td id="rowheader_t1_{{i}}-firstName" mat-cell *matCellDef="let element; let i = index;"> {{element.firstName}} + </td> + </ng-container> + + <!-- Last Name Column --> + <ng-container matColumnDef="lastName"> + <th id="col2" mat-header-cell *matHeaderCellDef mat-sort-header> Last Name </th> + <td id="rowheader_t1_{{i}}-lastName" mat-cell *matCellDef="let element; let i=index;"> {{element.lastName}} + </td> + </ng-container> + + <!-- User ID Column --> + <ng-container matColumnDef="userId"> + <th id="col3" mat-header-cell *matHeaderCellDef mat-sort-header> User ID </th> + <td id="rowheader_t1_{{i}}-userId" mat-cell *matCellDef="let element; let i=index;"> {{element.orgUserId}} + </td> + </ng-container> + + <!-- Roles Column --> + <ng-container matColumnDef="roles"> + <th id="col4" mat-header-cell *matHeaderCellDef> Roles </th> + <td id="rowheader_t1_{{i}}-applications" mat-cell *matCellDef="let element; let i=index;"> + <div *ngFor="let element of element.roles; let i=index;"> {{element.name}} </div> + </td> + </ng-container> + + <tr [hidden]="accountUsers.length === 0" mat-header-row *matHeaderRowDef="displayedColumns"></tr> + <tr mat-row *matRowDef="let row; columns: displayedColumns;" (click)="openExistingUserModal(row)"></tr> + </table> + <mat-paginator [hidden]="accountUsers.length === 0" [pageSizeOptions]="[10, 20]" showFirstLastButtons></mat-paginator> +</div>
\ No newline at end of file diff --git a/portal-FE-common/src/app/pages/users/users.component.scss b/portal-FE-common/src/app/pages/users/users.component.scss new file mode 100644 index 00000000..eebe72f4 --- /dev/null +++ b/portal-FE-common/src/app/pages/users/users.component.scss @@ -0,0 +1,66 @@ +/*- + * ============LICENSE_START========================================== + * ONAP Portal + * =================================================================== + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * =================================================================== + * + * Unless otherwise specified, all software contained herein is licensed + * under the Apache License, Version 2.0 (the "License"); + * you may not use this software 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. + * + * Unless otherwise specified, all documentation contained herein is licensed + * under the Creative Commons License, Attribution 4.0 Intl. (the "License"); + * you may not use this documentation except in compliance with the License. + * You may obtain a copy of the License at + * + * https://creativecommons.org/licenses/by/4.0/ + * + * Unless required by applicable law or agreed to in writing, documentation + * 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. + * + * ============LICENSE_END============================================ + * + * + */ +@import "../pages.component"; + +.mat-row { + cursor: pointer; +} + +.onap-spinner{ + z-index: 99999; +} + +.error-text { + width: 1170px; + margin: auto; + padding: 10px; + left: 20px; + font-weight: bold; + font-size: 16px; + text-align: left; + color: red; + .error-help { + color: grey; //@portalDGray; + font-weight: normal; + } + + .error-help-bold { + color: grey; //@portalDGray; + font-weight: bold; + } +} diff --git a/portal-FE-common/src/app/pages/users/users.component.spec.ts b/portal-FE-common/src/app/pages/users/users.component.spec.ts new file mode 100644 index 00000000..60d024ba --- /dev/null +++ b/portal-FE-common/src/app/pages/users/users.component.spec.ts @@ -0,0 +1,62 @@ +/*- + * ============LICENSE_START========================================== + * ONAP Portal + * =================================================================== + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * =================================================================== + * + * Unless otherwise specified, all software contained herein is licensed + * under the Apache License, Version 2.0 (the "License"); + * you may not use this software 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. + * + * Unless otherwise specified, all documentation contained herein is licensed + * under the Creative Commons License, Attribution 4.0 Intl. (the "License"); + * you may not use this documentation except in compliance with the License. + * You may obtain a copy of the License at + * + * https://creativecommons.org/licenses/by/4.0/ + * + * Unless required by applicable law or agreed to in writing, documentation + * 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. + * + * ============LICENSE_END============================================ + * + * + */ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UsersComponent } from './users.component'; + +describe('UsersComponent', () => { + let component: UsersComponent; + let fixture: ComponentFixture<UsersComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ UsersComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UsersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-FE-common/src/app/pages/users/users.component.ts b/portal-FE-common/src/app/pages/users/users.component.ts new file mode 100644 index 00000000..23538b5f --- /dev/null +++ b/portal-FE-common/src/app/pages/users/users.component.ts @@ -0,0 +1,234 @@ +/*- + * ============LICENSE_START========================================== + * ONAP Portal + * =================================================================== + * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved. + * =================================================================== + * + * Unless otherwise specified, all software contained herein is licensed + * under the Apache License, Version 2.0 (the "License"); + * you may not use this software 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. + * + * Unless otherwise specified, all documentation contained herein is licensed + * under the Creative Commons License, Attribution 4.0 Intl. (the "License"); + * you may not use this documentation except in compliance with the License. + * You may obtain a copy of the License at + * + * https://creativecommons.org/licenses/by/4.0/ + * + * Unless required by applicable law or agreed to in writing, documentation + * 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. + * + * ============LICENSE_END============================================ + * + * + */ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { MatTableDataSource, MatSort, MatPaginator } from '@angular/material'; +import { ApplicationsService, UsersService } from 'src/app/shared/services'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ConfirmationModalComponent } from 'src/app/modals/confirmation-modal/confirmation-modal.component'; +import { UserAdminApps } from 'src/app/shared/model'; +import { HttpErrorResponse } from '@angular/common/http'; +import { NewUserModalComponent } from './new-user-modal/new-user-modal.component'; +import { BulkUserComponent } from './bulk-user/bulk-user.component'; + +@Component({ + selector: 'app-users', + templateUrl: './users.component.html', + styleUrls: ['./users.component.scss'] +}) +export class UsersComponent implements OnInit { + multiAppAdmin: boolean; + adminApps: any; + selectApp = 'select-application'; + selectedApp: any; + appsIsDown: boolean; + noUsersInApp: boolean; + searchString: string; + isAppSelectDisabled: boolean; + accountUsers: any; + noAppSelected: boolean; + @ViewChild(MatSort) sort: MatSort; + @ViewChild(MatPaginator) paginator: MatPaginator; + displayedColumns: string[] = ['firstName', 'lastName', 'userId', 'roles']; + adminsDataSource = new MatTableDataSource(this.accountUsers); + adminsData: []; + showSpinner: boolean; + adminAppsIsNull: any; + + constructor(private applicationsService: ApplicationsService, public ngbModal: NgbModal, + private usersService: UsersService) { } + + ngOnInit() { + this.adminApps = []; + this.accountUsers = []; + this.getAdminApps(); + } + + openAddNewUserModal() { + const modalRef = this.ngbModal.open(NewUserModalComponent); + modalRef.componentInstance.title = 'New User'; + modalRef.componentInstance.dialogState = 1; + modalRef.componentInstance.disableBack = false; + modalRef.componentInstance.passBackNewUserPopup.subscribe((_result: any) => { + this.showSpinner = true; + this.updateUsersList(); + }, (_reason: any) => { + return; + }); + } + + openExistingUserModal(userData: any) { + const modalRef = this.ngbModal.open(NewUserModalComponent); + modalRef.componentInstance.userTitle = `${userData.firstName}, ${userData.lastName} ` + '(' + `${userData.orgUserId}` + ')'; + modalRef.componentInstance.dialogState = 2; + modalRef.componentInstance.userModalData = userData; + modalRef.componentInstance.disableBack = true; + modalRef.componentInstance.passBackNewUserPopup.subscribe((_result: any) => { + this.showSpinner = true; + this.updateUsersList(); + }, (_reason: any) => { + return; + }); + } + + openBulkUserUploadModal() { + const modalRef = this.ngbModal.open(BulkUserComponent); + modalRef.componentInstance.title = 'Bulk User Upload'; + modalRef.componentInstance.adminsAppsData = this.adminApps; + modalRef.componentInstance.passBackBulkUserPopup.subscribe((_result: any) => { + this.showSpinner = true; + this.updateUsersList(); + }, (_reason: any) => { + return; + }); + } + + applyDropdownFilter(_appValue: any) { + if (_appValue !== 'select-application') { + this.selectedApp = _appValue; + this.selectApp = this.selectedApp.value; + this.updateUsersList(); + } else { + this.showSpinner = false; + this.noAppSelected = true; + this.accountUsers = []; + this.adminsDataSource = new MatTableDataSource(this.accountUsers); + } + } + + applyFilter(filterValue: string) { + this.adminsDataSource.filter = filterValue.trim().toLowerCase(); + } + + getAdminApps() { + this.showSpinner = true; + this.applicationsService.getAdminApps().subscribe((apps: Array<UserAdminApps>) => { + this.showSpinner = false; + if (!apps) { + return null; + } + + if (apps.length >= 2) { + this.multiAppAdmin = true; + } else { + this.adminApps = []; + } + + let sortedApps = apps.sort(this.getSortOrder("name")); + let realAppIndex = 1; + for (let i = 1; i <= sortedApps.length; i++) { + this.adminApps.push({ + index: realAppIndex, + id: sortedApps[i - 1].id, + value: sortedApps[i - 1].name, + title: sortedApps[i - 1].name + }); + realAppIndex = realAppIndex + 1; + } + this.selectApp = this.adminApps[0]; + this.adminAppsIsNull = false; + if (this.selectApp != 'select-application') { + this.isAppSelectDisabled = false; + this.noUsersInApp = false; + this.noAppSelected = true; + } + }, (_err: HttpErrorResponse) => { + this.showSpinner = false; + if (_err.status === 403) { + this.adminAppsIsNull = true; + } else { + const modalErrorRef = this.ngbModal.open(ConfirmationModalComponent); + modalErrorRef.componentInstance.title = "Error"; + if (_err.status) { //Conflict + modalErrorRef.componentInstance.message = 'Error Status: ' + _err.status + ' There was a unknown problem adding the portal admin.' + 'Please try again later.'; + } + } + }); + } + + updateUsersList() { + this.appsIsDown = false; + this.noUsersInApp = false; + // $log.debug('UsersCtrl::updateUsersList: Starting updateUsersList'); + //reset search string + this.searchString = ''; + //should i disable this too in case of moving between tabs? + this.isAppSelectDisabled = true; + //activate spinner + this.showSpinner = true; + this.accountUsers = []; + this.adminsDataSource = new MatTableDataSource(this.accountUsers); + if (this.selectApp != 'select-application' && this.selectedApp) { // 'Select Application' + this.noAppSelected = false; + this.usersService.getAccountUsers(this.selectedApp.id) + .subscribe((accountUsers: []) => { + this.isAppSelectDisabled = false; + this.accountUsers = accountUsers; + if (!accountUsers || accountUsers.length === 0) { + this.noUsersInApp = true; + } + this.showSpinner = false; + this.adminsDataSource = new MatTableDataSource(this.accountUsers); + this.adminsDataSource.paginator = this.paginator; + this.adminsDataSource.sort = this.sort; + }, (_err: HttpErrorResponse) => { + this.isAppSelectDisabled = false; + const modalErrorRef = this.ngbModal.open(ConfirmationModalComponent); + modalErrorRef.componentInstance.title = "Error"; + modalErrorRef.componentInstance.message = 'Error Status: ' + _err.status + ' There was a problem updating the users List.' + 'Please try again later.'; + this.appsIsDown = true; + this.showSpinner = false; + }) + } else { + this.isAppSelectDisabled = false; + this.showSpinner = false; + this.noUsersInApp = false; + this.noAppSelected = true; + } + }; + + getSortOrder = (prop) => { + return function (a, b) { + if (a[prop] > b[prop]) { + return 1; + } else if (a[prop] < b[prop]) { + return -1; + } + return 0; + } + } +} |