2 * ============LICENSE_START==========================================
4 * ===================================================================
5 * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved.
6 * ===================================================================
8 * Unless otherwise specified, all software contained herein is licensed
9 * under the Apache License, Version 2.0 (the "License");
10 * you may not use this software except in compliance with the License.
11 * You may obtain a copy of the License at
13 * http://www.apache.org/licenses/LICENSE-2.0
15 * Unless required by applicable law or agreed to in writing, software
16 * distributed under the License is distributed on an "AS IS" BASIS,
17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 * See the License for the specific language governing permissions and
19 * limitations under the License.
21 * Unless otherwise specified, all documentation contained herein is licensed
22 * under the Creative Commons License, Attribution 4.0 Intl. (the "License");
23 * you may not use this documentation except in compliance with the License.
24 * You may obtain a copy of the License at
26 * https://creativecommons.org/licenses/by/4.0/
28 * Unless required by applicable law or agreed to in writing, documentation
29 * distributed under the License is distributed on an "AS IS" BASIS,
30 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31 * See the License for the specific language governing permissions and
32 * limitations under the License.
34 * ============LICENSE_END============================================
38 import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
39 import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
40 import { UsersService, ApplicationsService, FunctionalMenuService } from 'src/app/shared/services';
41 import { ConfirmationModalComponent } from 'src/app/modals/confirmation-modal/confirmation-modal.component';
42 import { MatTableDataSource } from '@angular/material';
45 selector: 'app-bulk-user',
46 templateUrl: './bulk-user.component.html',
47 styleUrls: ['./bulk-user.component.scss']
49 export class BulkUserComponent implements OnInit {
51 @Input() title: string;
52 @Input() adminsAppsData: any;
53 @Output() passBackBulkUserPopup: EventEmitter<any> = new EventEmitter();
55 // Roles fetched from app service
57 // Users fetched from user service
59 // Requests for user-role assignment built by validator
60 appUserRolesRequest: any;
61 fileSelected: boolean;
62 isProcessing: boolean;
63 isProcessedRecords: boolean;
69 selectedAppValue: any;
74 displayedColumns: string[] = ['line', 'orgUserId', 'appRole', 'status'];
75 uploadFileDataSource = new MatTableDataSource(this.uploadFile);
76 constructor(public ngbModal: NgbModal, public activeModal: NgbActiveModal, private applicationsService: ApplicationsService, private usersService: UsersService, private functionalMenuService: FunctionalMenuService) { }
79 this.selectApp = true;
80 this.fileSelected = false;
81 this.uploadCheck = false;
82 // Flag that indicates background work is proceeding
83 this.isProcessing = true;
84 this.isProcessedRecords = false;
88 changeSelectApp(val: any) {
89 if (val === 'select-application')
90 this.selectApp = true;
92 this.selectApp = false;
93 this.selectedAppValue = val;
96 // Answers a function that compares properties with the specified name.
97 getSortOrder = (prop, foldCase) => {
98 return function (a, b) {
99 let aProp = foldCase ? a[prop].toLowerCase() : a[prop];
100 let bProp = foldCase ? b[prop].toLowerCase() : b[prop];
103 else if (aProp < bProp)
110 onFileLoad(fileLoadedEvent) {
111 const textFromFileLoaded = fileLoadedEvent.target.result;
112 let lines = textFromFileLoaded.split('\n');
113 // this.uploadFile = lines;
117 // Need 1-based index below
118 for (len = lines.length, i = 1; i <= len; ++i) {
119 // Use 0-based index for array
120 line = lines[i - 1].trim();
121 if (line.length == 0) {
131 if (o.length !== 2) {
132 // other lengths not valid for upload
137 status: 'Failed to find 2 comma-separated values'
145 // leave status undefined, this could be valid.
147 if (o[0].toLowerCase() === 'orgUserId') {
148 // not valid for upload, so set status
149 entry['status'] = 'Header';
151 else if (o[0].trim() == '' || o[1].trim() == '') {
152 // defend against line with only a single comma etc.
153 entry['status'] = 'Failed to find 2 non-empty values';
161 onFileSelect(input: HTMLInputElement) {
162 var validExts = new Array(".csv", ".txt");
163 var fileExt = input.value;
164 fileExt = fileExt.substring(fileExt.lastIndexOf('.'));
165 if (validExts.indexOf(fileExt) < 0) {
166 const modalFileErrorRef = this.ngbModal.open(ConfirmationModalComponent);
167 modalFileErrorRef.componentInstance.title = 'Confirmation';
168 modalFileErrorRef.componentInstance.message = 'Invalid file selected, valid files are of ' +
169 validExts.toString() + ' types.'
170 this.uploadCheck = false;
174 const files = input.files;
175 this.isProcessing = true;
176 this.conformMsg = '';
177 this.isProcessedRecords = true;
178 this.progressMsg = 'Reading upload file..';
179 if (files && files.length) {
180 this.uploadCheck = true;
181 const fileToRead = files[0];
182 const fileReader = new FileReader();
183 fileReader.readAsText(fileToRead, "UTF-8");
184 fileReader.onloadend = (e) => {
185 this.uploadFile = this.onFileLoad(e);
186 this.uploadFile.sort(this.getSortOrder('orgUserId', true));
187 let appId = this.selectedAppValue.id;
188 this.progressMsg = 'Fetching application roles..';
189 this.functionalMenuService.getManagedRolesMenu(appId).toPromise().then((rolesObj) => {
190 this.appRolesResult = rolesObj;
191 this.progressMsg = 'Validating application roles..';
192 this.verifyAppRoles(this.appRolesResult);
193 this.progressMsg = 'Validating Org Users..';
194 let userPromises = this.buildUserChecks();
195 Promise.all(userPromises).then(userPromise => {
196 this.evalUserCheckResults();
197 let appPromises = this.buildAppRoleChecks();
198 this.progressMsg = 'Querying application for user roles..';
199 Promise.all(appPromises).then(() => {
200 this.evalAppRoleCheckResults();
201 // Re sort by line for the confirmation dialog
202 this.uploadFile.sort(this.getSortOrder('line', false));
203 // We're done, confirm box may show the table
204 this.progressMsg = 'Done.';
205 this.isProcessing = false;
206 this.isProcessedRecords = false;
209 this.isProcessing = false;
210 this.isProcessedRecords = false;
212 ); // then of app promises
215 this.isProcessing = false;
216 this.isProcessedRecords = false;
218 ); // then of user promises
221 this.isProcessing = false;
222 this.isProcessedRecords = false;
225 this.uploadFileDataSource = new MatTableDataSource(this.uploadFile);
226 this.dialogState = 3;
233 * Evaluates the result set returned by the app role service.
234 * Sets an uploadFile array element status if a role is not defined.
235 * Reads and writes scope variable uploadFile.
236 * Reads closure variable appRolesResult.
238 verifyAppRoles(appRolesResult: any) {
239 // check roles in upload file against defined app roles
240 this.uploadFile.forEach(function (uploadRow) {
241 // skip rows that already have a defined status: headers etc.
242 if (uploadRow.status) {
245 uploadRow.role = uploadRow.role.trim();
246 var foundRole = false;
247 for (var i = 0; i < appRolesResult.length; i++) {
248 if (uploadRow.role.toUpperCase() === appRolesResult[i].rolename.trim().toUpperCase()) {
254 uploadRow.status = 'Invalid role';
260 * Builds and returns an array of promises to invoke the
261 * searchUsers service for each unique Org User UID in the input.
262 * Reads and writes scope variable uploadFile, which must be sorted by Org User UID.
263 * The promise function writes to closure variable userCheckResult
267 // $log.debug('BulkUserModalCtrl::buildUserChecks: uploadFile length is ' + $scope.uploadFile.length);
268 this.userCheckResult = [];
271 this.uploadFile.forEach((uploadRow) => {
272 if (uploadRow.status) {
274 // $log.debug('BulkUserModalCtrl::buildUserChecks: skip row ' + uploadRow.line);
277 // detect repeated UIDs
278 if (prevRow == null || prevRow.orgUserId.toLowerCase() !== uploadRow.orgUserId.toLowerCase()) {
280 // $log.debug('BulkUserModalCtrl::buildUserChecks: create request for orgUserId ' + uploadRow.orgUserId);
281 let userPromise = this.usersService.searchUsers(uploadRow.orgUserId).toPromise().then((usersList) => {
282 if (typeof usersList[0] !== "undefined") {
283 this.userCheckResult.push({
284 orgUserId: usersList[0].orgUserId,
285 firstName: usersList[0].firstName,
286 lastName: usersList[0].lastName,
287 jobTitle: usersList[0].jobTitle
293 // $log.debug('BulkUserModalCtrl::buildUserChecks: searchUsers returned null');
295 }, function (error) {
296 // $log.error('BulkUserModalCtrl::buildUserChecks: searchUsers failed ' + JSON.stringify(error));
298 promises.push(userPromise);
302 // $log.debug('BulkUserModalCtrl::buildUserChecks: skip repeated orgUserId ' + uploadRow.orgUserId);
307 }; // buildUserChecks
310 * Evaluates the result set returned by the user service to set
311 * the uploadFile array element status if the user was not found.
312 * Reads and writes scope variable uploadFile.
313 * Reads closure variable userCheckResult.
315 evalUserCheckResults = () => {
317 // $log.debug('BulkUserModalCtrl::evalUserCheckResult: uploadFile length is ' + $scope.uploadFile.length);
318 this.uploadFile.forEach((uploadRow) => {
319 if (uploadRow.status) {
321 // $log.debug('BulkUserModalCtrl::evalUserCheckResults: skip row ' + uploadRow.line);
324 let foundorgUserId = false;
325 this.userCheckResult.forEach(function (userItem) {
326 if (uploadRow.orgUserId.toLowerCase() === userItem.orgUserId.toLowerCase()) {
328 // $log.debug('BulkUserModalCtrl::evalUserCheckResults: found orgUserId ' + uploadRow.orgUserId);
329 foundorgUserId = true;
332 if (!foundorgUserId) {
334 // $log.debug('BulkUserModalCtrl::evalUserCheckResults: NO match on orgUserId ' + uploadRow.orgUserId);
335 uploadRow.status = 'Invalid orgUserId';
338 }; // evalUserCheckResults
341 * Builds and returns an array of promises to invoke the getUserAppRoles
342 * service for each unique Org User in the input file.
343 * Each promise creates an update to be sent to the remote application
344 * with all role names.
345 * Reads scope variable uploadFile, which must be sorted by Org User.
346 * The promise function writes to closure variable appUserRolesRequest
348 buildAppRoleChecks() {
349 this.appUserRolesRequest = [];
350 let appId = this.selectedAppValue.id;
353 this.uploadFile.forEach((uploadRow) => {
354 if (uploadRow.status) {
357 // Because the input is sorted, generate only one request for each Org User
358 if (prevRow == null || prevRow.orgUserId.toLowerCase() !== uploadRow.orgUserId.toLowerCase()) {
359 let appPromise = this.usersService.getUserAppRoles(appId, uploadRow.orgUserId, true, false).toPromise().then((userAppRolesResult) => {
360 // Reply for unknown user has all defined roles with isApplied=false on each.
361 if (typeof userAppRolesResult[0] !== "undefined") {
362 this.appUserRolesRequest.push({
363 orgUserId: uploadRow.orgUserId,
364 userAppRoles: userAppRolesResult
367 // $log.error('BulkUserModalCtrl::buildAppRoleChecks: getUserAppRoles returned ' + JSON.stringify(userAppRolesResult));
369 }, function (error) {
370 // $log.error('BulkUserModalCtrl::buildAppRoleChecks: getUserAppRoles failed ', error);
372 promises.push(appPromise);
375 // $log.debug('BulkUserModalCtrl::buildAppRoleChecks: duplicate orgUserId, skip: '+ uploadRow.orgUserId);
380 }; // buildAppRoleChecks
383 * Evaluates the result set returned by the app service and adjusts
384 * the list of updates to be sent to the remote application by setting
385 * isApplied=true for each role name found in the upload file.
386 * Reads and writes scope variable uploadFile.
387 * Reads closure variable appUserRolesRequest.
389 evalAppRoleCheckResults() {
390 this.uploadFile.forEach((uploadRow) => {
391 if (uploadRow.status) {
394 // Search for the match in the app-user-roles array
395 this.appUserRolesRequest.forEach((appUserRoleObj) => {
396 if (uploadRow.orgUserId.toLowerCase() === appUserRoleObj.orgUserId.toLowerCase()) {
397 let roles = appUserRoleObj.userAppRoles;
398 roles.forEach(function (appRoleItem) {
400 // $log.debug('BulkUserModalCtrl::evalAppRoleCheckResults: checking uploadRow.role='
401 // + uploadRow.role + ', appRoleItem.roleName= ' + appRoleItem.roleName);
402 if (uploadRow.role === appRoleItem.roleName) {
403 if (appRoleItem.isApplied) {
404 uploadRow.status = 'Role exists';
407 // After much back-and-forth I decided a clear indicator
408 // is better than blank in the table status column.
409 uploadRow.status = 'OK';
410 appRoleItem.isApplied = true;
412 // This count is not especially interesting.
413 // numberUserRolesSucceeded++;
417 }); // for each result
419 }; // evalAppRoleCheckResults
421 // Sets the variable that hides/reveals the user controls
423 this.fileSelected = false;
424 this.selectedFile = null;
425 this.fileModel = null;
426 this.dialogState = 2;
429 // Navigate between dialog screens using number: 1,2,3
431 this.selectApp = true;
432 this.dialogState = 1;
433 this.fileSelected = false;
436 // Navigate between dialog screens using number: 1,2,3
438 this.dialogState = 2;
442 * Sends requests to Portal requesting user role assignment.
443 * That endpoint handles creation of the user at the remote app if necessary.
444 * Reads closure variable appUserRolesRequest.
445 * Invoked by the Next button on the confirmation dialog.
448 this.isProcessing = true;
449 this.conformMsg = '';
450 this.isProcessedRecords = true;
451 this.progressMsg = 'Sending requests to application..';
453 // $log.debug('BulkUserModalCtrl::updateDB: request length is ' + appUserRolesRequest.length);
454 var numberUsersSucceeded = 0;
456 this.appUserRolesRequest.forEach(appUserRoleObj => {
458 // $log.debug('BulkUserModalCtrl::updateDB: appUserRoleObj is ' + JSON.stringify(appUserRoleObj));
459 let updateRequest = {
460 orgUserId: appUserRoleObj.orgUserId,
461 appId: this.selectedAppValue.id,
462 appRoles: appUserRoleObj.userAppRoles
465 // $log.debug('BulkUserModalCtrl::updateDB: updateRequest is ' + JSON.stringify(updateRequest));
466 let updatePromise = this.usersService.updateUserAppRoles(updateRequest).toPromise().then(res => {
468 // $log.debug('BulkUserModalCtrl::updateDB: updated successfully: ' + JSON.stringify(res));
469 numberUsersSucceeded++;
471 // What to do if one of many fails??
472 // $log.error('BulkUserModalCtrl::updateDB failed: ', err);
473 const modelErrorRef = this.ngbModal.open(ConfirmationModalComponent);
474 modelErrorRef.componentInstance.title = 'Error';
475 modelErrorRef.componentInstance.message = 'Failed to update the user application roles. ' +
476 'Error: ' + err.status;
478 // $log.debug('BulkUserModalCtrl::updateDB: finally()');
480 promises.push(updatePromise);
483 // Run all the promises
484 Promise.all(promises).then(() => {
486 this.conformMsg = 'Processed ' + numberUsersSucceeded + ' users.';
487 const modelRef = this.ngbModal.open(ConfirmationModalComponent);
488 modelRef.componentInstance.title = 'Confirmation';
489 modelRef.componentInstance.message = this.conformMsg
490 this.isProcessing = false;
491 this.isProcessedRecords = true;
492 this.uploadFile = [];
493 this.dialogState = 2;