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.html b/app/users/controllers/user-accounts-controller.html
index 11cc85c..fd6a28c 100644
--- a/app/users/controllers/user-accounts-controller.html
+++ b/app/users/controllers/user-accounts-controller.html
@@ -1,129 +1,31 @@
 <loader loading="loading"></loader>
-<div id="user-accounts">
-
-  <div class="row column acnt-prop-header">
-    <h1>User account properties</h1>
-  </div>
-
-  <div class="col-sm-12">
-    <form class="row column user-manage__form">
-      <div class="col-sm-12">
-        <label class="col-md-1 control-label" for="lockoutTime"> User Lockout Time (sec) </label>
-        <div class="col-md-3 acnt-prop__input-wrapper">
-          <input type="number" id="lockoutTime" min="30" max="600" ng-model="properties.AccountLockoutDuration"/>
-        </div>
-      </div>
-      <div class="col-sm-12">
-        <label class="col-md-1 control-label" for="lockoutThreshold"> Failed Login Attempts </label>
-        <div class="col-md-3 acnt-prop__input-wrapper">
-          <input type="number" id="lockoutThreshold" min="3" max="10" ng-model="properties.AccountLockoutThreshold"/>
-        </div>
-      </div>
-      <div class="acnt-prop__submit-wrapper">
-          <button type="button" class="btn  btn-primary" ng-click="saveAllValues()">Save settings</button>
-      </div>
-    </form>
-  </div>
-
+<div class="local-users">
   <div class="row column">
-    <h1>User account information</h1>
-  </div>
-  <div class="table row column user-list__tbl" ng-show="users.length != 0">
-    <div class="table__head">
-      <div class="table__row">
-        <div class="table__cell"> Username </div>
-        <div class="table__cell"> Enabled </div>
-        <div class="table__cell"> Role </div>
-        <div class="table__cell"> Locked </div>
-        <div class="table__cell"> Action </div>
-      </div>
-    </div>
-    <div class="table__body">
-      <div class="table__row" ng-repeat="user in users">
-        <div class="table__cell"> {{user.UserName}} </div>
-        <div class="table__cell"> {{user.Enabled}} </div>
-        <div class="table__cell"> {{user.RoleId}} </div>
-        <div class="table__cell"> {{user.Locked}} </div>
-        <div class="table__cell">
-          <button type="button" class="btn  btn-primary" ng-disabled="isUserSelected" ng-click="setSelectedUser(user)">Edit</button>
-          <button type="button" class="btn btn-primary" ng-disabled="isUserSelected" ng-click="deleteUser(user.UserName)">Delete</button>
-        </div>
-      </div>
+    <div class="column small-12">
+      <h1 class="page-title">Local user management</h1>
     </div>
   </div>
-  <div class="table row column" ng-show="users.length == 0">
-    <span>No users exist in system</span>
+  <div class="row column">
+    <div class="column small-12">
+      <div class="local-users__actions">
+        <button ng-disabled="accountSettings === null"
+                ng-click="onClickAccountSettingsPolicy()"
+                class="btn btn-tertiary">
+          <icon file="icon-config.svg"></icon>
+          Account policy settings
+        </button>
+        <button ng-disabled="userRoles === null || localUsers.length >= 15"
+                ng-click="onClickAddUser()"
+                class="btn btn-primary">
+          <icon file="icon-plus.svg"></icon>
+          Add user
+        </button>
+      </div>
+      <bmc-table
+        model="tableModel"
+        emit-action="onEmitAction(value)"
+        class="local-users__table">
+      </bmc-table>
+    </div>
   </div>
-
-  <form role="form" name="user__form" class="user-manage__form">
-    <section class="row column" aria-label="user manage form" ng-class="{'submitted':submitted}">
-       <div class="column small-12 page-header">
-        <h2 class="inline">User account settings</h2>
-       </div>
-        <div class='col-sm-12'>
-          <label class="col-md-1 control-label" for="user-manage__username">UserName</label>
-          <div class="col-md-3">
-            <input type="text" name="UserName" id="user-manage__username" ng-model="selectedUser.UserName" has-error="doesUserExist()" required />
-            <div ng-messages="user__form.UserName.$error" class="form-error" ng-class="{'visible' : user__form.UserName.$touched || submitted}">
-              <p ng-message="required">Field is required</p>
-              <p ng-message="hasError">Username exists</p>
-            </div>
-          </div>
-        </div>
-        <div class='col-sm-12 inline'>
-          <label class="col-md-1 control-label" for="user-manage__passwd">Password</label>
-          <div class="col-md-3 user-manage__input-wrapper inline">
-            <input type="{{showpassword ? 'text' : 'password'}}" class="user-manage__new-password inline" name="Password" id="user-manage__passwd" ng-model="selectedUser.Password" ng-minlength="properties.MinPasswordLength" ng-maxlength="properties.MaxPasswordLength" required autocomplete="off"/>
-            <button ng-model="showpassword" ng-click="togglePassword = !togglePassword; showpassword = !showpassword;" class="btn btn-tertiary  password-toggle">
-              <span ng-hide="togglePassword">Show</span>
-              <span ng-show="togglePassword">Hide</span>
-            </button>
-            <div ng-messages="user__form.Password.$error" class="form-error" ng-class="{'visible' : user__form.Password.$touched || submitted}">
-              <p ng-message="required">Field is required</p>
-              <p ng-message="minlength">Must be at least {{properties.MinPasswordLength}} characters</p>
-              <p ng-message="maxlength">Must be {{properties.MaxPasswordLength}} characters or less</p>
-            </div>
-          </div>
-        </div>
-        <div class='col-sm-12'>
-          <label class="col-md-1 control-label" for="user-manage__verifypasswd">Retype Password</label>
-          <div class="col-md-3 user-manage__input-wrapper inline">
-            <input type="{{showpasswordVerify ? 'text' : 'password'}}" class="user-manage__verify-password inline" name="VerifyPassword" id="user-manage__verifypasswd" ng-model="selectedUser.VerifyPassword" has-error="selectedUser.VerifyPassword != selectedUser.Password" required autocomplete="off">
-            <button ng-model="showpasswordVerify" ng-click="toggleVerify = !toggleVerify; showpasswordVerify = !showpasswordVerify;" class="btn  btn-tertiary  password-toggle">
-              <span ng-hide="toggleVerify">Show</span>
-              <span ng-show="toggleVerify">Hide</span>
-            </button>
-            <div ng-messages="user__form.VerifyPassword.$error" class="form-error" ng-class="{'visible' : user__form.VerifyPassword.$touched || submitted}">
-              <p ng-message="required">Field is required</p>
-              <p ng-message="hasError">Passwords do not match</p>
-            </div>
-          </div>
-        </div>
-        <div class='col-sm-12'>
-          <label class="col-md-1 control-label" for="role">Role</label>
-          <div class="col-md-3 user-manage__input-wrapper inline">
-            <select ng-model="selectedUser.RoleId" id="role" name="role" class="inline" required>
-              <option ng-repeat="role in roles" class="inline">{{role}}</option>
-            </select>
-            <div ng-messages="user__form.role.$error" class="form-error" ng-class="{'visible' : user__form.role.$touched || submitted}">
-              <p ng-message="required">Field is required</p>
-            </div>
-          </div>
-        </div>
-        <div class='col-sm-12'>
-          <label class="col-md-1 control-label" for="user-manage__enabled">Enabled</label>
-          <div class="col-md-3 user-manage__input-wrapper inline">
-            <label class="control-check">
-              <input type="checkbox" name="Enabled" id="user-manage__enabled" ng-model="selectedUser.Enabled"/>
-                <span class="control__indicator"></span>
-            </label>
-          </div>
-        </div>
-        <div class="user-manage__submit-wrapper">
-            <button type="button" ng-click="submitted=true; user__form.$valid && createNewUser(); user__form.$setUntouched()" ng-show="!isUserSelected" class="btn btn-primary">Create user</button>
-            <button type="button" class="btn  btn-primary" ng-click="submitted=true; user__form.$valid && updateUserInfo(); user__form.$setUntouched()" ng-show="isUserSelected">Save</button>
-            <button type="button" class="btn  btn-primary" ng-if="isUserSelected" ng-click="cancel()">Cancel</button>
-        </div>
-    </section>
-  </form>
-</div>
+</div>
\ No newline at end of file
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);
diff --git a/app/users/controllers/user-accounts-modal-remove.html b/app/users/controllers/user-accounts-modal-remove.html
new file mode 100644
index 0000000..e615251
--- /dev/null
+++ b/app/users/controllers/user-accounts-modal-remove.html
@@ -0,0 +1,21 @@
+<div class="uib-modal__content  modal__local-users-remove">
+  <div class="modal-header">
+    <h2 class="modal-title" id="dialog_label">
+      Remove user
+    </h2>
+    <button type="button" class="btn  btn--close  float-right" ng-click="$dismiss()" aria-label="Close">
+      <icon file="icon-close.svg" aria-hidden="true"></icon>
+    </button>
+  </div>
+  <div class="modal-body">
+    <p>Are you sure you want to remove user '{{modalCtrl.user}}'? This action cannot be undone.</p>
+  </div>
+  <div class="modal-footer">
+    <button class="btn btn-secondary" ng-click="$dismiss()" type="button">
+      Cancel
+    </button>
+    <button class="btn btn-primary" ng-click="$close()" type="button">
+      Remove
+    </button>
+  </div>
+</div>
diff --git a/app/users/controllers/user-accounts-modal-settings.html b/app/users/controllers/user-accounts-modal-settings.html
new file mode 100644
index 0000000..d48809f
--- /dev/null
+++ b/app/users/controllers/user-accounts-modal-settings.html
@@ -0,0 +1,85 @@
+<div class="uib-modal__content  modal__local-users-settings">
+  <div class="modal-header">
+    <h2 class="modal-title" id="dialog_label">Account policy settings</h2>
+    <button type="button" class="btn  btn--close  float-right" ng-click="$dismiss()" aria-label="Close">
+      <icon file="icon-close.svg" aria-hidden="true"></icon>
+    </button>
+  </div>
+  <form name="form">
+    <div class="modal-body">
+      <div class="row">
+        <div class="column medium-6">
+            <!-- Max login attempts -->
+            <div class="field-group-container">
+              <label for="maxLogin">Max failed login attempts</label>
+              <p class="label__helper-text">Value must be between <span class="nowrap">0 – 65535</span></p>
+              <input id="maxLogin"
+                     name="maxLogin"
+                     type="number"
+                     required
+                     min="0"
+                     max="65535"
+                     ng-model="modalCtrl.settings.maxLogin" />
+              <div ng-if="form.maxLogin.$invalid && form.maxLogin.$dirty" class="form__validation-message">
+                <span ng-show="form.maxLogin.$error.required">
+                  Field is required</span>
+                <span ng-show="form.maxLogin.$error.min || form.maxLogin.$error.max">
+                  Value must be between <span class="nowrap">1 - 65535</span></span>
+              </div>
+            </div>
+        </div>
+        <div class="column medium-6">
+          <!-- User unlock method -->
+          <fieldset class="field-group-container">
+            <legend>User unlock method</legend>
+            <!-- Automatic radio option -->
+            <label class="radio-label">
+              <input name="lockoutMethod"
+                     type="radio"
+                     ng-value="1"
+                     ng-model="modalCtrl.settings.lockoutMethod">
+              Automatic after timeout
+            </label>
+            <!-- Automatic timeout value -->
+            <div class="field-group-container  radio-option__input-field-group">
+              <label for="lockoutMethod1">Timeout duration (seconds)</label>
+              <p class="label__helper-text" id="lockoutMethod1Helper">Must be at least 1</p>
+              <input id="lockoutMethod1"
+                     name="timeoutDuration"
+                     type="number"
+                     aria-describedby="lockoutMethod1Helper"
+                     ng-min="modalCtrl.settings.lockoutMethod ? 1 : null"
+                     ng-disabled="!modalCtrl.settings.lockoutMethod"
+                     ng-required="modalCtrl.settings.lockoutMethod"
+                     ng-model="modalCtrl.settings.timeoutDuration"/>
+              <div ng-if="form.timeoutDuration.$invalid && form.timeoutDuration.$touched" class="form__validation-message">
+                <span ng-show="form.timeoutDuration.$error.required">
+                  Field is required</span>
+                <span ng-show="form.timeoutDuration.$error.min">
+                  Value must be at least 1</span>
+              </div>
+            </div>
+            <!-- Manual radio option -->
+            <label class="radio-label">
+              <input name="lockoutMethod"
+                     type="radio"
+                     ng-value="0"
+                     ng-model="modalCtrl.settings.lockoutMethod">
+              Manual
+            </label>
+          </fieldset>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <button class="btn btn-secondary" ng-click="$dismiss()" type="button">Cancel</button>
+      <button class="btn btn-primary"
+              type="submit"
+              ng-click="$close(form)"
+              ng-disabled="form.$invalid || form.$pristine"
+              ng-class="{'disabled': form.$invalid}">
+        Save
+      </button>
+    </div>
+  </form>
+</div>
diff --git a/app/users/controllers/user-accounts-modal-user.html b/app/users/controllers/user-accounts-modal-user.html
new file mode 100644
index 0000000..7df5ea1
--- /dev/null
+++ b/app/users/controllers/user-accounts-modal-user.html
@@ -0,0 +1,149 @@
+<div class="uib-modal__content  modal__local-users">
+  <div class="modal-header">
+    <h2 class="modal-title" id="dialog_label">
+      {{ modalCtrl.user.new ? 'Add user' : 'Modify user' }}
+    </h2>
+    <button type="button" class="btn  btn--close" ng-click="$dismiss()" aria-label="Close">
+      <icon file="icon-close.svg" aria-hidden="true"></icon>
+    </button>
+  </div>
+  <form name="form">
+    <div class="modal-body">
+      <div class="row">
+        <div class="column medium-6">
+            <!-- Account Status -->
+            <fieldset class="field-group-container">
+              <legend>Account Status</legend>
+              <label class="radio-label">
+                <input type="radio"
+                       name="accountStatus"
+                       ng-value="true"
+                       ng-model="modalCtrl.user.accountStatus"
+                       ng-disabled="modalCtrl.user.isRoot">
+                Enabled
+              </label>
+              <label class="radio-label">
+                <input type="radio"
+                       name="accountStatus1"
+                       ng-value="false"
+                       ng-model="modalCtrl.user.accountStatus"
+                       ng-disabled="modalCtrl.user.isRoot">
+                Disabled
+              </label>
+            </fieldset>
+            <!-- Username -->
+            <div class="field-group-container">
+              <label for="username">Username</label>
+              <p class="label__helper-text">Cannot start with a number</p>
+              <p class="label__helper-text">No special characters except underscore</p>
+              <input id="username"
+                     name="username"
+                     type="text"
+                     required
+                     minlength="1"
+                     maxlength="16"
+                     ng-pattern="'^([a-zA-Z_][a-zA-Z0-9_]*)'"
+                     ng-readonly="modalCtrl.user.isRoot"
+                     ng-model="modalCtrl.user.username"
+                     username-validator
+                     existing-usernames="modalCtrl.existingUsernames"/>
+              <div ng-if="form.username.$invalid && form.username.$touched" class="form__validation-message">
+                <span ng-show="form.username.$error.required">
+                  Field is required</span>
+                <span ng-show="form.username.$error.minlength || form.username.$error.maxlength">
+                  Length must be between <span class="nowrap">1 – 16</span> characters</span>
+                <span ng-show="form.username.$error.pattern">
+                  Invalid format</span>
+                <span ng-show="form.username.$error.duplicateUsername">
+                  Username already exists</span>
+              </div>
+            </div>
+            <!-- Privlege -->
+            <div class="field-group-container">
+              <label for="privilege">Privilege</label>
+              <select id="privilege"
+                      name="privilege"
+                      required
+                      ng-disabled="modalCtrl.user.isRoot"
+                      ng-model="modalCtrl.user.privilege">
+                <option ng-if="modalCtrl.user.new"
+                        ng-selected="modalCtrl.user.new"
+                        value=""
+                        disabled>
+                  Select an option
+                </option>
+                <option ng-value="role"
+                        ng-repeat="role in modalCtrl.privilegeRoles">
+                  {{role}}
+                </option>
+              </select>
+            </div>
+        </div>
+        <div class="column medium-6">
+           <!-- Password -->
+           <div class="field-group-container">
+            <label for="password">User password</label>
+            <p class="label__helper-text">Password must between <span class="nowrap">{{modalCtrl.minPasswordLength}} – {{modalCtrl.maxPasswordLength}}</span> characters</p>
+            <input id="password"
+                   name="password"
+                   type="password"
+                   ng-minlength="modalCtrl.minPasswordLength"
+                   ng-maxlength="modalCtrl.maxPasswordLength"
+                   autocomplete="new-password"
+                   ng-required="modalCtrl.user.new || form.password.$touched || form.passwordConfirm.$touched"
+                   ng-model="modalCtrl.user.password"
+                   ng-click="form.password.$setTouched()"
+                   placeholder="{{
+                    (modalCtrl.user.new ||
+                    form.password.$touched ||
+                    form.passwordConfirm.$touched) ? '' : '******'}}"/>
+            <div ng-if="form.password.$invalid && form.password.$dirty" class="form__validation-message">
+              <span ng-show="form.password.$error.required">
+                Field is required</span>
+              <span ng-show="form.password.$error.minlength || form.password.$error.maxlength">
+                Length must be between <span class="nowrap">{{modalCtrl.minPasswordLength}} – {{modalCtrl.maxPasswordLength}}</span> characters</span>
+            </div>
+          </div>
+          <!-- Password confirm -->
+          <div class="field-group-container">
+            <label for="passwordConfirm">Confirm user password</label>
+            <input id="passwordConfirm"
+                   name="passwordConfirm"
+                   type="password"
+                   autocomplete="new-password"
+                   ng-required="modalCtrl.user.new || form.password.$touched || form.passwordConfirm.$touched"
+                   ng-model="modalCtrl.user.passwordConfirm"
+                   password-confirm
+                   first-password="form.password.$modelValue"
+                   ng-click="form.passwordConfirm.$setTouched()"
+                   placeholder="{{(
+                    modalCtrl.user.new ||
+                    form.password.$touched ||
+                    form.passwordConfirm.$touched) ? '' : '******'}}"/>
+            <div ng-if="form.passwordConfirm.$invalid && form.passwordConfirm.$dirty" class="form__validation-message">
+              <span ng-show="form.passwordConfirm.$error.required">
+                Field is required</span>
+              <span ng-show="form.passwordConfirm.$error.passwordConfirm"
+                    ng-hide="form.passwordConfirm.$error.required">
+                Passwords do not match</span>
+              <span ng-show="form.passwordConfirm.$error.minlength || form.passwordConfirm.$error.maxlength">
+                Length must be between <span class="nowrap">1 – 16</span> characters</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <button class="btn btn-secondary" ng-click="$dismiss()" type="button">
+        Cancel
+      </button>
+      <button class="btn btn-primary"
+              type="submit"
+              ng-click="$close(form)"
+              ng-disabled="form.$invalid || form.$pristine"
+              ng-class="{'disabled': form.$invalid}">
+        {{ modalCtrl.user.new ? 'Add user' : 'Save' }}
+      </button>
+    </div>
+  </form>
+</div>
diff --git a/app/users/directives/username-validator.js b/app/users/directives/username-validator.js
new file mode 100644
index 0000000..d8c5848
--- /dev/null
+++ b/app/users/directives/username-validator.js
@@ -0,0 +1,38 @@
+window.angular && (function(angular) {
+  'use strict';
+
+  /**
+   * Username validator
+   *
+   * Checks if entered username is a duplicate
+   * Provide existingUsernames scope that should be an array of
+   * existing usernames
+   *
+   * <input username-validator  existing-usernames="[]"/>
+   *
+   */
+  angular.module('app.users').directive('usernameValidator', function() {
+    return {
+      restrict: 'A', require: 'ngModel', scope: {existingUsernames: '='},
+          link: function(scope, element, attrs, controller) {
+            if (scope.existingUsernames === undefined) {
+              return;
+            }
+            controller.$validators.duplicateUsername =
+                (modelValue, viewValue) => {
+                  const enteredUsername = modelValue || viewValue;
+                  const matchedExisting = scope.existingUsernames.find(
+                      (username) => username === enteredUsername);
+                  if (matchedExisting) {
+                    return false;
+                  } else {
+                    return true;
+                  }
+                };
+            element.on('blur', () => {
+              controller.$validate();
+            });
+          }
+    }
+  });
+})(window.angular);
diff --git a/app/users/styles/user-accounts.scss b/app/users/styles/user-accounts.scss
index a91bca6..9658b90 100644
--- a/app/users/styles/user-accounts.scss
+++ b/app/users/styles/user-accounts.scss
@@ -1,75 +1,31 @@
-.acnt-prop-header {
-  width: 100%;
-  border-bottom: 2px solid $border-color-01;
-  margin: 0px 0px 15px;
+.local-users__actions {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
 }
-.user-manage__form {
-  width: 100%;
-  .dropdown__button {
-    margin-bottom: 1.2em;
+
+.local-users__actions,
+.local-users__table .bmc-table {
+  max-width: 900px;
+}
+
+.modal__local-users,
+.modal__local-users-settings {
+  .modal-body {
+    padding-left: 0;
+    padding-right: 0;
   }
-  label {
-    width: 100%;
-    min-width: 210px;
-    font-weight: 700;
-    margin-right: 4em;
-  }
-  select,
-  input {
-    width: 225px;
-    width: 225px;
-  }
-  fieldset {
-    display: block;
-    padding-left: 1.5em;
-    margin-bottom: 1em;
-    border-bottom: 1px solid $border-color-01;
-  }
-  .acnt-prop__input-wrapper,
-  .user-manage__input-wrapper {
-    margin-bottom: 5px;
-    select {
-      margin-bottom: 0;
+}
+
+.modal__local-users {
+  input[type="password"] {
+    &::placeholder {
+      color: $primary-dark;
+      font-weight: bold;
     }
-  }
-  .acnt-prop__span-wrapper {
-    position: relative;
-    height: 20px;
-    margin-bottom: 5px;
-  }
-  .password-toggle {
-    position: absolute;
-    right: 20px;
-    top: .6em;
-    padding: 3px;
-    font-size: .8em;
-  }
-  .acnt-prop__submit-wrapper,
-  .user-manage__submit-wrapper {
-    width: 100%;
-    margin-top: 6px;
-    padding-top: 1px;
-    border-top: 1px solid $border-color-01;
-    button {
-      float: right;
-      margin: .5em;
+    &::-ms-placeholder {
+      color: $primary-dark;
+      font-weight: bold;
     }
   }
-  .user-manage__error {
-    background: lighten($status-error, 20%);
-    padding: 1em;
-    text-align: center;
-    font-size: 1em;
-    border: 1px solid $status-error;
-    color: $primary-dark;
-    font-family: "Courier New", Helvetica, Arial, sans-serif;
-    font-weight: 700;
-  }
-  .user-manage__success {
-    color: $primary-accent;
-    padding: 1em;
-    font-size: 1em;
-    font-family: "Courier New", Helvetica, Arial, sans-serif;
-    font-weight: 500;
-  }
 }