2 * ================================================================================
\r
4 * ================================================================================
\r
5 * Copyright (C) 2017 AT&T Intellectual Property
\r
6 * ================================================================================
\r
7 * Licensed under the Apache License, Version 2.0 (the "License");
\r
8 * you may not use this file except in compliance with the License.
\r
9 * You may obtain a copy of the License at
\r
11 * http://www.apache.org/licenses/LICENSE-2.0
\r
13 * Unless required by applicable law or agreed to in writing, software
\r
14 * distributed under the License is distributed on an "AS IS" BASIS,
\r
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
\r
16 * See the License for the specific language governing permissions and
\r
17 * limitations under the License.
\r
18 * ================================================================================
\r
21 * bulk user upload controller
\r
25 class BulkUserModalCtrl {
\r
26 constructor($scope, $log, $filter, $q, usersService, applicationsService, confirmBoxService, functionalMenuService, ngDialog) {
\r
28 // Set to true for copious console output
\r
30 // Roles fetched from app service
\r
31 var appRolesResult = [];
\r
32 // Users fetched from user service
\r
33 var userCheckResult = [];
\r
34 // Requests for user-role assignment built by validator
\r
35 var appUserRolesRequest = [];
\r
39 $log.debug('BulkUserModalCtrl::init');
\r
40 // Angular insists on this.
\r
41 $scope.fileModel = {};
\r
42 // Model for drop-down
\r
43 $scope.adminApps = [];
\r
44 // Enable modal controls
\r
46 this.fileSelected = false;
\r
48 // Flag that indicates background work is proceeding
\r
49 $scope.isProcessing = true;
\r
51 // Load user's admin applications
\r
52 applicationsService.getAdminApps().promise().then(apps => {
\r
54 $log.debug('BulkUserModalCtrl::init: getAdminApps returned' + JSON.stringify(apps));
\r
55 if (!apps || typeof(apps) != 'object') {
\r
56 $log.error('BulkUserModalCtrl::init: getAdminApps returned unexpected data');
\r
60 $log.debug('BulkUserModalCtrl::init: admin apps length is ', apps.length);
\r
62 // Sort app names and populate the drop-down model
\r
63 let sortedApps = apps.sort(getSortOrder('name', true));
\r
64 for (let i = 0; i < sortedApps.length; ++i) {
\r
65 $scope.adminApps.push({
\r
67 id: sortedApps[i].id,
\r
68 value: sortedApps[i].name,
\r
69 title: sortedApps[i].name
\r
72 // Pick the first one in the list
\r
73 $scope.selectedApplication = $scope.adminApps[0];
\r
75 $scope.isProcessing = false;
\r
77 $log.error('BulkUserModalCtrl::init: getAdminApps threw', err);
\r
78 $scope.isProcessing = false;
\r
83 // Answers a function that compares properties with the specified name.
\r
84 let getSortOrder = (prop, foldCase) => {
\r
85 return function(a, b) {
\r
86 let aProp = foldCase ? a[prop].toLowerCase() : a[prop];
\r
87 let bProp = foldCase ? b[prop].toLowerCase() : b[prop];
\r
90 else if (aProp < bProp)
\r
97 //This is a fix for dropdown selection, due to b2b dropdown only update value field
\r
98 $scope.$watch('selectedApplication.value', (newVal, oldVal) => {
\r
99 for(var i=0;i<$scope.adminApps.length;i++){
\r
100 if($scope.adminApps[i].value==newVal){
\r
101 $scope.selectedApplication=angular.copy($scope.adminApps[i]);;
\r
106 // Invoked when user picks an app on the drop-down.
\r
107 $scope.appSelected = () => {
\r
109 $log.debug('BulkUserModalCtrl::appSelected: selectedApplication.id is ' + $scope.selectedApplication.id);
\r
110 this.appSelected = true;
\r
113 // Caches the file name supplied by the event handler.
\r
114 $scope.fileChangeHandler = (event, files) => {
\r
115 this.fileSelected = true;
\r
116 this.fileToRead = files[0];
\r
118 $log.debug("BulkUserModalCtrl::fileChangeHandler: file is ", this.fileToRead);
\r
119 }; // file change handler
\r
122 * Reads the contents of the file, calls portal endpoints
\r
123 * to validate roles, userIds and existing role assignments;
\r
124 * ultimately builds array of requests to be sent.
\r
125 * Creates scope variable with input file contents for
\r
126 * communication with functions.
\r
128 * This function performs a synchronous step-by-step process
\r
129 * using asynchronous promises. The code could all be inline
\r
130 * here but the nesting becomes unwieldy.
\r
132 $scope.readValidateFile = () => {
\r
133 $scope.isProcessing = true;
\r
134 $scope.progressMsg = 'Reading upload file..';
\r
135 var reader = new FileReader();
\r
136 reader.onload = function(event) {
\r
137 $scope.uploadFile = $filter('csvToObj')(reader.result);
\r
139 $log.debug('BulkUserModalCtrl::readValidateFile onload: data length is ' + $scope.uploadFile.length);
\r
140 // sort input by orgUserId
\r
141 $scope.uploadFile.sort(getSortOrder('orgUserId', true));
\r
143 let appid = $scope.selectedApplication.id;
\r
144 $scope.progressMsg = 'Fetching application roles..';
\r
145 functionalMenuService.getManagedRolesMenu(appid).then(function (rolesObj) {
\r
147 $log.debug("BulkUserModalCtrl::readValidateFile: managedRolesMenu returned " + JSON.stringify(rolesObj));
\r
148 appRolesResult = rolesObj;
\r
149 $scope.progressMsg = 'Validating application roles..';
\r
150 $scope.verifyRoles();
\r
152 let userPromises = $scope.buildUserChecks();
\r
154 $log.debug('BulkUserModalCtrl::readValidateFile: userPromises length is ' + userPromises.length);
\r
155 $scope.progressMsg = 'Validating Org Users..';
\r
156 $q.all(userPromises).then(function() {
\r
158 $log.debug('BulkUserModalCtrl::readValidateFile: userCheckResult length is ' + userCheckResult.length);
\r
159 $scope.evalUserCheckResults();
\r
161 let appPromises = $scope.buildAppRoleChecks();
\r
163 $log.debug('BulkUserModalCtrl::readValidateFile: appPromises length is ' + appPromises.length);
\r
164 $scope.progressMsg = 'Querying application for user roles..';
\r
165 $q.all(appPromises).then( function() {
\r
167 $log.debug('BulkUserModalCtrl::readValidateFile: appUserRolesRequest length is ' + appUserRolesRequest.length);
\r
168 $scope.evalAppRoleCheckResults();
\r
170 // Re sort by line for the confirmation dialog
\r
171 $scope.uploadFile.sort(getSortOrder('line', false));
\r
172 // We're done, confirm box may show the table
\r
174 $log.debug('BulkUserModalCtrl::readValidateFile inner-then ends');
\r
175 $scope.progressMsg = 'Done.';
\r
176 $scope.isProcessing = false;
\r
179 $log.error('BulkUserModalCtrl::readValidateFile: failed retrieving user-app roles');
\r
180 $scope.isProcessing = false;
\r
182 ); // then of app promises
\r
185 $log.error('BulkUserModalCtrl::readValidateFile: failed retrieving user info');
\r
186 $scope.isProcessing = false;
\r
188 ); // then of user promises
\r
191 $log.error('BulkUserModalCtrl::readValidateFile: failed retrieving app role info');
\r
192 $scope.isProcessing = false;
\r
194 ); // then of role promise
\r
198 // Invoke the reader on the selected file
\r
199 reader.readAsText(this.fileToRead);
\r
203 * Evaluates the result set returned by the app role service.
\r
204 * Sets an uploadFile array element status if a role is not defined.
\r
205 * Reads and writes scope variable uploadFile.
\r
206 * Reads closure variable appRolesResult.
\r
208 $scope.verifyRoles = () => {
\r
210 $log.debug('BulkUserModalCtrl::verifyRoles: appRoles is ' + JSON.stringify(appRolesResult));
\r
211 // check roles in upload file against defined app roles
\r
212 $scope.uploadFile.forEach( function (uploadRow) {
\r
213 // skip rows that already have a defined status: headers etc.
\r
214 if (uploadRow.status) {
\r
216 $log.debug('BulkUserModalCtrl::verifyRoles: skip row ' + uploadRow.line);
\r
219 uploadRow.role = uploadRow.role.trim();
\r
220 var foundRole=false;
\r
221 for (var i=0; i < appRolesResult.length; i++) {
\r
222 if (uploadRow.role.toUpperCase() === appRolesResult[i].rolename.trim().toUpperCase()) {
\r
224 $log.debug('BulkUserModalCtrl::verifyRoles: match on role ' + uploadRow.role);
\r
231 $log.debug('BulkUserModalCtrl::verifyRoles: NO match on role ' + uploadRow.role);
\r
232 uploadRow.status = 'Invalid role';
\r
238 * Builds and returns an array of promises to invoke the
\r
239 * searchUsers service for each unique Org User UID in the input.
\r
240 * Reads and writes scope variable uploadFile, which must be sorted by Org User UID.
\r
241 * The promise function writes to closure variable userCheckResult
\r
243 $scope.buildUserChecks = () => {
\r
245 $log.debug('BulkUserModalCtrl::buildUserChecks: uploadFile length is ' + $scope.uploadFile.length);
\r
246 userCheckResult = [];
\r
248 let prevRow = null;
\r
249 $scope.uploadFile.forEach(function (uploadRow) {
\r
250 if (uploadRow.status) {
\r
252 $log.debug('BulkUserModalCtrl::buildUserChecks: skip row ' + uploadRow.line);
\r
255 // detect repeated UIDs
\r
256 if (prevRow == null || prevRow.orgUserId.toLowerCase() !== uploadRow.orgUserId.toLowerCase()) {
\r
258 $log.debug('BulkUserModalCtrl::buildUserChecks: create request for orgUserId ' + uploadRow.orgUserId);
\r
259 let userPromise = usersService.searchUsers(uploadRow.orgUserId).promise().then( (usersList) => {
\r
260 if (typeof usersList[0] !== "undefined") {
\r
261 userCheckResult.push({
\r
262 orgUserId: usersList[0].orgUserId,
\r
263 firstName: usersList[0].firstName,
\r
264 lastName: usersList[0].lastName,
\r
265 jobTitle: usersList[0].jobTitle
\r
271 $log.debug('BulkUserModalCtrl::buildUserChecks: searchUsers returned null');
\r
273 }, function(error){
\r
274 $log.error('BulkUserModalCtrl::buildUserChecks: searchUsers failed ' + JSON.stringify(error));
\r
276 promises.push(userPromise);
\r
280 $log.debug('BulkUserModalCtrl::buildUserChecks: skip repeated orgUserId ' + uploadRow.orgUserId);
\r
282 prevRow = uploadRow;
\r
285 }; // buildUserChecks
\r
288 * Evaluates the result set returned by the user service to set
\r
289 * the uploadFile array element status if the user was not found.
\r
290 * Reads and writes scope variable uploadFile.
\r
291 * Reads closure variable userCheckResult.
\r
293 $scope.evalUserCheckResults = () => {
\r
295 $log.debug('BulkUserModalCtrl::evalUserCheckResult: uploadFile length is ' + $scope.uploadFile.length);
\r
296 $scope.uploadFile.forEach(function (uploadRow) {
\r
297 if (uploadRow.status) {
\r
299 $log.debug('BulkUserModalCtrl::evalUserCheckResults: skip row ' + uploadRow.line);
\r
302 let foundorgUserId = false;
\r
303 userCheckResult.forEach(function(userItem) {
\r
304 if (uploadRow.orgUserId.toLowerCase() === userItem.orgUserId.toLowerCase()) {
\r
306 $log.debug('BulkUserModalCtrl::evalUserCheckResults: found orgUserId ' + uploadRow.orgUserId);
\r
307 foundorgUserId=true;
\r
310 if (!foundorgUserId) {
\r
312 $log.debug('BulkUserModalCtrl::evalUserCheckResults: NO match on orgUserId ' + uploadRow.orgUserId);
\r
313 uploadRow.status = 'Invalid orgUserId';
\r
316 }; // evalUserCheckResults
\r
319 * Builds and returns an array of promises to invoke the getUserAppRoles
\r
320 * service for each unique Org User in the input file.
\r
321 * Each promise creates an update to be sent to the remote application
\r
322 * with all role names.
\r
323 * Reads scope variable uploadFile, which must be sorted by Org User.
\r
324 * The promise function writes to closure variable appUserRolesRequest
\r
326 $scope.buildAppRoleChecks = () => {
\r
328 $log.debug('BulkUserModalCtrl::buildAppRoleChecks: uploadFile length is ' + $scope.uploadFile.length);
\r
329 appUserRolesRequest = [];
\r
330 let appId = $scope.selectedApplication.id;
\r
332 let prevRow = null;
\r
333 $scope.uploadFile.forEach( function (uploadRow) {
\r
334 if (uploadRow.status) {
\r
336 $log.debug('BulkUserModalCtrl::buildAppRoleChecks: skip row ' + uploadRow.line);
\r
339 // Because the input is sorted, generate only one request for each Org User
\r
340 if (prevRow == null || prevRow.orgUserId.toLowerCase() !== uploadRow.orgUserId.toLowerCase()) {
\r
342 $log.debug('BulkUserModalCtrl::buildAppRoleChecks: create request for orgUserId ' + uploadRow.orgUserId);
\r
343 let appPromise = usersService.getUserAppRoles(appId, uploadRow.orgUserId).promise().then( (userAppRolesResult) => {
\r
344 // Reply for unknown user has all defined roles with isApplied=false on each.
\r
345 if (typeof userAppRolesResult[0] !== "undefined") {
\r
347 $log.debug('BulkUserModalCtrl::buildAppRoleChecks: adding result '
\r
348 + JSON.stringify(userAppRolesResult));
\r
349 appUserRolesRequest.push({
\r
350 orgUserId: uploadRow.orgUserId,
\r
351 userAppRoles: userAppRolesResult
\r
354 $log.error('BulkUserModalCtrl::buildAppRoleChecks: getUserAppRoles returned ' + JSON.stringify(userAppRolesResult));
\r
356 }, function(error){
\r
357 $log.error('BulkUserModalCtrl::buildAppRoleChecks: getUserAppRoles failed ', error);
\r
359 promises.push(appPromise);
\r
362 $log.debug('BulkUserModalCtrl::buildAppRoleChecks: duplicate orgUserId, skip: '+ uploadRow.orgUserId);
\r
364 prevRow = uploadRow;
\r
367 }; // buildAppRoleChecks
\r
370 * Evaluates the result set returned by the app service and adjusts
\r
371 * the list of updates to be sent to the remote application by setting
\r
372 * isApplied=true for each role name found in the upload file.
\r
373 * Reads and writes scope variable uploadFile.
\r
374 * Reads closure variable appUserRolesRequest.
\r
376 $scope.evalAppRoleCheckResults = () => {
\r
378 $log.debug('BulkUserModalCtrl::evalAppRoleCheckResults: uploadFile length is ' + $scope.uploadFile.length);
\r
379 $scope.uploadFile.forEach(function (uploadRow) {
\r
380 if (uploadRow.status) {
\r
382 $log.debug('BulkUserModalCtrl::evalAppRoleCheckResults: skip row ' + uploadRow.line);
\r
385 // Search for the match in the app-user-roles array
\r
386 appUserRolesRequest.forEach( function (appUserRoleObj) {
\r
387 if (uploadRow.orgUserId.toLowerCase() === appUserRoleObj.orgUserId.toLowerCase()) {
\r
389 $log.debug('BulkUserModalCtrl::evalAppRoleCheckResults: match on orgUserId ' + uploadRow.orgUserId);
\r
390 let roles = appUserRoleObj.userAppRoles;
\r
391 roles.forEach(function (appRoleItem) {
\r
393 // $log.debug('BulkUserModalCtrl::evalAppRoleCheckResults: checking uploadRow.role='
\r
394 // + uploadRow.role + ', appRoleItem.roleName= ' + appRoleItem.roleName);
\r
395 if (uploadRow.role === appRoleItem.roleName) {
\r
396 if (appRoleItem.isApplied) {
\r
398 $log.debug('BulkUserModalCtrl::evalAppRoleCheckResults: existing role '
\r
399 + appRoleItem.roleName);
\r
400 uploadRow.status = 'Role exists';
\r
404 $log.debug('BulkUserModalCtrl::evalAppRoleCheckResults: new role '
\r
405 + appRoleItem.roleName);
\r
406 // After much back-and-forth I decided a clear indicator
\r
407 // is better than blank in the table status column.
\r
408 uploadRow.status = 'OK';
\r
409 appRoleItem.isApplied = true;
\r
411 // This count is not especially interesting.
\r
412 // numberUserRolesSucceeded++;
\r
414 }); // for each role
\r
416 }); // for each result
\r
417 }); // for each row
\r
418 }; // evalAppRoleCheckResults
\r
421 * Sends requests to Portal requesting user role assignment.
\r
422 * That endpoint handles creation of the user at the remote app if necessary.
\r
423 * Reads closure variable appUserRolesRequest.
\r
424 * Invoked by the Next button on the confirmation dialog.
\r
426 $scope.updateDB = () => {
\r
427 $scope.isProcessing = true;
\r
428 $scope.progressMsg = 'Sending requests to application..';
\r
430 $log.debug('BulkUserModalCtrl::updateDB: request length is ' + appUserRolesRequest.length);
\r
431 var numberUsersSucceeded = 0;
\r
433 appUserRolesRequest.forEach(function(appUserRoleObj) {
\r
435 $log.debug('BulkUserModalCtrl::updateDB: appUserRoleObj is ' + JSON.stringify(appUserRoleObj));
\r
436 let updateRequest = {
\r
437 orgUserId: appUserRoleObj.orgUserId,
\r
438 appId: $scope.selectedApplication.id,
\r
439 appRoles: appUserRoleObj.userAppRoles
\r
442 $log.debug('BulkUserModalCtrl::updateDB: updateRequest is ' + JSON.stringify(updateRequest));
\r
443 let updatePromise = usersService.updateUserAppRoles(updateRequest).promise().then(res => {
\r
445 $log.debug('BulkUserModalCtrl::updateDB: updated successfully: ' + JSON.stringify(res));
\r
446 numberUsersSucceeded++;
\r
448 // What to do if one of many fails??
\r
449 $log.error('BulkUserModalCtrl::updateDB failed: ', err);
\r
450 confirmBoxService.showInformation(
\r
451 'Failed to update the user application roles. ' +
\r
452 'Error: ' + err.status).then(isConfirmed => { });
\r
453 }).finally( () => {
\r
454 // $log.debug('BulkUserModalCtrl::updateDB: finally()');
\r
456 promises.push(updatePromise);
\r
459 // Run all the promises
\r
460 $q.all(promises).then(function(){
\r
461 $scope.isProcessing = false;
\r
462 confirmBoxService.showInformation('Processed ' + numberUsersSucceeded + ' users.').then(isConfirmed => {
\r
463 // Close the upload-confirm dialog
\r
469 // Sets the variable that hides/reveals the user controls
\r
470 $scope.step2 = () => {
\r
471 this.fileSelected = false;
\r
472 $scope.selectedFile = null;
\r
473 $scope.fileModel = null;
\r
474 this.step1 = false;
\r
477 // Navigate between dialog screens using step number: 1,2,...
\r
478 $scope.navigateBack = () => {
\r
480 this.fileSelected = false;
\r
483 // Opens a dialog to show the data to be uploaded.
\r
484 // Invoked by the upload button on the bulk user dialog.
\r
485 $scope.confirmUpload = () => {
\r
486 // Start the process
\r
487 $scope.readValidateFile();
\r
488 // Dialog shows progress
\r
490 templateUrl: 'app/views/users/new-user-dialogs/bulk-user.confirm.html',
\r
495 // Invoked by the Cancel button on the confirmation dialog.
\r
496 $scope.cancelUpload = () => {
\r
503 BulkUserModalCtrl.$inject = ['$scope', '$log', '$filter', '$q', 'usersService', 'applicationsService', 'confirmBoxService', 'functionalMenuService', 'ngDialog'];
\r
504 angular.module('ecompApp').controller('BulkUserModalCtrl', BulkUserModalCtrl);
\r
506 angular.module('ecompApp').directive('fileChange', ['$parse', function($parse){
\r
508 require: 'ngModel',
\r
510 link : function($scope, element, attrs, ngModel) {
\r
511 var attrHandler = $parse(attrs['fileChange']);
\r
512 var handler=function(e) {
\r
513 $scope.$apply(function() {
\r
514 attrHandler($scope, { $event:e, files:e.target.files } );
\r
515 $scope.selectedFile = e.target.files[0].name;
\r
518 element[0].addEventListener('change',handler,false);
\r
523 angular.module('ecompApp').filter('csvToObj',function() {
\r
524 return function(input) {
\r
526 var len, i, line, o;
\r
527 var lines = input.split('\n');
\r
528 // Need 1-based index below
\r
529 for (len = lines.length, i = 1; i <= len; ++i) {
\r
530 // Use 0-based index for array
\r
531 line = lines[i - 1].trim();
\r
532 if (line.length == 0) {
\r
533 // console.log("Skipping blank line");
\r
538 status: 'Blank line'
\r
542 o = line.split(',');
\r
543 if (o.length !== 2) {
\r
544 // other lengths not valid for upload
\r
549 status: 'Failed to find 2 comma-separated values'
\r
553 // console.log("Valid line: ", val);
\r
558 // leave status undefined, this could be valid.
\r
560 if (o[0].toLowerCase() === 'orgUserId') {
\r
561 // not valid for upload, so set status
\r
562 entry.status = 'Header';
\r
564 else if (o[0].trim() == '' || o[1].trim() == '') {
\r
565 // defend against line with only a single comma etc.
\r
566 entry.status = 'Failed to find 2 non-empty values';
\r
568 result.push(entry);
\r