xref: /openbmc/webui-vue/src/components/AppHeader/AppHeader.vue (revision 20983592cffa5213a37f30724b101a61a7caa550)
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: inset 0 0 0 3px $padding-color, inset 0 0 0 5px $outline-color;
250}
251.app-header {
252  .link-skip-nav {
253    position: absolute;
254    top: -60px;
255    left: 0.5rem;
256    z-index: $zindex-popover;
257    transition: $duration--moderate-01 $exit-easing--expressive;
258    &:focus {
259      top: 0.5rem;
260      transition-timing-function: $entrance-easing--expressive;
261    }
262  }
263  .navbar-text,
264  .nav-link,
265  .btn-link {
266    color: color('white') !important;
267    fill: currentColor;
268    padding: 0.68rem 1rem !important;
269
270    &:hover {
271      background-color: theme-color-level(light, 10);
272    }
273    &:active {
274      background-color: theme-color-level(light, 9);
275    }
276    &:focus {
277      @include focus-box-shadow;
278      outline: 0;
279    }
280  }
281
282  .nav-item {
283    fill: theme-color('light');
284  }
285
286  .navbar {
287    padding: 0;
288    background-color: $navbar-color;
289    @include media-breakpoint-up($responsive-layout-bp) {
290      height: $header-height;
291    }
292
293    .helper-menu {
294      @include media-breakpoint-down(sm) {
295        background-color: gray('800');
296        width: 100%;
297        justify-content: flex-end;
298
299        .nav-link,
300        .btn {
301          padding: $spacer / 1.125 $spacer / 2;
302        }
303
304        .nav-link:focus,
305        .btn:focus {
306          @include focus-box-shadow($gray-800);
307        }
308      }
309
310      .responsive-text {
311        @include media-breakpoint-down(xs) {
312          @include sr-only;
313        }
314      }
315    }
316  }
317
318  .navbar-nav {
319    @include media-breakpoint-up($responsive-layout-bp) {
320      padding: 0 $spacer;
321    }
322    align-items: center;
323
324    .navbar-brand,
325    .nav-link {
326      transition: $focus-transition;
327    }
328    .nav-tags {
329      color: theme-color-level(light, 3);
330      @include media-breakpoint-down(xs) {
331        @include sr-only;
332      }
333      .asset-tag {
334        @include media-breakpoint-down($responsive-layout-bp) {
335          @include sr-only;
336        }
337      }
338    }
339  }
340
341  .nav-trigger {
342    fill: theme-color('light');
343    width: $header-height;
344    height: $header-height;
345    transition: none;
346    display: inline-flex;
347    flex: 0 0 20px;
348    align-items: center;
349
350    svg {
351      margin: 0;
352    }
353
354    &:hover {
355      fill: theme-color('light');
356      background-color: theme-color-level(light, 10);
357    }
358
359    &.open {
360      background-color: gray('800');
361    }
362
363    @include media-breakpoint-up($responsive-layout-bp) {
364      display: none;
365    }
366  }
367
368  .dropdown-menu {
369    margin-top: 0;
370
371    @include media-breakpoint-only(md) {
372      margin-top: 4px;
373    }
374  }
375
376  .navbar-expand {
377    @include media-breakpoint-down(sm) {
378      flex-flow: wrap;
379    }
380  }
381}
382
383.navbar-brand {
384  padding: $spacer/2;
385  height: $header-height;
386  line-height: 1;
387  &:focus {
388    box-shadow: inset 0 0 0 3px $navbar-color, inset 0 0 0 5px color('white');
389    outline: 0;
390  }
391}
392</style>
393