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