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