1<template> 2 <div> 3 <header id="page-header"> 4 <a 5 class="link-skip-nav btn btn-light" 6 href="#main-content" 7 @click="setFocus" 8 > 9 {{ $t('appHeader.skipToContent') }} 10 </a> 11 12 <b-navbar type="dark" :aria-label="$t('appHeader.applicationHeader')"> 13 <!-- Left aligned nav items --> 14 <b-button 15 id="app-header-trigger" 16 class="nav-trigger" 17 aria-hidden="true" 18 type="button" 19 variant="link" 20 :class="{ open: isNavigationOpen }" 21 @click="toggleNavigation" 22 > 23 <icon-close 24 v-if="isNavigationOpen" 25 :title="$t('appHeader.titleHideNavigation')" 26 /> 27 <icon-menu 28 v-if="!isNavigationOpen" 29 :title="$t('appHeader.titleShowNavigation')" 30 /> 31 </b-button> 32 <b-navbar-nav> 33 <b-navbar-brand 34 class="me-0" 35 to="/" 36 data-test-id="appHeader-container-overview" 37 > 38 <img 39 svg-inline 40 class="header-logo" 41 src="@/assets/images/logo-header.svg" 42 :alt="altLogo" 43 /> 44 </b-navbar-brand> 45 <div v-if="isNavTagPresent" :key="routerKey" class="ps-2 nav-tags"> 46 <span>|</span> 47 <span class="ps-3 asset-tag">{{ assetTag }}</span> 48 <span class="ps-3">{{ modelType }}</span> 49 <span class="ps-3">{{ serialNumber }}</span> 50 </div> 51 </b-navbar-nav> 52 <!-- Right aligned nav items --> 53 <b-navbar-nav class="ms-auto helper-menu"> 54 <b-nav-item 55 to="/logs/event-logs" 56 data-test-id="appHeader-container-health" 57 > 58 <status-icon :status="healthStatusIcon" /> 59 {{ $t('appHeader.health') }} 60 </b-nav-item> 61 <b-nav-item 62 to="/operations/server-power-operations" 63 data-test-id="appHeader-container-power" 64 > 65 <status-icon :status="serverStatusIcon" /> 66 {{ $t('appHeader.power') }} 67 </b-nav-item> 68 <!-- Using LI elements instead of b-nav-item to support semantic button elements --> 69 <li class="nav-item"> 70 <b-button 71 id="app-header-refresh" 72 variant="link" 73 data-test-id="appHeader-button-refresh" 74 @click="refresh" 75 > 76 <icon-renew :title="$t('appHeader.titleRefresh')" /> 77 <span class="responsive-text">{{ $t('appHeader.refresh') }}</span> 78 </b-button> 79 </li> 80 <li class="nav-item"> 81 <b-dropdown 82 id="app-header-user" 83 variant="link" 84 right 85 data-test-id="appHeader-container-user" 86 > 87 <template #button-content> 88 <icon-avatar :title="$t('appHeader.titleProfile')" /> 89 <span class="responsive-text">{{ username }}</span> 90 </template> 91 <b-dropdown-item 92 to="/profile-settings" 93 data-test-id="appHeader-link-profile" 94 >{{ $t('appHeader.profileSettings') }} 95 </b-dropdown-item> 96 <b-dropdown-item 97 data-test-id="appHeader-link-logout" 98 @click="logout" 99 > 100 {{ $t('appHeader.logOut') }} 101 </b-dropdown-item> 102 </b-dropdown> 103 </li> 104 </b-navbar-nav> 105 </b-navbar> 106 </header> 107 <loading-bar /> 108 </div> 109</template> 110 111<script> 112import BVToastMixin from '@/components/Mixins/BVToastMixin'; 113import IconAvatar from '@carbon/icons-vue/es/user--avatar/20'; 114import IconClose from '@carbon/icons-vue/es/close/20'; 115import IconMenu from '@carbon/icons-vue/es/menu/20'; 116import IconRenew from '@carbon/icons-vue/es/renew/20'; 117import StatusIcon from '@/components/Global/StatusIcon'; 118import LoadingBar from '@/components/Global/LoadingBar'; 119import { mapState } from 'vuex'; 120import i18n from '@/i18n'; 121 122export default { 123 name: 'AppHeader', 124 components: { 125 IconAvatar, 126 IconClose, 127 IconMenu, 128 IconRenew, 129 StatusIcon, 130 LoadingBar, 131 }, 132 mixins: [BVToastMixin], 133 props: { 134 routerKey: { 135 type: Number, 136 default: 0, 137 }, 138 }, 139 emits: ['refresh'], 140 data() { 141 return { 142 isNavigationOpen: false, 143 altLogo: process.env.VUE_APP_COMPANY_NAME || 'Built on OpenBMC', 144 }; 145 }, 146 computed: { 147 ...mapState('authentication', ['consoleWindow']), 148 isNavTagPresent() { 149 return this.assetTag || this.modelType || this.serialNumber; 150 }, 151 assetTag() { 152 return this.$store.getters['global/assetTag']; 153 }, 154 modelType() { 155 return this.$store.getters['global/modelType']; 156 }, 157 serialNumber() { 158 return this.$store.getters['global/serialNumber']; 159 }, 160 isAuthorized() { 161 return this.$store.getters['global/isAuthorized']; 162 }, 163 userPrivilege() { 164 return this.$store.getters['global/userPrivilege']; 165 }, 166 serverStatus() { 167 return this.$store.getters['global/serverStatus']; 168 }, 169 healthStatus() { 170 return this.$store.getters['eventLog/healthStatus']; 171 }, 172 serverStatusIcon() { 173 switch (this.serverStatus) { 174 case 'on': 175 return 'success'; 176 case 'error': 177 return 'danger'; 178 case 'diagnosticMode': 179 return 'warning'; 180 case 'off': 181 default: 182 return 'secondary'; 183 } 184 }, 185 healthStatusIcon() { 186 switch (this.healthStatus) { 187 case 'OK': 188 return 'success'; 189 case 'Warning': 190 return 'warning'; 191 case 'Critical': 192 return 'danger'; 193 default: 194 return 'secondary'; 195 } 196 }, 197 username() { 198 return this.$store.getters['global/username']; 199 }, 200 }, 201 watch: { 202 consoleWindow() { 203 if (this.consoleWindow === false) this.$eventBus.$consoleWindow?.close(); 204 }, 205 isAuthorized(value) { 206 if (value === false) { 207 this.errorToast(i18n.global.t('global.toast.unAuthDescription'), { 208 title: i18n.global.t('global.toast.unAuthTitle'), 209 }); 210 } 211 }, 212 }, 213 created() { 214 // Reset auth state to check if user is authenticated based 215 // on available browser cookies 216 this.$store.dispatch('authentication/resetStoreState'); 217 this.getSystemInfo(); 218 this.getEvents(); 219 }, 220 mounted() { 221 require('@/eventBus').default.$on( 222 'change-is-navigation-open', 223 (isNavigationOpen) => (this.isNavigationOpen = isNavigationOpen), 224 ); 225 }, 226 beforeUnmount() { 227 require('@/eventBus').default.$off( 228 'change-is-navigation-open', 229 this.handleNavigationChange, 230 ); 231 }, 232 methods: { 233 getSystemInfo() { 234 this.$store.dispatch('global/getSystemInfo'); 235 }, 236 getEvents() { 237 this.$store.dispatch('eventLog/getEventLogData'); 238 }, 239 refresh() { 240 this.$emit('refresh'); 241 }, 242 logout() { 243 this.$store.dispatch('authentication/logout'); 244 }, 245 toggleNavigation() { 246 require('@/eventBus').default.$emit('toggle-navigation'); 247 }, 248 setFocus(event) { 249 event.preventDefault(); 250 require('@/eventBus').default.$emit('skip-navigation'); 251 }, 252 }, 253}; 254</script> 255 256<style lang="scss"> 257@mixin focus-box-shadow($padding-color: $navbar-color, $outline-color: $white) { 258 box-shadow: 259 inset 0 0 0 3px $padding-color, 260 inset 0 0 0 5px $outline-color; 261} 262.app-header { 263 .link-skip-nav { 264 position: absolute; 265 top: -60px; 266 left: 0.5rem; 267 z-index: $zindex-popover; 268 transition: $duration--moderate-01 $exit-easing--expressive; 269 &:focus { 270 top: 0.5rem; 271 transition-timing-function: $entrance-easing--expressive; 272 } 273 } 274 .navbar-text, 275 .nav-link, 276 .btn-link { 277 color: $white !important; 278 fill: currentColor; 279 padding: 0.68rem 1rem !important; 280 281 &:hover { 282 background-color: theme-color-level(light, 10); 283 } 284 &:active { 285 background-color: theme-color-level(light, 9); 286 } 287 &:focus { 288 @include focus-box-shadow; 289 outline: 0; 290 } 291 } 292 293 .nav-item { 294 fill: theme-color('light'); 295 } 296 297 .navbar { 298 padding: 0; 299 background-color: $navbar-color; 300 @include media-breakpoint-up($responsive-layout-bp) { 301 height: $header-height; 302 } 303 304 .helper-menu { 305 @include media-breakpoint-down(sm) { 306 background-color: $gray-800; 307 width: 100%; 308 justify-content: flex-end; 309 310 .nav-link, 311 .btn { 312 padding: calc(#{$spacer} / 1.125) calc(#{$spacer} / 2); 313 } 314 315 .nav-link:focus, 316 .btn:focus { 317 @include focus-box-shadow($gray-800); 318 } 319 } 320 321 .responsive-text { 322 @include media-breakpoint-down(sm) { 323 @include visually-hidden; 324 } 325 } 326 } 327 } 328 329 .navbar-nav { 330 @include media-breakpoint-up($responsive-layout-bp) { 331 padding: 0 $spacer; 332 } 333 align-items: center; 334 335 .navbar-brand, 336 .nav-link { 337 transition: $focus-transition; 338 } 339 .nav-tags { 340 color: theme-color-level(light, 3); 341 @include media-breakpoint-down(sm) { 342 @include visually-hidden; 343 } 344 .asset-tag { 345 @include media-breakpoint-down($responsive-layout-bp) { 346 @include visually-hidden; 347 } 348 } 349 } 350 } 351 352 .nav-trigger { 353 fill: theme-color('light'); 354 width: $header-height; 355 height: $header-height; 356 transition: none; 357 display: inline-flex; 358 flex: 0 0 20px; 359 align-items: center; 360 361 svg { 362 margin: 0; 363 } 364 365 &:hover { 366 fill: theme-color('light'); 367 background-color: theme-color-level(light, 10); 368 } 369 370 &.open { 371 background-color: $gray-800; 372 } 373 374 @include media-breakpoint-up($responsive-layout-bp) { 375 display: none; 376 } 377 } 378 379 .dropdown-menu { 380 margin-top: 0; 381 382 @include media-breakpoint-only(md) { 383 margin-top: 4px; 384 } 385 } 386 387 .navbar-expand { 388 @include media-breakpoint-down(sm) { 389 flex-flow: wrap; 390 } 391 } 392} 393 394.navbar-brand { 395 padding: calc(#{$spacer} / 2); 396 height: $header-height; 397 line-height: 1; 398 &:focus { 399 box-shadow: 400 inset 0 0 0 3px $navbar-color, 401 inset 0 0 0 5px $white; 402 outline: 0; 403 } 404} 405</style> 406