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="mr-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="pl-2 nav-tags"> 46 <span>|</span> 47 <span class="pl-3 asset-tag">{{ assetTag }}</span> 48 <span class="pl-3">{{ modelType }}</span> 49 <span class="pl-3">{{ serialNumber }}</span> 50 </div> 51 </b-navbar-nav> 52 <!-- Right aligned nav items --> 53 <b-navbar-nav class="ml-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 { useI18n } from 'vue-i18n'; 120import { mapState } from 'vuex'; 121import i18n from '@/i18n'; 122 123export default { 124 name: 'AppHeader', 125 components: { 126 IconAvatar, 127 IconClose, 128 IconMenu, 129 IconRenew, 130 StatusIcon, 131 LoadingBar, 132 }, 133 mixins: [BVToastMixin], 134 props: { 135 routerKey: { 136 type: Number, 137 default: 0, 138 }, 139 }, 140 data() { 141 return { 142 $t: useI18n().t, 143 isNavigationOpen: false, 144 altLogo: process.env.VUE_APP_COMPANY_NAME || 'Built on OpenBMC', 145 }; 146 }, 147 computed: { 148 ...mapState('authentication', ['consoleWindow']), 149 isNavTagPresent() { 150 return this.assetTag || this.modelType || this.serialNumber; 151 }, 152 assetTag() { 153 return this.$store.getters['global/assetTag']; 154 }, 155 modelType() { 156 return this.$store.getters['global/modelType']; 157 }, 158 serialNumber() { 159 return this.$store.getters['global/serialNumber']; 160 }, 161 isAuthorized() { 162 return this.$store.getters['global/isAuthorized']; 163 }, 164 userPrivilege() { 165 return this.$store.getters['global/userPrivilege']; 166 }, 167 serverStatus() { 168 return this.$store.getters['global/serverStatus']; 169 }, 170 healthStatus() { 171 return this.$store.getters['eventLog/healthStatus']; 172 }, 173 serverStatusIcon() { 174 switch (this.serverStatus) { 175 case 'on': 176 return 'success'; 177 case 'error': 178 return 'danger'; 179 case 'diagnosticMode': 180 return 'warning'; 181 case 'off': 182 default: 183 return 'secondary'; 184 } 185 }, 186 healthStatusIcon() { 187 switch (this.healthStatus) { 188 case 'OK': 189 return 'success'; 190 case 'Warning': 191 return 'warning'; 192 case 'Critical': 193 return 'danger'; 194 default: 195 return 'secondary'; 196 } 197 }, 198 username() { 199 return this.$store.getters['global/username']; 200 }, 201 }, 202 watch: { 203 consoleWindow() { 204 if (this.consoleWindow === false) this.$eventBus.$consoleWindow?.close(); 205 }, 206 isAuthorized(value) { 207 if (value === false) { 208 this.errorToast(i18n.global.t('global.toast.unAuthDescription'), { 209 title: i18n.global.t('global.toast.unAuthTitle'), 210 }); 211 } 212 }, 213 }, 214 created() { 215 // Reset auth state to check if user is authenticated based 216 // on available browser cookies 217 this.$store.dispatch('authentication/resetStoreState'); 218 this.getSystemInfo(); 219 this.getEvents(); 220 }, 221 mounted() { 222 this.$root.$on( 223 'change-is-navigation-open', 224 (isNavigationOpen) => (this.isNavigationOpen = isNavigationOpen), 225 ); 226 }, 227 methods: { 228 getSystemInfo() { 229 this.$store.dispatch('global/getSystemInfo'); 230 }, 231 getEvents() { 232 this.$store.dispatch('eventLog/getEventLogData'); 233 }, 234 refresh() { 235 this.$emit('refresh'); 236 }, 237 logout() { 238 this.$store.dispatch('authentication/logout'); 239 }, 240 toggleNavigation() { 241 this.$root.$emit('toggle-navigation'); 242 }, 243 setFocus(event) { 244 event.preventDefault(); 245 this.$root.$emit('skip-navigation'); 246 }, 247 }, 248}; 249</script> 250 251<style lang="scss"> 252@mixin focus-box-shadow($padding-color: $navbar-color, $outline-color: $white) { 253 box-shadow: 254 inset 0 0 0 3px $padding-color, 255 inset 0 0 0 5px $outline-color; 256} 257.app-header { 258 .link-skip-nav { 259 position: absolute; 260 top: -60px; 261 left: 0.5rem; 262 z-index: $zindex-popover; 263 transition: $duration--moderate-01 $exit-easing--expressive; 264 &:focus { 265 top: 0.5rem; 266 transition-timing-function: $entrance-easing--expressive; 267 } 268 } 269 .navbar-text, 270 .nav-link, 271 .btn-link { 272 color: color('white') !important; 273 fill: currentColor; 274 padding: 0.68rem 1rem !important; 275 276 &:hover { 277 background-color: theme-color-level(light, 10); 278 } 279 &:active { 280 background-color: theme-color-level(light, 9); 281 } 282 &:focus { 283 @include focus-box-shadow; 284 outline: 0; 285 } 286 } 287 288 .nav-item { 289 fill: theme-color('light'); 290 } 291 292 .navbar { 293 padding: 0; 294 background-color: $navbar-color; 295 @include media-breakpoint-up($responsive-layout-bp) { 296 height: $header-height; 297 } 298 299 .helper-menu { 300 @include media-breakpoint-down(sm) { 301 background-color: gray('800'); 302 width: 100%; 303 justify-content: flex-end; 304 305 .nav-link, 306 .btn { 307 padding: $spacer / 1.125 $spacer / 2; 308 } 309 310 .nav-link:focus, 311 .btn:focus { 312 @include focus-box-shadow($gray-800); 313 } 314 } 315 316 .responsive-text { 317 @include media-breakpoint-down(xs) { 318 @include sr-only; 319 } 320 } 321 } 322 } 323 324 .navbar-nav { 325 @include media-breakpoint-up($responsive-layout-bp) { 326 padding: 0 $spacer; 327 } 328 align-items: center; 329 330 .navbar-brand, 331 .nav-link { 332 transition: $focus-transition; 333 } 334 .nav-tags { 335 color: theme-color-level(light, 3); 336 @include media-breakpoint-down(xs) { 337 @include sr-only; 338 } 339 .asset-tag { 340 @include media-breakpoint-down($responsive-layout-bp) { 341 @include sr-only; 342 } 343 } 344 } 345 } 346 347 .nav-trigger { 348 fill: theme-color('light'); 349 width: $header-height; 350 height: $header-height; 351 transition: none; 352 display: inline-flex; 353 flex: 0 0 20px; 354 align-items: center; 355 356 svg { 357 margin: 0; 358 } 359 360 &:hover { 361 fill: theme-color('light'); 362 background-color: theme-color-level(light, 10); 363 } 364 365 &.open { 366 background-color: gray('800'); 367 } 368 369 @include media-breakpoint-up($responsive-layout-bp) { 370 display: none; 371 } 372 } 373 374 .dropdown-menu { 375 margin-top: 0; 376 377 @include media-breakpoint-only(md) { 378 margin-top: 4px; 379 } 380 } 381 382 .navbar-expand { 383 @include media-breakpoint-down(sm) { 384 flex-flow: wrap; 385 } 386 } 387} 388 389.navbar-brand { 390 padding: $spacer/2; 391 height: $header-height; 392 line-height: 1; 393 &:focus { 394 box-shadow: 395 inset 0 0 0 3px $navbar-color, 396 inset 0 0 0 5px color('white'); 397 outline: 0; 398 } 399} 400</style> 401