summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json5
-rw-r--r--package.json1
-rw-r--r--src/assets/styles/_form-components.scss29
-rw-r--r--src/assets/styles/_modal.scss7
-rw-r--r--src/assets/styles/_obmc-custom.scss6
-rw-r--r--src/assets/styles/_table.scss21
-rw-r--r--src/main.js10
-rw-r--r--src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue47
-rw-r--r--src/views/AccessControl/LocalUserManagement/ModalUser.vue269
-rw-r--r--src/views/AccessControl/LocalUserManagement/TableRoles.vue2
10 files changed, 339 insertions, 58 deletions
diff --git a/package-lock.json b/package-lock.json
index acf7a25c..4b905b29 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16086,6 +16086,11 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true
},
+ "vuelidate": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/vuelidate/-/vuelidate-0.7.4.tgz",
+ "integrity": "sha512-QHZWYOL325Zo+2K7VBNEJTZ496Kd8Z31p85aQJFldKudUUGBmgw4zu4ghl4CyqPwjRCmqZ9lDdx4FSdMnu4fGg=="
+ },
"vuepress": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/vuepress/-/vuepress-1.2.0.tgz",
diff --git a/package.json b/package.json
index 2200800f..188545db 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"vue": "2.6.11",
"vue-date-fns": "1.1.0",
"vue-router": "3.1.3",
+ "vuelidate": "^0.7.4",
"vuex": "3.0.1"
},
"devDependencies": {
diff --git a/src/assets/styles/_form-components.scss b/src/assets/styles/_form-components.scss
new file mode 100644
index 00000000..41b291b2
--- /dev/null
+++ b/src/assets/styles/_form-components.scss
@@ -0,0 +1,29 @@
+.form-text {
+ margin-top: -$spacer / 4;
+ margin-bottom: $spacer / 2;
+ color: $gray-800;
+}
+
+.col-form-label {
+ color: $gray-800;
+ font-size: 14px;
+}
+
+.form-group {
+ margin-bottom: $spacer * 2;
+}
+
+.custom-select,
+.custom-control-label,
+.form-control {
+ //important needed to override validation colors on radio labels
+ color: $gray-900!important;
+ border-color: $gray-400!important;
+ &::before {
+ border-color: $primary;
+ }
+ &.is-invalid,
+ &:invalid {
+ border-bottom: 2px solid $danger!important;
+ }
+} \ No newline at end of file
diff --git a/src/assets/styles/_modal.scss b/src/assets/styles/_modal.scss
new file mode 100644
index 00000000..b20327e7
--- /dev/null
+++ b/src/assets/styles/_modal.scss
@@ -0,0 +1,7 @@
+.modal-header {
+ .close {
+ font-weight: normal;
+ color: $gray-900;
+ opacity: 1;
+ }
+} \ No newline at end of file
diff --git a/src/assets/styles/_obmc-custom.scss b/src/assets/styles/_obmc-custom.scss
index e87e01bb..d20e64e4 100644
--- a/src/assets/styles/_obmc-custom.scss
+++ b/src/assets/styles/_obmc-custom.scss
@@ -1,4 +1,5 @@
$enable-rounded: false;
+$enable-validation-icons: false;
// Required
@import "~bootstrap/scss/functions";
@@ -52,4 +53,7 @@ $colors: map-remove($theme-colors, "light", "dark");
@import "~bootstrap-vue/src/index.scss";
-@import "./buttons"; \ No newline at end of file
+@import "./buttons";
+@import "./form-components";
+@import "./modal";
+@import "./table"; \ No newline at end of file
diff --git a/src/assets/styles/_table.scss b/src/assets/styles/_table.scss
new file mode 100644
index 00000000..ff1ed302
--- /dev/null
+++ b/src/assets/styles/_table.scss
@@ -0,0 +1,21 @@
+.table-light {
+ td {
+ border-top: none;
+ border-bottom: 1px solid $gray-300;
+ }
+}
+
+.thead-light.thead-light {
+ th {
+ border: none;
+ color: $gray-900;
+ }
+}
+
+.table-cell__actions {
+ text-align: right;
+ .btn {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+}
diff --git a/src/main.js b/src/main.js
index b69c6591..e32a56be 100644
--- a/src/main.js
+++ b/src/main.js
@@ -7,6 +7,7 @@ import {
AlertPlugin,
BadgePlugin,
ButtonPlugin,
+ BVConfigPlugin,
CollapsePlugin,
FormPlugin,
FormCheckboxPlugin,
@@ -22,12 +23,20 @@ import {
NavPlugin,
TablePlugin
} from 'bootstrap-vue';
+import Vuelidate from 'vuelidate';
Vue.filter('date', dateFilter);
Vue.use(AlertPlugin);
Vue.use(BadgePlugin);
Vue.use(ButtonPlugin);
+Vue.use(BVConfigPlugin, {
+ BFormText: { textVariant: 'black' },
+ BTable: {
+ headVariant: 'light',
+ footVariant: 'light'
+ }
+});
Vue.use(CollapsePlugin);
Vue.use(FormPlugin);
Vue.use(FormCheckboxPlugin);
@@ -43,6 +52,7 @@ Vue.use(ModalPlugin);
Vue.use(NavbarPlugin);
Vue.use(NavPlugin);
Vue.use(TablePlugin);
+Vue.use(Vuelidate);
new Vue({
router,
diff --git a/src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue b/src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue
index b016dcc6..0ca3428d 100644
--- a/src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue
+++ b/src/views/AccessControl/LocalUserManagement/LocalUserManagement.vue
@@ -2,7 +2,7 @@
<b-container class="ml-0">
<page-title />
<b-row>
- <b-col lg="10">
+ <b-col lg="10" class="text-right">
<b-button variant="link" @click="initModalSettings">
Account policy settings
<icon-settings />
@@ -15,11 +15,11 @@
</b-row>
<b-row>
<b-col lg="10">
- <b-table bordered show-empty head-variant="dark" :items="tableItems">
- <template v-slot:head(actions)="data"></template>
+ <b-table show-empty :fields="fields" :items="tableItems">
<template v-slot:cell(actions)="data">
<b-button
aria-label="Edit user"
+ title="Edit user"
variant="link"
:disabled="!data.value.edit"
@click="initModalUser(data.item)"
@@ -28,6 +28,7 @@
</b-button>
<b-button
aria-label="Delete user"
+ title="Delete user"
variant="link"
:disabled="!data.value.delete"
@click="initModalDelete(data.item)"
@@ -42,6 +43,7 @@
<b-col lg="8">
<b-button v-b-toggle.collapse-role-table variant="link" class="mt-3">
View privilege role descriptions
+ <icon-chevron />
</b-button>
<b-collapse id="collapse-role-table" class="mt-3">
<table-roles />
@@ -50,11 +52,7 @@
</b-row>
<!-- Modals -->
<modal-settings :settings="settings"></modal-settings>
- <modal-user
- :user="activeUser"
- @ok="saveUser"
- @hidden="clearActiveUser"
- ></modal-user>
+ <modal-user :user="activeUser" @ok="saveUser"></modal-user>
</b-container>
</template>
@@ -63,6 +61,7 @@ import IconTrashcan from '@carbon/icons-vue/es/trash-can/20';
import IconEdit from '@carbon/icons-vue/es/edit/20';
import IconAdd from '@carbon/icons-vue/es/add--alt/20';
import IconSettings from '@carbon/icons-vue/es/settings/20';
+import IconChevron from '@carbon/icons-vue/es/chevron--up/20';
import TableRoles from './TableRoles';
import ModalUser from './ModalUser';
@@ -73,6 +72,7 @@ export default {
name: 'LocalUsers',
components: {
IconAdd,
+ IconChevron,
IconEdit,
IconSettings,
IconTrashcan,
@@ -84,7 +84,17 @@ export default {
data() {
return {
activeUser: null,
- settings: null
+ settings: null,
+ fields: [
+ 'username',
+ 'privilege',
+ 'status',
+ {
+ key: 'actions',
+ label: '',
+ tdClass: 'table-cell__actions'
+ }
+ ]
};
},
computed: {
@@ -105,7 +115,8 @@ export default {
actions: {
edit: true,
delete: user.UserName === 'root' ? false : true
- }
+ },
+ ...user
};
});
}
@@ -143,18 +154,15 @@ export default {
// fetch settings then show modal
}
},
- saveUser({ newUser, form }) {
- if (newUser) {
- this.$store.dispatch('localUsers/createUser', form);
+ saveUser({ isNewUser, userData }) {
+ if (isNewUser) {
+ this.$store.dispatch('localUsers/createUser', userData);
} else {
- this.$store.dispatch('localUsers/updateUser', form);
+ this.$store.dispatch('localUsers/updateUser', userData);
}
},
deleteUser({ username }) {
this.$store.dispatch('localUsers/deleteUser', username);
- },
- clearActiveUser() {
- this.activeUser = null;
}
}
};
@@ -164,4 +172,9 @@ export default {
h1 {
margin-bottom: 2rem;
}
+.btn.collapsed {
+ svg {
+ transform: rotate(180deg);
+ }
+}
</style>
diff --git a/src/views/AccessControl/LocalUserManagement/ModalUser.vue b/src/views/AccessControl/LocalUserManagement/ModalUser.vue
index d84fb6d5..59e57062 100644
--- a/src/views/AccessControl/LocalUserManagement/ModalUser.vue
+++ b/src/views/AccessControl/LocalUserManagement/ModalUser.vue
@@ -1,9 +1,5 @@
<template>
- <b-modal
- id="modal-user"
- @ok="$emit('ok', { newUser, form })"
- @hidden="$emit('hidden')"
- >
+ <b-modal id="modal-user" ref="modal" @ok="onOk" @hidden="resetForm">
<template v-slot:modal-title>
<template v-if="newUser">
Add user
@@ -12,27 +8,116 @@
Edit user
</template>
</template>
- <b-form>
- <b-form-group label="Account status">
- <b-form-radio v-model="form.status" name="user-status" value="true"
- >Enabled</b-form-radio
- >
- <b-form-radio v-model="form.status" name="user-status" value="false"
- >Disabled</b-form-radio
- >
- </b-form-group>
- <b-form-group label="Username">
- <b-form-input v-model="form.username" type="text" />
- </b-form-group>
- <b-form-group label="Privilege">
- <b-form-select
- v-model="form.privilege"
- :options="privilegeTypes"
- ></b-form-select>
- </b-form-group>
- <b-form-group label="Password">
- <b-form-input v-model="form.password" type="password" />
- </b-form-group>
+ <b-form novalidate @submit="handleSubmit">
+ <b-container>
+ <b-row>
+ <b-col>
+ <b-form-group label="Account status">
+ <b-form-radio
+ v-model="form.status"
+ name="user-status"
+ :value="true"
+ @input="$v.form.status.$touch()"
+ >
+ Enabled
+ </b-form-radio>
+ <b-form-radio
+ v-model="form.status"
+ name="user-status"
+ :value="false"
+ @input="$v.form.status.$touch()"
+ >
+ Disabled
+ </b-form-radio>
+ </b-form-group>
+ <b-form-group label-for="Username">
+ <b-form-text id="username-help-block">
+ Cannot start with a number
+ <br />
+ No special characters except underscore
+ </b-form-text>
+ <b-form-input
+ v-model="form.username"
+ type="text"
+ aria-describedby="username-help-block"
+ :state="getValidationState('username')"
+ :disabled="!newUser && originalUsername === 'root'"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.username.required">
+ Field required
+ </template>
+ <template v-else-if="!$v.form.username.maxLength">
+ Length must be between 1 – 16 characters
+ </template>
+ <template v-else-if="!$v.form.username.pattern">
+ Invalid format
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ <b-form-group label-for="Privilege">
+ <b-form-select
+ v-model="form.privilege"
+ required
+ :options="privilegeTypes"
+ :state="getValidationState('privilege')"
+ @input="$v.form.privilege.$touch()"
+ >
+ </b-form-select>
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.privilege.required">
+ Field required
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ <b-col>
+ <b-form-group label-for="User password">
+ <b-form-text id="password-help-block" text-variant="black">
+ <!-- TODO: Should be dynamic values -->
+ Password must between 8 – 20 characters
+ </b-form-text>
+ <b-form-input
+ v-model="form.password"
+ type="password"
+ aria-describedby="password-help-block"
+ :state="getValidationState('password')"
+ @input="$v.form.password.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.password.required">
+ Field required
+ </template>
+ <template
+ v-if="
+ !$v.form.password.minLength || !$v.form.password.maxLength
+ "
+ >
+ Length must be between 8 – 20 characters
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ <b-form-group label-for="Confirm user password">
+ <b-form-input
+ v-model="form.passwordConfirmation"
+ type="password"
+ :state="getValidationState('passwordConfirmation')"
+ @input="$v.form.passwordConfirmation.$touch()"
+ />
+ <b-form-invalid-feedback role="alert">
+ <template v-if="!$v.form.passwordConfirmation.required">
+ Field required
+ </template>
+ <template
+ v-else-if="!$v.form.passwordConfirmation.sameAsPassword"
+ >
+ Passwords do not match
+ </template>
+ </b-form-invalid-feedback>
+ </b-form-group>
+ </b-col>
+ </b-row>
+ </b-container>
</b-form>
<template v-slot:modal-ok>
<template v-if="newUser">
@@ -46,6 +131,15 @@
</template>
<script>
+import {
+ required,
+ maxLength,
+ minLength,
+ sameAs,
+ helpers,
+ requiredIf
+} from 'vuelidate/lib/validators';
+
export default {
props: {
user: {
@@ -55,25 +149,122 @@ export default {
},
data() {
return {
- privilegeTypes: ['Administrator', 'Operator', 'ReadOnly', 'NoAccess']
+ privilegeTypes: ['Administrator', 'Operator', 'ReadOnly', 'NoAccess'],
+ originalUsername: '',
+ form: {
+ status: true,
+ username: '',
+ privilege: '',
+ password: '',
+ passwordConfirmation: ''
+ }
};
},
computed: {
newUser() {
return this.user ? false : true;
+ }
+ },
+ watch: {
+ user: function(value) {
+ if (value === null) return;
+ this.originalUsername = value.username;
+ this.form.username = value.username;
+ this.form.status = value.Enabled;
+ this.form.privilege = value.privilege;
+ }
+ },
+ validations: {
+ form: {
+ status: {
+ required
+ },
+ username: {
+ required,
+ maxLength: maxLength(16),
+ pattern: helpers.regex('pattern', /^([a-zA-Z_][a-zA-Z0-9_]*)/)
+ },
+ privilege: {
+ required
+ },
+ password: {
+ required: requiredIf(function() {
+ return this.requirePassword();
+ }),
+ minLength: minLength(8),
+ maxLength: maxLength(20)
+ },
+ passwordConfirmation: {
+ required: requiredIf(function() {
+ return this.requirePassword();
+ }),
+ sameAsPassword: sameAs('password')
+ }
+ }
+ },
+ methods: {
+ handleSubmit() {
+ let userData = {};
+
+ if (this.newUser) {
+ this.$v.$touch();
+ if (this.$v.$invalid) return;
+ userData.username = this.form.username;
+ userData.status = this.form.status;
+ userData.privilege = this.form.privilege;
+ userData.password = this.form.password;
+ } else {
+ if (this.$v.$invalid) return;
+ userData.originalUsername = this.originalUsername;
+ if (this.$v.form.status.$dirty) {
+ userData.status = this.form.status;
+ }
+ if (this.$v.form.username.$dirty) {
+ userData.username = this.form.username;
+ }
+ if (this.$v.form.privilege.$dirty) {
+ userData.privilege = this.form.privilege;
+ }
+ if (this.$v.form.password.$dirty) {
+ userData.password = this.form.password;
+ }
+ if (Object.entries(userData).length === 1) {
+ this.closeModal();
+ return;
+ }
+ }
+
+ this.$emit('ok', { isNewUser: this.newUser, userData });
+ this.closeModal();
+ },
+ closeModal() {
+ this.$nextTick(() => {
+ this.$refs.modal.hide();
+ });
+ },
+ resetForm() {
+ this.form.originalUsername = '';
+ this.form.status = true;
+ this.form.username = '';
+ this.form.privilege = '';
+ this.form.password = '';
+ this.form.passwordConfirmation = '';
+ this.$v.$reset();
+ },
+ getValidationState(name) {
+ const { $dirty, $error } = this.$v.form[name];
+ return $dirty ? !$error : null;
+ },
+ requirePassword() {
+ if (this.newUser) return true;
+ if (this.$v.form.password.$dirty) return true;
+ if (this.$v.form.passwordConfirmation.$dirty) return true;
+ return false;
},
- form() {
- return {
- originalUsername: this.newUser ? null : this.user.username,
- status: this.newUser
- ? true
- : this.user.status === 'Enabled'
- ? true
- : false,
- username: this.newUser ? '' : this.user.username,
- privilege: this.newUser ? '' : this.user.privilege,
- password: ''
- };
+ onOk(bvModalEvt) {
+ // prevent modal close
+ bvModalEvt.preventDefault();
+ this.handleSubmit();
}
}
};
diff --git a/src/views/AccessControl/LocalUserManagement/TableRoles.vue b/src/views/AccessControl/LocalUserManagement/TableRoles.vue
index ad313bef..0927c55d 100644
--- a/src/views/AccessControl/LocalUserManagement/TableRoles.vue
+++ b/src/views/AccessControl/LocalUserManagement/TableRoles.vue
@@ -1,5 +1,5 @@
<template>
- <b-table bordered small head-variant="dark" :items="items" :fields="fields">
+ <b-table small :items="items" :fields="fields">
<template v-slot:cell(administrator)="data">
<template v-if="data.value">
<checkmark20 />