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