diff options
-rw-r--r-- | src/assets/styles/vendor-overrides/bootstrap/_dropdown.scss | 9 | ||||
-rw-r--r-- | src/components/AppHeader/AppHeader.vue | 116 | ||||
-rw-r--r-- | src/locales/en-US.json | 11 | ||||
-rw-r--r-- | src/router/index.js | 8 | ||||
-rw-r--r-- | src/store/modules/Authentication/AuthenticanStore.js | 1 | ||||
-rw-r--r-- | src/store/modules/GlobalStore.js | 9 | ||||
-rw-r--r-- | src/views/Login/Login.vue | 2 | ||||
-rw-r--r-- | src/views/ProfileSettings/ProfileSettings.vue | 162 | ||||
-rw-r--r-- | src/views/ProfileSettings/index.js | 2 |
9 files changed, 264 insertions, 56 deletions
diff --git a/src/assets/styles/vendor-overrides/bootstrap/_dropdown.scss b/src/assets/styles/vendor-overrides/bootstrap/_dropdown.scss index 0eb310f6..c7d39548 100644 --- a/src/assets/styles/vendor-overrides/bootstrap/_dropdown.scss +++ b/src/assets/styles/vendor-overrides/bootstrap/_dropdown.scss @@ -9,11 +9,12 @@ } } +// Adding component style to global stylesheet because +// single-file component scoped styles aren't +// being applied to dynamically appended elements +// The overflow menu should be above the table + .table-filter { - // Adding component style to global stylesheet because - // single-file component scoped styles aren't - // being applied to dynamically appended elements - // The overflow menu should be above the table .dropdown-menu { z-index: $zindex-dropdown + 1; padding: 0; diff --git a/src/components/AppHeader/AppHeader.vue b/src/components/AppHeader/AppHeader.vue index a755a628..39d52b83 100644 --- a/src/components/AppHeader/AppHeader.vue +++ b/src/components/AppHeader/AppHeader.vue @@ -43,11 +43,19 @@ <icon-renew /> </b-button> </li> - <li> - <b-button id="app-header-logout" variant="link" @click="logout"> - {{ $t('appHeader.logOut') }} - <icon-avatar /> - </b-button> + <li class="nav-item"> + <b-dropdown id="app-header-user" variant="link" right> + <template v-slot:button-content> + <icon-avatar /> + {{ username }} + </template> + <b-dropdown-item to="/profile-settings" + >{{ $t('appHeader.profileSettings') }} + </b-dropdown-item> + <b-dropdown-item @click="logout">{{ + $t('appHeader.logOut') + }}</b-dropdown-item> + </b-dropdown> </li> </b-navbar-nav> </b-navbar> @@ -110,6 +118,9 @@ export default { default: return 'secondary'; } + }, + username() { + return this.$store.getters['global/username']; } }, created() { @@ -142,64 +153,71 @@ export default { }; </script> -<style lang="scss" scoped> +<style lang="scss"> @import 'src/assets/styles/helpers'; -.link-skip-nav { - position: absolute; - top: -60px; - left: 0.5rem; - z-index: $zindex-popover; - transition: $duration--moderate-01 $exit-easing--expressive; - &:focus { - top: 0.5rem; - transition-timing-function: $entrance-easing--expressive; +.app-header { + .link-skip-nav { + position: absolute; + top: -60px; + left: 0.5rem; + z-index: $zindex-popover; + transition: $duration--moderate-01 $exit-easing--expressive; + &:focus { + top: 0.5rem; + transition-timing-function: $entrance-easing--expressive; + } } -} -.navbar-dark { - .navbar-text, - .nav-link, - .btn-link { - color: $white !important; - fill: currentColor; + .navbar-dark { + .navbar-text, + .nav-link, + .btn-link { + color: $white !important; + fill: currentColor; + } } -} - -.nav-item { - fill: $light; -} -.navbar { - padding: 0; - height: $header-height; - overflow: hidden; - - .btn-link { - padding: $spacer / 2; + .nav-item { + fill: $light; } -} -.navbar-nav { - padding: 0 $spacer; -} + .navbar { + padding: 0; + height: $header-height; -.nav-trigger { - fill: $light; - width: $header-height; - height: $header-height; - transition: none; + .btn-link { + padding: $spacer / 2; + } + } - svg { - margin: 0; + .navbar-nav { + padding: 0 $spacer; } - &:hover { + .nav-trigger { fill: $light; - background-color: $dark; + width: $header-height; + height: $header-height; + transition: none; + + svg { + margin: 0; + } + + &:hover { + fill: $light; + background-color: $dark; + } + + @include media-breakpoint-up($responsive-layout-bp) { + display: none; + } } - @include media-breakpoint-up($responsive-layout-bp) { - display: none; + .dropdown { + .dropdown-menu { + margin-top: 7px; + } } } </style> diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 1ccc330e..84a2a604 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -64,6 +64,7 @@ "health": "Health", "logOut": "Log out", "power": "Power", + "profileSettings": "@:appPageTitle.profileSettings", "refresh": "Refresh", "skipToContent": "Skip to content" }, @@ -98,6 +99,7 @@ "managePowerUsage": "Manage power usage", "networkSettings": "Network settings", "overview": "Overview", + "profileSettings":"Profile settings", "rebootBmc": "Reboot BMC", "sensors": "Sensors", "serverLed": "Server LED", @@ -299,6 +301,15 @@ "solConsole": "Serial over LAN console" } }, + "profileSettings": { + "changePassword": "Change password", + "confirmPassword": "Confirm new password", + "newPassword": "New password", + "newPassLabelTextInfo": "Password must be between %{min} - %{max} characters", + "passwordsDoNotMatch": "Passwords do not match", + "profileInfoTitle": "Profile information", + "username": "Username" + }, "pageManagePowerUsage": { "description": "Set a power cap to keep power consumption at or below the specified value in watts", "powerCapLabel": "Power cap value (in watts)", diff --git a/src/router/index.js b/src/router/index.js index f67d5ee4..22662d71 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -24,6 +24,14 @@ const routes = [ } }, { + path: '/profile-settings', + name: 'profile-settings', + component: () => import('@/views/ProfileSettings'), + meta: { + title: 'appPageTitle.profileSettings' + } + }, + { path: '/health/event-logs', name: 'event-logs', component: () => import('@/views/Health/EventLogs'), diff --git a/src/store/modules/Authentication/AuthenticanStore.js b/src/store/modules/Authentication/AuthenticanStore.js index 7a0c5ba3..407c2b57 100644 --- a/src/store/modules/Authentication/AuthenticanStore.js +++ b/src/store/modules/Authentication/AuthenticanStore.js @@ -23,6 +23,7 @@ const AuthenticationStore = { }, logout() { Cookies.remove('XSRF-TOKEN'); + localStorage.removeItem('storedUsername'); } }, actions: { diff --git a/src/store/modules/GlobalStore.js b/src/store/modules/GlobalStore.js index 42e9e2bf..55b07965 100644 --- a/src/store/modules/GlobalStore.js +++ b/src/store/modules/GlobalStore.js @@ -31,19 +31,22 @@ const GlobalStore = { state: { bmcTime: null, hostStatus: 'unreachable', - languagePreference: localStorage.getItem('storedLanguage') || 'en-US' + languagePreference: localStorage.getItem('storedLanguage') || 'en-US', + username: localStorage.getItem('storedUsername') }, getters: { hostStatus: state => state.hostStatus, bmcTime: state => state.bmcTime, - languagePreference: state => state.languagePreference + languagePreference: state => state.languagePreference, + username: state => state.username }, mutations: { setBmcTime: (state, bmcTime) => (state.bmcTime = bmcTime), setHostStatus: (state, hostState) => (state.hostStatus = hostStateMapper(hostState)), setLanguagePreference: (state, language) => - (state.languagePreference = language) + (state.languagePreference = language), + setUsername: (state, username) => (state.username = username) }, actions: { async getBmcTime({ commit }) { diff --git a/src/views/Login/Login.vue b/src/views/Login/Login.vue index 4d8f2484..3993800c 100644 --- a/src/views/Login/Login.vue +++ b/src/views/Login/Login.vue @@ -138,6 +138,8 @@ export default { .then(() => this.$router.push('/')) .then(() => { localStorage.setItem('storedLanguage', i18n.locale); + localStorage.setItem('storedUsername', username); + this.$store.commit('global/setUsername', username); this.$store.commit('global/setLanguagePreference', i18n.locale); }) .catch(error => console.log(error)) diff --git a/src/views/ProfileSettings/ProfileSettings.vue b/src/views/ProfileSettings/ProfileSettings.vue new file mode 100644 index 00000000..df74b4b7 --- /dev/null +++ b/src/views/ProfileSettings/ProfileSettings.vue @@ -0,0 +1,162 @@ +<template> + <b-container fluid="xl"> + <page-title /> + + <b-row> + <b-col md="8" lg="8" xl="6"> + <page-section :section-title="$t('profileSettings.profileInfoTitle')"> + <dl> + <dt>{{ $t('profileSettings.username') }}</dt> + <dd> + {{ username }} + </dd> + </dl> + </page-section> + </b-col> + </b-row> + + <b-form @submit.prevent="submitForm"> + <b-row> + <b-col sm="8" md="6" xl="3"> + <page-section :section-title="$t('profileSettings.changePassword')"> + <b-form-group + id="input-group-1" + :label="$t('profileSettings.newPassword')" + label-for="input-1" + > + <b-form-text id="password-help-block"> + {{ + $t('pageLocalUserManagement.modal.passwordMustBeBetween', { + min: passwordRequirements.minLength, + max: passwordRequirements.maxLength + }) + }} + </b-form-text> + <input-password-toggle> + <b-form-input + id="password" + v-model="form.newPassword" + type="password" + aria-describedby="password-help-block" + :state="getValidationState($v.form.newPassword)" + @input="$v.form.newPassword.$touch()" + /> + <b-form-invalid-feedback role="alert"> + <template v-if="!$v.form.newPassword.required"> + {{ $t('global.form.fieldRequired') }} + </template> + <template + v-if=" + !$v.form.newPassword.minLength || + !$v.form.newPassword.maxLength + " + > + {{ + $t('profileSettings.newPassLabelTextInfo', { + min: passwordRequirements.minLength, + max: passwordRequirements.maxLength + }) + }} + </template> + </b-form-invalid-feedback> + </input-password-toggle> + </b-form-group> + <b-form-group + id="input-group-2" + :label="$t('profileSettings.confirmPassword')" + label-for="input-2" + > + <input-password-toggle> + <b-form-input + id="password-confirmation" + v-model="form.confirmPassword" + type="password" + :state="getValidationState($v.form.confirmPassword)" + @input="$v.form.confirmPassword.$touch()" + /> + <b-form-invalid-feedback role="alert"> + <template v-if="!$v.form.confirmPassword.required"> + {{ $t('global.form.fieldRequired') }} + </template> + <template v-else-if="!$v.form.confirmPassword.sameAsPassword"> + {{ $t('profileSettings.passwordsDoNotMatch') }} + </template> + </b-form-invalid-feedback> + </input-password-toggle> + </b-form-group> + </page-section> + </b-col> + </b-row> + <b-button variant="primary" type="submit"> + {{ $t('global.action.save') }} + </b-button> + </b-form> + </b-container> +</template> + +<script> +import PageTitle from '@/components/Global/PageTitle'; +import PageSection from '@/components/Global/PageSection'; +import BVToastMixin from '@/components/Mixins/BVToastMixin'; +import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js'; +import InputPasswordToggle from '@/components/Global/InputPasswordToggle'; +import { + maxLength, + minLength, + required, + sameAs +} from 'vuelidate/lib/validators'; + +export default { + name: 'ProfileSettings', + components: { PageTitle, PageSection, InputPasswordToggle }, + mixins: [BVToastMixin, VuelidateMixin], + data() { + return { + passwordRequirements: { + minLength: 8, + maxLength: 20 + }, + form: { + newPassword: '', + confirmPassword: '' + } + }; + }, + validations() { + return { + form: { + newPassword: { + required, + minLength: minLength(this.passwordRequirements.minLength), + maxLength: maxLength(this.passwordRequirements.maxLength) + }, + confirmPassword: { + required, + sameAsPassword: sameAs('newPassword') + } + } + }; + }, + computed: { + username() { + return this.$store.getters['global/username']; + } + }, + methods: { + submitForm() { + this.$v.$touch(); + if (this.$v.$invalid) return; + let userData = { + originalUsername: this.username, + password: this.form.newPassword + }; + + this.$store + .dispatch('localUsers/updateUser', userData) + .then(message => this.successToast(message)) + .catch(({ message }) => this.errorToast(message)); + } + } +}; +</script> diff --git a/src/views/ProfileSettings/index.js b/src/views/ProfileSettings/index.js new file mode 100644 index 00000000..d6589c72 --- /dev/null +++ b/src/views/ProfileSettings/index.js @@ -0,0 +1,2 @@ +import ProfileSettings from './ProfileSettings.vue'; +export default ProfileSettings; |