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/common/components/index.js b/app/common/components/index.js
new file mode 100644
index 0000000..67d5a2d
--- /dev/null
+++ b/app/common/components/index.js
@@ -0,0 +1,9 @@
+/**
+ * A module to contain common components
+ */
+window.angular && (function(angular) {
+  'use strict';
+
+  // Register app.common.components module
+  angular.module('app.common.components', []);
+})(window.angular);
diff --git a/app/common/components/table/table.html b/app/common/components/table/table.html
new file mode 100644
index 0000000..6ec520c
--- /dev/null
+++ b/app/common/components/table/table.html
@@ -0,0 +1,36 @@
+<table class="bmc-table">
+  <thead>
+    <!-- Header row -->
+    <tr>
+      <th ng-repeat="header in $ctrl.model.header"
+          class="bmc-table__column-header">
+        {{header}}
+      </th>
+    </tr>
+  </thead>
+  <tbody>
+    <!-- Data rows -->
+    <tr ng-if="$ctrl.model.data.length > 0"
+        ng-repeat="row in $ctrl.model.data"
+        class="bmc-table__row">
+      <!-- Row item -->
+      <td ng-repeat="item in row.uiData"
+          class="bmc-table__cell">
+        {{item}}
+      </td>
+      <!-- Row Actions -->
+      <td ng-if="$ctrl.model.actions.length > 0"
+          class="bmc-table__cell  bmc-table__row-actions">
+        <button ng-repeat="action in $ctrl.model.actions"
+                ng-click="$ctrl.onClickAction(action, row);"
+                class="btn  btn-tertiary">
+          {{action}}
+        </button>
+      </td>
+    </tr>
+    <!-- Empty table -->
+    <tr ng-if="$ctrl.model.data.length === 0">
+      <td>No data</td>
+    </tr>
+  </tbody>
+</table>
\ No newline at end of file
diff --git a/app/common/components/table/table.js b/app/common/components/table/table.js
new file mode 100644
index 0000000..2d7fc77
--- /dev/null
+++ b/app/common/components/table/table.js
@@ -0,0 +1,90 @@
+window.angular && (function(angular) {
+  'use strict';
+
+  /**
+   *
+   * Controller for bmcTable Component
+   *
+   * To use:
+   * The <bmc-table> component expects a 'model' attribute
+   * that will contain all the data needed to render the table.
+   *
+   * The model object should contain 'header', 'data', and 'actions'
+   * properties.
+   *
+   * model: {
+   *    header: <string>[],   // Array of header labels
+   *    data: <any>[],        // Array of each row object
+   *    actions: <string>[]   // Array of action labels
+   * }
+   *
+   * The header property will render each label as a <th> in the table.
+   *
+   * The data property will render each item as a <tr> in the table.
+   * Each row object in the model.data array should also have a 'uiData'
+   * property that should be an array of the properties that will render
+   * as each table cell <td>.
+   *
+   * The actions property will render into clickable buttons at the end
+   * of each row.
+   * When a user clicks an action button, the component
+   * will emit the action label with the associated row object.
+   *
+   */
+  const TableController = function() {
+    /**
+     * Init model data
+     * @param {any} model : table model object
+     * @returns : table model object with defaults
+     */
+    const setModel = (model) => {
+      model.header = model.header === undefined ? [] : model.header;
+      model.data = model.data === undefined ? [] : model.data;
+      model.data = model.data.map((row) => {
+        if (row.uiData === undefined) {
+          row.uiData = [];
+        }
+        return row;
+      })
+      model.actions = model.actions === undefined ? [] : model.actions;
+
+      if (model.actions.length > 0) {
+        // If table actions were provided, push an empty
+        // string to the header array to account for additional
+        // table actions cell
+        model.header.push('');
+      }
+      return model;
+    };
+
+    /**
+     * Callback when table row action clicked
+     * Emits user desired action and associated row data to
+     * parent controller
+     * @param {string} action : action type
+     * @param {any} row : user object
+     */
+    this.onClickAction = (action, row) => {
+      if (action !== undefined && row !== undefined) {
+        const value = {action, row};
+        this.emitAction({value});
+      }
+    };
+
+    /**
+     * onInit Component lifecycle hooked
+     */
+    this.$onInit = () => {
+      this.model = setModel(this.model);
+    };
+  };
+
+  /**
+   * Register bmcTable component
+   */
+  angular.module('app.common.components').component('bmcTable', {
+    template: require('./table.html'),
+    controller: TableController,
+    bindings: {model: '<', emitAction: '&'}
+  })
+})(window.angular);
diff --git a/app/common/directives/password-confirmation.js b/app/common/directives/password-confirmation.js
new file mode 100644
index 0000000..253a6a6
--- /dev/null
+++ b/app/common/directives/password-confirmation.js
@@ -0,0 +1,42 @@
+window.angular && (function(angular) {
+  'use strict';
+
+  /**
+   * Password confirmation validator
+   *
+   * To use, add attribute directive to password confirmation input field
+   * Also include attribute 'first-password' with value set to first password
+   * to check against
+   *
+   * <input password-confirmation first-password="ctrl.password"
+   * name="passwordConfirm">
+   *
+   */
+  angular.module('app.common.directives')
+      .directive('passwordConfirm', function() {
+        return {
+          restrict: 'A',
+          require: 'ngModel',
+          scope: {firstPassword: '='},
+          link: function(scope, element, attrs, controller) {
+            if (controller === undefined) {
+              return;
+            }
+            controller.$validators.passwordConfirm =
+                (modelValue, viewValue) => {
+                  const firstPassword =
+                      scope.firstPassword ? scope.firstPassword : '';
+                  const secondPassword = modelValue || viewValue || '';
+                  if (firstPassword == secondPassword) {
+                    return true;
+                  } else {
+                    return false;
+                  }
+                };
+            element.on('keyup', () => {
+              controller.$validate();
+            });
+          }
+        };
+      });
+})(window.angular);
diff --git a/app/common/services/api-utils.js b/app/common/services/api-utils.js
index d485016..27b122d 100644
--- a/app/common/services/api-utils.js
+++ b/app/common/services/api-utils.js
@@ -530,22 +530,17 @@
                        '/redfish/v1/AccountService/Roles',
                    withCredentials: true
                  })
-              .then(
-                  function(response) {
-                    var members = response.data['Members'];
-                    angular.forEach(members, function(member) {
-                      roles.push(member['@odata.id'].split('/').pop());
-                    });
-                    return roles;
-                  },
-                  function(error) {
-                    console.log(error);
-                  });
+              .then(function(response) {
+                var members = response.data['Members'];
+                angular.forEach(members, function(member) {
+                  roles.push(member['@odata.id'].split('/').pop());
+                });
+                return roles;
+              });
         },
         getAllUserAccounts: function() {
           var deferred = $q.defer();
           var promises = [];
-          var users = [];
 
           $http({
             method: 'GET',
@@ -581,19 +576,15 @@
           return deferred.promise;
         },
 
-        getAllUserAccountProperties: function(callback) {
+        getAllUserAccountProperties: function() {
           return $http({
                    method: 'GET',
                    url: DataService.getHost() + '/redfish/v1/AccountService',
                    withCredentials: true
                  })
-              .then(
-                  function(response) {
-                    return response.data;
-                  },
-                  function(error) {
-                    console.log(error);
-                  });
+              .then(function(response) {
+                return response.data;
+              });
         },
 
         saveUserAccountProperties: function(lockoutduration, lockoutthreshold) {
diff --git a/app/common/styles/base/buttons.scss b/app/common/styles/base/buttons.scss
index 1d90036..25e5a91 100644
--- a/app/common/styles/base/buttons.scss
+++ b/app/common/styles/base/buttons.scss
@@ -53,6 +53,7 @@
     display: inline-block;
     margin-right: 0.3em;
     vertical-align: bottom;
+    margin-left: -0.5em;
   }
   img {
     width: 1.5em;
diff --git a/app/common/styles/base/forms.scss b/app/common/styles/base/forms.scss
index f04e827..c775c48 100644
--- a/app/common/styles/base/forms.scss
+++ b/app/common/styles/base/forms.scss
@@ -1,8 +1,12 @@
 label,
 legend {
-  font-size: 1em;
-  font-weight: 300;
   margin: 0;
+  color: $text-02;
+  text-transform: uppercase;
+  font-weight: 700;
+  font-size: 0.75em;
+  margin-bottom: 0;
+  line-height: 2.2;
   .error {
     font-size: 0.9em;
   }
@@ -141,7 +145,21 @@
 }
 .form__validation-message {
   color: $status-error;
+  font-size: 0.8em;
+  line-height: 1.1;
+  padding-top: 2px;
+}
+
+.radio-label {
+  text-transform: none;
+  font-weight: normal;
   font-size: 0.9em;
+  line-height: 1.2;
+  margin: 0.8em 0;
+  color: $text-01;
+  input[type=radio] {
+    margin-bottom: 0;
+  }
 }
 
 /**
@@ -210,3 +228,19 @@
     margin-left: 1rem;
   }
 }
+
+.radio-option__input-field-group {
+  margin-left: 1.5em;
+}
+
+.field-group-container {
+  margin-bottom: 30px;
+  position: relative;
+  &:last-child {
+    margin-bottom: 12px;
+  }
+
+  input + .form__validation-message {
+    position: absolute;
+  }
+}
diff --git a/app/common/styles/base/typography.scss b/app/common/styles/base/typography.scss
index baa6a60..bcf26d1 100644
--- a/app/common/styles/base/typography.scss
+++ b/app/common/styles/base/typography.scss
@@ -62,3 +62,8 @@
   font-weight: 700;
   margin-bottom: 0;
 }
+
+.page-title {
+  margin-bottom: 50px;
+  font-size: 2rem;
+}
diff --git a/app/common/styles/base/utility.scss b/app/common/styles/base/utility.scss
index 26f138a..a271d33 100644
--- a/app/common/styles/base/utility.scss
+++ b/app/common/styles/base/utility.scss
@@ -130,4 +130,8 @@
 @keyframes flash {
   0% { background: $primary-accent; }
   100% { background: none; }
+}
+
+.nowrap {
+  white-space: nowrap!important;
 }
\ No newline at end of file
diff --git a/app/common/styles/components/table.scss b/app/common/styles/components/table.scss
index 67dc0be..17df264 100644
--- a/app/common/styles/components/table.scss
+++ b/app/common/styles/components/table.scss
@@ -146,3 +146,25 @@
     }
   }
 }
+
+.bmc-table {
+  width: 100%;
+}
+
+.bmc-table__row {
+  border-bottom: 1px solid $border-color-01;
+}
+
+.bmc-table__column-header {
+  padding: 10px 16px;
+  background-color: $background-03;
+}
+
+.bmc-table__cell {
+  padding: 4px 16px;
+  background-color: $base-02--07;
+}
+
+.bmc-table__row-actions {
+  text-align: right;
+}
\ No newline at end of file
diff --git a/app/common/styles/elements/modals.scss b/app/common/styles/elements/modals.scss
index dc1c9d8..0e21a39 100644
--- a/app/common/styles/elements/modals.scss
+++ b/app/common/styles/elements/modals.scss
@@ -97,9 +97,25 @@
   }
 }
 
+.uib-modal .modal-dialog {
+  // override bootstrap max-width set at 500px
+  max-width: 550px;
+}
+
 .modal-backdrop.in {
   opacity: 0.5;
 }
 .uib-modal__content {
   padding: 1em;
 }
+
+.uib-modal {
+  .btn--close {
+    position: absolute;
+    right: 0;
+    top: 0;
+    svg {
+      height: 2em;
+    }
+  }
+}
\ No newline at end of file
diff --git a/app/index.js b/app/index.js
index a4d4bee..5525aef 100644
--- a/app/index.js
+++ b/app/index.js
@@ -62,6 +62,10 @@
 import dir_paginate from './common/directives/dirPagination.js';
 import form_input_error from './common/directives/form-input-error.js';
 import icon_provider from './common/directives/icon-provider.js';
+import password_confirmation from './common/directives/password-confirmation.js';
+
+import components_index from './common/components/index.js';
+import table_component from './common/components/table/table.js';
 
 import login_index from './login/index.js';
 import login_controller from './login/controllers/login-controller.js';
@@ -97,6 +101,7 @@
 
 import users_index from './users/index.js';
 import user_accounts_controller from './users/controllers/user-accounts-controller.js';
+import username_validator from './users/directives/username-validator.js';
 
 window.angular && (function(angular) {
   'use strict';
@@ -111,7 +116,7 @@
             'ui.bootstrap',
             // Basic resources
             'app.common.services', 'app.common.directives',
-            'app.common.filters',
+            'app.common.filters', 'app.common.components',
             // Model resources
             'app.login', 'app.overview', 'app.serverControl',
             'app.serverHealth', 'app.configuration', 'app.users', 'app.redfish'
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;
-  }
 }