Added new componetnts inside page modules
[portal.git] / portal-FE-common / src / app / pages / users / bulk-user / bulk-user.component.ts
1 /*-
2  * ============LICENSE_START==========================================
3  * ONAP Portal
4  * ===================================================================
5  * Copyright (C) 2019 AT&T Intellectual Property. All rights reserved.
6  * ===================================================================
7  *
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
12  *
13  *             http://www.apache.org/licenses/LICENSE-2.0
14  *
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.
20  *
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
25  *
26  *             https://creativecommons.org/licenses/by/4.0/
27  *
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.
33  *
34  * ============LICENSE_END============================================
35  *
36  * 
37  */
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';
43
44 @Component({
45   selector: 'app-bulk-user',
46   templateUrl: './bulk-user.component.html',
47   styleUrls: ['./bulk-user.component.scss']
48 })
49 export class BulkUserComponent implements OnInit {
50
51   @Input() title: string;
52   @Input() adminsAppsData: any;
53   @Output() passBackBulkUserPopup: EventEmitter<any> = new EventEmitter();
54   adminApps: any;
55   // Roles fetched from app service
56   appRolesResult: any;
57   // Users fetched from user service
58   userCheckResult: any;
59   // Requests for user-role assignment built by validator
60   appUserRolesRequest: any;
61   fileSelected: boolean;
62   isProcessing: boolean;
63   isProcessedRecords: boolean;
64   dialogState: number;
65   selectedFile: any;
66   fileModel: any;
67   selectApp: boolean;
68   fileToRead: any;
69   selectedAppValue: any;
70   progressMsg: string;
71   conformMsg: string;
72   uploadFile: any;
73   uploadCheck: boolean;
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) { }
77
78   ngOnInit() {
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;
85     this.dialogState = 1;
86   }
87
88   changeSelectApp(val: any) {
89     if (val === 'select-application')
90       this.selectApp = true;
91     else
92       this.selectApp = false;
93     this.selectedAppValue = val;
94   }
95
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];
101       if (aProp > bProp)
102         return 1;
103       else if (aProp < bProp)
104         return -1;
105       else
106         return 0;
107     }
108   }
109
110   onFileLoad(fileLoadedEvent) {
111     const textFromFileLoaded = fileLoadedEvent.target.result;
112     let lines = textFromFileLoaded.split('\n');
113     // this.uploadFile = lines;
114     let result = [];
115     var len, i, line, o;
116
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) {
122         result.push({
123           line: i,
124           orgUserId: '',
125           role: '',
126           status: 'Blank line'
127         });
128         continue;
129       }
130       o = line.split(',');
131       if (o.length !== 2) {
132         // other lengths not valid for upload
133         result.push({
134           line: i,
135           orgUserId: line,
136           role: '',
137           status: 'Failed to find 2 comma-separated values'
138         });
139       }
140       else {
141         let entry = {
142           line: i,
143           orgUserId: o[0],
144           role: o[1]
145           // leave status undefined, this could be valid.
146         };
147         if (o[0].toLowerCase() === 'orgUserId') {
148           // not valid for upload, so set status
149           entry['status'] = 'Header';
150         }
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';
154         }
155         result.push(entry);
156       } // len 2
157     } // for
158     return result;
159   }
160
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;
171       return false;
172     }
173     else {
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;
207               },
208                 function (error) {
209                   this.isProcessing = false;
210                   this.isProcessedRecords = false;
211                 }
212               ); // then of app promises
213             },
214               function (_error) {
215                 this.isProcessing = false;
216                 this.isProcessedRecords = false;
217               }
218             ); // then of user promises
219           },
220             function (error) {
221               this.isProcessing = false;
222               this.isProcessedRecords = false;
223             }
224           );
225           this.uploadFileDataSource = new MatTableDataSource(this.uploadFile);
226           this.dialogState = 3;
227         };
228       }
229     }
230   }
231
232   /**
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.
237    */
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) {
243         return;
244       }
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()) {
249           foundRole = true;
250           break;
251         }
252       };
253       if (!foundRole) {
254         uploadRow.status = 'Invalid role';
255       };
256     }); // foreach
257   }; // verifyRoles
258
259   /**
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
264   */
265   buildUserChecks() {
266     // if (debug)
267     // $log.debug('BulkUserModalCtrl::buildUserChecks: uploadFile length is ' + $scope.uploadFile.length);
268     this.userCheckResult = [];
269     let promises = [];
270     let prevRow = null;
271     this.uploadFile.forEach((uploadRow) => {
272       if (uploadRow.status) {
273         // if (debug)
274         // $log.debug('BulkUserModalCtrl::buildUserChecks: skip row ' + uploadRow.line);
275         return;
276       };
277       // detect repeated UIDs
278       if (prevRow == null || prevRow.orgUserId.toLowerCase() !== uploadRow.orgUserId.toLowerCase()) {
279         // if (debug)
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
288             });
289           }
290           else {
291             // User not found.
292             // if (debug)
293             // $log.debug('BulkUserModalCtrl::buildUserChecks: searchUsers returned null');
294           }
295         }, function (error) {
296           // $log.error('BulkUserModalCtrl::buildUserChecks: searchUsers failed ' + JSON.stringify(error));
297         });
298         promises.push(userPromise);
299       }
300       else {
301         // if (debug)
302         // $log.debug('BulkUserModalCtrl::buildUserChecks: skip repeated orgUserId ' + uploadRow.orgUserId);                                            
303       }
304       prevRow = uploadRow;
305     }); // foreach
306     return promises;
307   }; // buildUserChecks
308
309   /**
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.
314    */
315   evalUserCheckResults = () => {
316     // if (debug)
317     // $log.debug('BulkUserModalCtrl::evalUserCheckResult: uploadFile length is ' + $scope.uploadFile.length);
318     this.uploadFile.forEach((uploadRow) => {
319       if (uploadRow.status) {
320         // if (debug)
321         // $log.debug('BulkUserModalCtrl::evalUserCheckResults: skip row ' + uploadRow.line);
322         return;
323       };
324       let foundorgUserId = false;
325       this.userCheckResult.forEach(function (userItem) {
326         if (uploadRow.orgUserId.toLowerCase() === userItem.orgUserId.toLowerCase()) {
327           // if (debug)
328           // $log.debug('BulkUserModalCtrl::evalUserCheckResults: found orgUserId ' + uploadRow.orgUserId);
329           foundorgUserId = true;
330         };
331       });
332       if (!foundorgUserId) {
333         // if (debug)
334         // $log.debug('BulkUserModalCtrl::evalUserCheckResults: NO match on orgUserId ' + uploadRow.orgUserId);
335         uploadRow.status = 'Invalid orgUserId';
336       }
337     }); // foreach
338   }; // evalUserCheckResults
339
340   /**
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
347   */
348   buildAppRoleChecks() {
349     this.appUserRolesRequest = [];
350     let appId = this.selectedAppValue.id;
351     let promises = [];
352     let prevRow = null;
353     this.uploadFile.forEach((uploadRow) => {
354       if (uploadRow.status) {
355         return;
356       }
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
365             });
366           } else {
367             //  $log.error('BulkUserModalCtrl::buildAppRoleChecks: getUserAppRoles returned ' + JSON.stringify(userAppRolesResult));
368           };
369         }, function (error) {
370           //  $log.error('BulkUserModalCtrl::buildAppRoleChecks: getUserAppRoles failed ', error);
371         });
372         promises.push(appPromise);
373       } else {
374         //  if (debug)
375         //  $log.debug('BulkUserModalCtrl::buildAppRoleChecks: duplicate orgUserId, skip: '+ uploadRow.orgUserId);
376       }
377       prevRow = uploadRow;
378     }); // foreach
379     return promises;
380   }; // buildAppRoleChecks
381
382   /**
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.
388    */
389   evalAppRoleCheckResults() {
390     this.uploadFile.forEach((uploadRow) => {
391       if (uploadRow.status) {
392         return;
393       }
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) {
399             //if (debug)
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';
405               }
406               else {
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;
411               }
412               // This count is not especially interesting.
413               // numberUserRolesSucceeded++;
414             }
415           }); // for each role
416         }
417       }); // for each result            
418     }); // for each row
419   }; // evalAppRoleCheckResults
420
421   // Sets the variable that hides/reveals the user controls
422   uploadFileDialog() {
423     this.fileSelected = false;
424     this.selectedFile = null;
425     this.fileModel = null;
426     this.dialogState = 2;
427   }
428
429   // Navigate between dialog screens using number: 1,2,3
430   navigateBack() {
431     this.selectApp = true;
432     this.dialogState = 1;
433     this.fileSelected = false;
434   };
435
436   // Navigate between dialog screens using number: 1,2,3
437   navigateDialog2() {
438     this.dialogState = 2;
439   };
440
441   /**
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.
446   */
447   updateDB() {
448     this.isProcessing = true;
449     this.conformMsg = '';
450     this.isProcessedRecords = true;
451     this.progressMsg = 'Sending requests to application..';
452     // if (debug)
453     // $log.debug('BulkUserModalCtrl::updateDB: request length is ' + appUserRolesRequest.length);
454     var numberUsersSucceeded = 0;
455     let promises = [];
456     this.appUserRolesRequest.forEach(appUserRoleObj => {
457       // if (debug) 
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
463       };
464       //  if (debug)
465       //  $log.debug('BulkUserModalCtrl::updateDB: updateRequest is ' + JSON.stringify(updateRequest));
466       let updatePromise = this.usersService.updateUserAppRoles(updateRequest).toPromise().then(res => {
467         //  if (debug)
468         //  $log.debug('BulkUserModalCtrl::updateDB: updated successfully: ' + JSON.stringify(res));
469         numberUsersSucceeded++;
470       }).catch(err => {
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;
477       }).finally(() => {
478         // $log.debug('BulkUserModalCtrl::updateDB: finally()');
479       });
480       promises.push(updatePromise);
481     }); // for each
482
483     // Run all the promises
484     Promise.all(promises).then(() => {
485
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;
494     });
495   }; // updateDb
496
497 }