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