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