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