Update local user table to new design

This commit will introduce a reusable data table component.
By creating a reusable component, we can ensure tables in the
GUI will look consistent and common table actions (sort, select row)
are shared.

- Created new components directory to store shared components
- Add password-confirmation directive
- Remove some error handling from API utils so it can be
  handled in the UI

TODO:
- Add show/hide toggle to password fields
- Enhance table component with icons
- Manual user unlock
- Batch table actions
- Role table

Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: I03c95874d2942a2450a5da2f1d2a8bb895aa1746
diff --git a/app/users/controllers/user-accounts-controller.js b/app/users/controllers/user-accounts-controller.js
index 12ec170..11ba13d 100644
--- a/app/users/controllers/user-accounts-controller.js
+++ b/app/users/controllers/user-accounts-controller.js
@@ -10,211 +10,362 @@
   'use strict';
 
   angular.module('app.users').controller('userAccountsController', [
-    '$scope', '$q', 'APIUtils', 'toastService',
-    function($scope, $q, APIUtils, toastService) {
-      $scope.users = [];
-      $scope.roles = [];
-      $scope.loading = true;
-      $scope.properties = {};
-      $scope.origProp = {};
-      $scope.submitted = false;
+    '$scope', 'APIUtils', 'toastService', '$uibModal',
+    function($scope, APIUtils, toastService, $uibModal) {
+      $scope.loading;
+      $scope.accountSettings;
+      $scope.userRoles;
+      $scope.localUsers;
 
-      function loadUserInfo() {
+      $scope.tableModel = {};
+      $scope.tableModel.data = [];
+      $scope.tableModel.header = ['Username', 'Privilege', 'Account status']
+      $scope.tableModel.actions = ['Edit', 'Delete'];
+
+      /**
+       * Data table mapper
+       * @param {*} user
+       */
+      function mapTableData(user) {
+        let accountStatus =
+            user.Locked ? 'Locked' : user.Enabled ? 'Enabled' : 'Disabled';
+        user.uiData = [user.UserName, user.RoleId, accountStatus];
+        return user;
+      }
+
+      /**
+       * API call to get all user accounts
+       */
+      function getLocalUsers() {
         $scope.loading = true;
-        $scope.submitted = false;
-        $scope.isUserSelected = false;
-        $scope.selectedUser = {};
-        $scope.togglePassword = false;
-        $scope.toggleVerify = false;
+        APIUtils.getAllUserAccounts()
+            .then((users) => {
+              $scope.localUsers = users;
+              $scope.tableModel.data = users.map(mapTableData);
+            })
+            .catch((error) => {
+              console.log(JSON.stringify(error));
+              toastService.error('Failed to load users.');
+            })
+            .finally(() => {
+              $scope.loading = false;
+            })
+      }
 
-        $q.all([
-            APIUtils.getAllUserAccounts().then(
-                function(res) {
-                  $scope.users = res;
-                },
-                function(error) {
-                  console.log(JSON.stringify(error));
-                }),
+      /**
+       * API call to get current Account settings
+       */
+      function getAccountSettings() {
+        APIUtils.getAllUserAccountProperties()
+            .then((settings) => {
+              $scope.accountSettings = settings;
+            })
+            .catch((error) => {
+              console.log(JSON.stringify(error));
+              $scope.accountSettings = null;
+            })
+      }
 
-            APIUtils.getAllUserAccountProperties().then(
-                function(res) {
-                  $scope.properties = res;
-                  $scope.origProp = angular.copy($scope.properties);
-                },
-                function(error) {
-                  console.log(JSON.stringify(error));
-                }),
+      /**
+       * API call to get local user roles
+       */
+      function getUserRoles() {
+        APIUtils.getAccountServiceRoles()
+            .then((roles) => {
+              $scope.userRoles = roles;
+            })
+            .catch((error) => {
+              console.log(JSON.stringify(error));
+              $scope.userRoles = null;
+            })
+      }
 
-            APIUtils.getAccountServiceRoles().then(
-                function(res) {
-                  $scope.roles = res;
-                },
-                function(error) {
-                  console.log(JSON.stringify(error));
-                })
-          ]).finally(function() {
-          $scope.loading = false;
-        });
-      };
-
-      $scope.cancel = function() {
-        loadUserInfo();
-      };
-
-      $scope.saveAllValues = function() {
+      /**
+       * API call to create new user
+       * @param {*} user
+       */
+      function createUser(username, password, role, enabled) {
         $scope.loading = true;
-        var data = {};
-        if ($scope.properties.AccountLockoutDuration !=
-            $scope.origProp.AccountLockoutDuration) {
-          data['AccountLockoutDuration'] =
-              $scope.properties.AccountLockoutDuration;
-        }
-        if ($scope.properties.AccountLockoutThreshold !=
-            $scope.origProp.AccountLockoutThreshold) {
-          data['AccountLockoutThreshold'] =
-              $scope.properties.AccountLockoutThreshold;
-        }
-
-        if ($scope.properties.AccountLockoutDuration ==
-                $scope.origProp.AccountLockoutDuration &&
-            $scope.properties.AccountLockoutThreshold ==
-                $scope.origProp.AccountLockoutThreshold) {
-          // No change in properties, just return;
-          $scope.loading = false;
-          return;
-        }
-
-        APIUtils
-            .saveUserAccountProperties(
-                data['AccountLockoutDuration'], data['AccountLockoutThreshold'])
-            .then(
-                function(response) {
-                  toastService.success(
-                      'User account properties have been updated successfully');
-                },
-                function(error) {
-                  toastService.error('Unable to update account properties');
-                })
-            .finally(function() {
-              loadUserInfo();
+        APIUtils.createUser(username, password, role, enabled)
+            .then(() => {
+              getLocalUsers();
+              toastService.success(`User '${username}' has been created.`);
+            })
+            .catch((error) => {
+              console.log(JSON.stringify(error));
+              toastService.error(`Failed to create new user '${username}'.`);
+            })
+            .finally(() => {
               $scope.loading = false;
             });
-      };
+      }
 
-      $scope.setSelectedUser = function(user) {
-        $scope.isUserSelected = true;
-        $scope.selectedUser = angular.copy(user);
-        $scope.selectedUser.VerifyPassword = null;
-        // Used while renaming the user.
-        $scope.selectedUser.CurrentUserName = $scope.selectedUser.UserName;
-      };
-      $scope.createNewUser = function() {
-        if ($scope.users.length >= 15) {
-          toastService.error(
-              'Cannot create user. The maximum number of users that can be created is 15');
-          return;
-        }
-        if (!$scope.selectedUser.UserName || !$scope.selectedUser.Password) {
-          toastService.error('Username or password cannot be empty');
-          return;
-        }
-        if ($scope.selectedUser.Password !==
-            $scope.selectedUser.VerifyPassword) {
-          toastService.error('Passwords do not match');
-          return;
-        }
-        if ($scope.doesUserExist()) {
-          toastService.error('Username already exists');
-          return;
-        }
-        var user = $scope.selectedUser.UserName;
-        var passwd = $scope.selectedUser.Password;
-        var role = $scope.selectedUser.RoleId;
-        var enabled = false;
-        if ($scope.selectedUser.Enabled != null) {
-          enabled = $scope.selectedUser.Enabled;
-        }
-
+      /**
+       * API call to update existing user
+       */
+      function updateUser(originalUsername, username, password, role, enabled) {
         $scope.loading = true;
-        APIUtils.createUser(user, passwd, role, enabled)
-            .then(
-                function(response) {
-                  toastService.success('User has been created successfully');
-                },
-                function(error) {
-                  toastService.error('Failed to create new user');
-                })
-            .finally(function() {
-              loadUserInfo();
+        APIUtils.updateUser(originalUsername, username, password, role, enabled)
+            .then(() => {
+              getLocalUsers();
+              toastService.success('User has been updated successfully.')
+            })
+            .catch((error) => {
+              console.log(JSON.stringify(error));
+              toastService.error(`Unable to update user '${originalUsername}'.`)
+            })
+            .finally(() => {
+              $scope.loading = false;
+            })
+      }
+
+      /**
+       * API call to delete user
+       * @param {*} username
+       */
+      function deleteUser(username) {
+        $scope.loading = true;
+        APIUtils.deleteUser(username)
+            .then(() => {
+              getLocalUsers();
+              toastService.success(`User '${username}' has been deleted.`);
+            })
+            .catch((error) => {
+              console.log(JSON.stringify(error));
+              toastService.error(`Failed to delete user '${username}'.`);
+            })
+            .finally(() => {
               $scope.loading = false;
             });
-      };
-      $scope.updateUserInfo = function() {
-        if ($scope.selectedUser.Password !==
-            $scope.selectedUser.VerifyPassword) {
-          toastService.error('Passwords do not match');
-          return;
-        }
-        if ($scope.doesUserExist()) {
-          toastService.error('Username already exists');
-          return;
-        }
-        var data = {};
-        if ($scope.selectedUser.UserName !==
-            $scope.selectedUser.CurrentUserName) {
-          data['UserName'] = $scope.selectedUser.UserName;
-        }
-        $scope.selectedUser.VerifyPassword = null;
-        if ($scope.selectedUser.Password != null) {
-          data['Password'] = $scope.selectedUser.Password;
-        }
-        data['RoleId'] = $scope.selectedUser.RoleId;
-        data['Enabled'] = $scope.selectedUser.Enabled;
+      }
 
+      /**
+       * API call to save account policy settings
+       * @param {number} lockoutDuration
+       * @param {number} lockoutThreshold
+       */
+      function updateAccountSettings(lockoutDuration, lockoutThreshold) {
         $scope.loading = true;
-        APIUtils
-            .updateUser(
-                $scope.selectedUser.CurrentUserName, data['UserName'],
-                data['Password'], data['RoleId'], data['Enabled'])
-            .then(
-                function(response) {
-                  toastService.success('User has been updated successfully');
-                },
-                function(error) {
-                  toastService.error('Unable to update user');
-                })
-            .finally(function() {
-              loadUserInfo();
+        APIUtils.saveUserAccountProperties(lockoutDuration, lockoutThreshold)
+            .then(() => {
+              $scope.accountSettings['AccountLockoutDuration'] =
+                  lockoutDuration;
+              $scope.accountSettings['AccountLockoutThreshold'] =
+                  lockoutThreshold;
+              toastService.success(
+                  'Account policy settings have been updated.');
+            })
+            .catch((error) => {
+              console.log(JSON.stringify(error));
+              toastService.error('Failed to update account policy settings.');
+            })
+            .finally(() => {
               $scope.loading = false;
             });
-      };
-      $scope.deleteUser = function(userName) {
-        $scope.loading = true;
-        APIUtils.deleteUser(userName)
-            .then(
-                function(response) {
-                  toastService.success('User has been deleted successfully');
-                },
-                function(error) {
-                  toastService.error('Unable to delete user');
-                })
-            .finally(function() {
-              loadUserInfo();
-              $scope.loading = false;
-            });
-      };
+      }
 
-      $scope.doesUserExist = function() {
-        for (var i in $scope.users) {
-          // If a user exists with the same user name and a different Id then
-          // the username already exists and isn't valid
-          if (($scope.users[i].UserName === $scope.selectedUser.UserName) &&
-              ($scope.users[i].Id !== $scope.selectedUser.Id)) {
-            return true;
-          }
+      /**
+       * Initiate account settings modal
+       */
+      function initAccountSettingsModal() {
+        const template = require('./user-accounts-modal-settings.html');
+        $uibModal
+            .open({
+              template,
+              windowTopClass: 'uib-modal',
+              ariaLabelledBy: 'dialog_label',
+              controllerAs: 'modalCtrl',
+              controller: function() {
+                // If AccountLockoutDuration is not 0 the lockout
+                // method is automatic. If AccountLockoutDuration is 0 the
+                // lockout method is manual
+                const lockoutMethod =
+                    $scope.accountSettings.AccountLockoutDuration ? 1 : 0;
+                this.settings = {};
+                this.settings.maxLogin =
+                    $scope.accountSettings.AccountLockoutThreshold;
+                this.settings.lockoutMethod = lockoutMethod;
+                this.settings.timeoutDuration = !lockoutMethod ?
+                    null :
+                    $scope.accountSettings.AccountLockoutDuration;
+              }
+            })
+            .result
+            .then((form) => {
+              if (form.$valid) {
+                const lockoutDuration = form.lockoutMethod.$modelValue ?
+                    form.timeoutDuration.$modelValue :
+                    0;
+                const lockoutThreshold = form.maxLogin.$modelValue;
+                updateAccountSettings(lockoutDuration, lockoutThreshold);
+              }
+            })
+            .catch(
+                () => {
+                    // do nothing
+                })
+      }
+
+      /**
+       * Initiate user modal
+       * Can be triggered by clicking edit in table or 'Add user' button
+       * If triggered from the table, user parameter will be provided
+       * If triggered by add user button, user parameter will be undefined
+       * @optional @param {*} user
+       */
+      function initUserModal(user) {
+        if ($scope.userRoles === null || $scope.userRoles === undefined) {
+          // If userRoles failed to load,  do not allow add/edit
+          // functionality
+          return;
+        }
+        const newUser = user ? false : true;
+        const originalUsername = user ? angular.copy(user.UserName) : null;
+        const template = require('./user-accounts-modal-user.html');
+        $uibModal
+            .open({
+              template,
+              windowTopClass: 'uib-modal',
+              ariaLabelledBy: 'dialog_label',
+              controllerAs: 'modalCtrl',
+              controller: function() {
+                // Set default status to Enabled
+                const status = newUser ? true : user.Enabled;
+                // Check if UserName is root
+                // Some form controls will be disabled for root users:
+                // edit enabled status, edit username, edit role
+                const isRoot =
+                    newUser ? false : user.UserName === 'root' ? true : false;
+                // Array of existing usernames (excluding current user instance)
+                const existingUsernames =
+                    $scope.localUsers.reduce((acc, val) => {
+                      if (user && (val.UserName === user.UserName)) {
+                        return acc;
+                      }
+                      acc.push(val.UserName);
+                      return acc;
+                    }, []);
+
+                this.user = {};
+                this.user.isRoot = isRoot;
+                this.user.new = newUser;
+                this.user.accountStatus = status;
+                this.user.username = newUser ? '' : user.UserName;
+                this.user.privilege = newUser ? '' : user.RoleId;
+
+                this.privilegeRoles = $scope.userRoles;
+                this.existingUsernames = existingUsernames;
+                this.minPasswordLength = $scope.accountSettings ?
+                    $scope.accountSettings.MinPasswordLength :
+                    null;
+                this.maxPasswordLength = $scope.accountSettings ?
+                    $scope.accountSettings.MaxPasswordLength :
+                    null;
+              }
+            })
+            .result
+            .then((form) => {
+              if (form.$valid) {
+                // If form control is pristine set property to null
+                // this will make sure only changed values are updated when
+                // modifying existing users
+                // API utils checks for null values
+                const username =
+                    form.username.$pristine ? null : form.username.$modelValue;
+                const password =
+                    form.password.$pristine ? null : form.password.$modelValue;
+                const role = form.privilege.$pristine ?
+                    null :
+                    form.privilege.$modelValue;
+                const enabled = (form.accountStatus.$pristine &&
+                                 form.accountStatus1.$pristine) ?
+                    null :
+                    form.accountStatus.$modelValue;
+
+                if (!newUser) {
+                  updateUser(
+                      originalUsername, username, password, role, enabled);
+                } else {
+                  createUser(
+                      username, password, role, form.accountStatus.$modelValue);
+                }
+              }
+            })
+            .catch(
+                () => {
+                    // do nothing
+                })
+      }
+
+      /**
+       * Intiate remove user modal
+       * @param {*} user
+       */
+      function initRemoveModal(user) {
+        const template = require('./user-accounts-modal-remove.html');
+        $uibModal
+            .open({
+              template,
+              windowTopClass: 'uib-modal',
+              ariaLabelledBy: 'dialog_label',
+              controllerAs: 'modalCtrl',
+              controller: function() {
+                this.user = user.UserName;
+              }
+            })
+            .result
+            .then(() => {
+              const isRoot = user.UserName === 'root' ? true : false;
+              if (isRoot) {
+                toastService.error(`Cannot delete 'root' user.`)
+                return;
+              }
+              deleteUser(user.UserName);
+            })
+            .catch(
+                () => {
+                    // do nothing
+                })
+      }
+
+      /**
+       * Callback when action emitted from table
+       * @param {*} value
+       */
+      $scope.onEmitAction = (value) => {
+        switch (value.action) {
+          case 'Edit':
+            initUserModal(value.row);
+            break;
+          case 'Delete':
+            initRemoveModal(value.row);
+            break;
+          default:
         }
       };
-      loadUserInfo();
+
+      /**
+       * Callback when 'Account settings policy' button clicked
+       */
+      $scope.onClickAccountSettingsPolicy = () => {
+        initAccountSettingsModal();
+      };
+
+      /**
+       * Callback when 'Add user' button clicked
+       */
+      $scope.onClickAddUser = () => {
+        initUserModal();
+      };
+
+      /**
+       * Callback when controller view initially loaded
+       */
+      $scope.$on('$viewContentLoaded', () => {
+        getLocalUsers();
+        getUserRoles();
+        getAccountSettings();
+      })
     }
   ]);
 })(angular);