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;
- }
}