xref: /openbmc/webui-vue/src/components/AppHeader/AppHeader.vue (revision 313d15cf9f14e8a264f97a3370c3be4e494e5e0d)
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