xref: /openbmc/webui-vue/src/components/AppNavigation/AppNavigation.vue (revision 24b377db47a05f742b1f3db77606f6bae845f637)
1<template>
2  <div>
3    <div class="nav-container" :class="{ open: isNavigationOpen }">
4      <nav ref="nav" :aria-label="$t('appNavigation.primaryNavigation')">
5        <b-nav vertical class="mb-4">
6          <template v-for="navItem in navigationItems">
7            <!-- Navigation items with no children -->
8            <b-nav-item
9              v-if="!navItem.children"
10              :key="navItem.index"
11              :to="navItem.route"
12              :data-test-id="`nav-item-${navItem.id}`"
13            >
14              <component :is="navItem.icon" />
15              {{ navItem.label }}
16            </b-nav-item>
17
18            <!-- Navigation items with children -->
19            <li v-else :key="navItem.index" class="nav-item">
20              <b-button
21                v-b-toggle="`${navItem.id}`"
22                variant="link"
23                :data-test-id="`nav-button-${navItem.id}`"
24              >
25                <component :is="navItem.icon" />
26                {{ navItem.label }}
27                <icon-expand class="icon-expand" />
28              </b-button>
29              <b-collapse :id="navItem.id" tag="ul" class="nav-item__nav">
30                <li class="nav-item">
31                  <router-link
32                    v-for="(subNavItem, i) of filteredNavItem(navItem.children)"
33                    :key="i"
34                    :to="subNavItem.route"
35                    :data-test-id="`nav-item-${subNavItem.id}`"
36                    class="nav-link"
37                  >
38                    {{ subNavItem.label }}
39                  </router-link>
40                </li>
41              </b-collapse>
42            </li>
43          </template>
44        </b-nav>
45      </nav>
46    </div>
47    <transition name="fade">
48      <div
49        v-if="isNavigationOpen"
50        id="nav-overlay"
51        class="nav-overlay"
52        @click="toggleIsOpen"
53      ></div>
54    </transition>
55  </div>
56</template>
57
58<script>
59//Do not change Mixin import.
60//Exact match alias set to support
61//dotenv customizations.
62import AppNavigationMixin from './AppNavigationMixin';
63import { useI18n } from 'vue-i18n';
64
65export default {
66  name: 'AppNavigation',
67  mixins: [AppNavigationMixin],
68  data() {
69    return {
70      $t: useI18n().t,
71      isNavigationOpen: false,
72      currentUserRole: null,
73    };
74  },
75  watch: {
76    $route: function () {
77      this.isNavigationOpen = false;
78    },
79    isNavigationOpen: function (isNavigationOpen) {
80      this.$root.$emit('change-is-navigation-open', isNavigationOpen);
81    },
82  },
83  mounted() {
84    this.getPrivilege();
85    this.$root.$on('toggle-navigation', () => this.toggleIsOpen());
86  },
87  methods: {
88    toggleIsOpen() {
89      this.isNavigationOpen = !this.isNavigationOpen;
90    },
91    getPrivilege() {
92      this.currentUserRole = this.$store?.getters['global/userPrivilege'];
93    },
94    filteredNavItem(navItem) {
95      if (this.currentUserRole) {
96        return navItem.filter(({ exclusiveToRoles }) => {
97          if (!exclusiveToRoles?.length) return true;
98          return exclusiveToRoles.includes(this.currentUserRole);
99        });
100      } else return navItem;
101    },
102  },
103};
104</script>
105
106<style scoped lang="scss">
107svg {
108  fill: currentColor;
109  height: 1.2rem;
110  width: 1.2rem;
111  margin-left: 0 !important; //!important overriding button specificity
112  vertical-align: text-bottom;
113  &:not(.icon-expand) {
114    margin-right: $spacer;
115  }
116}
117
118.nav {
119  padding-top: $spacer / 4;
120  @include media-breakpoint-up($responsive-layout-bp) {
121    padding-top: $spacer;
122  }
123}
124
125.nav-item__nav {
126  list-style: none;
127  padding-left: 0;
128  margin-left: 0;
129
130  .nav-item {
131    outline: none;
132  }
133
134  .nav-link {
135    padding-left: $spacer * 4;
136    outline: none;
137
138    &:not(.nav-link--current) {
139      font-weight: normal;
140    }
141  }
142}
143
144.btn-link {
145  display: inline-block;
146  width: 100%;
147  text-align: left;
148  text-decoration: none !important;
149  border-radius: 0;
150
151  &.collapsed {
152    .icon-expand {
153      transform: rotate(180deg);
154    }
155  }
156}
157
158.icon-expand {
159  float: right;
160  margin-top: $spacer / 4;
161}
162
163.btn-link,
164.nav-link {
165  position: relative;
166  font-weight: $headings-font-weight;
167  padding-left: $spacer; // defining consistent padding for links and buttons
168  padding-right: $spacer;
169  color: theme-color('secondary');
170
171  &:hover {
172    background-color: theme-color-level(dark, -10.5);
173    color: theme-color('dark');
174  }
175
176  &:focus {
177    background-color: theme-color-level(light, 0);
178    box-shadow: inset 0 0 0 2px theme-color('primary');
179    color: theme-color('dark');
180    outline: 0;
181  }
182
183  &:active {
184    background-color: theme-color('secondary');
185    color: $white;
186  }
187}
188
189.nav-link--current {
190  font-weight: $headings-font-weight;
191  background-color: theme-color('secondary');
192  color: theme-color('light');
193  cursor: default;
194  box-shadow: none;
195
196  &::before {
197    content: '';
198    position: absolute;
199    top: 0;
200    bottom: 0;
201    left: 0;
202    width: 4px;
203    background-color: theme-color('primary');
204  }
205
206  &:hover,
207  &:focus {
208    background-color: theme-color('secondary');
209    color: theme-color('light');
210  }
211}
212
213.nav-container {
214  position: fixed;
215  width: $navigation-width;
216  top: $header-height;
217  bottom: 0;
218  left: 0;
219  z-index: $zindex-fixed;
220  overflow-y: auto;
221  background-color: theme-color('light');
222  transform: translateX(-$navigation-width);
223  transition: transform $exit-easing--productive $duration--moderate-02;
224  border-right: 1px solid theme-color-level('light', 2.85);
225
226  @include media-breakpoint-down(md) {
227    z-index: $zindex-fixed + 2;
228  }
229
230  &.open,
231  &:focus-within {
232    transform: translateX(0);
233    transition-timing-function: $entrance-easing--productive;
234  }
235
236  @include media-breakpoint-up($responsive-layout-bp) {
237    transition-duration: $duration--fast-01;
238    transform: translateX(0);
239  }
240}
241
242.nav-overlay {
243  position: fixed;
244  top: $header-height;
245  bottom: 0;
246  left: 0;
247  right: 0;
248  z-index: $zindex-fixed + 1;
249  background-color: $black;
250  opacity: 0.5;
251
252  &.fade-enter-active {
253    transition: opacity $duration--moderate-02 $entrance-easing--productive;
254  }
255
256  &.fade-leave-active {
257    transition: opacity $duration--fast-02 $exit-easing--productive;
258  }
259
260  &.fade-enter, // Remove this vue2 based only class when switching to vue3
261  &.fade-enter-from, // This is vue3 based only class modified from 'fade-enter'
262  &.fade-leave-to {
263    opacity: 0;
264  }
265
266  @include media-breakpoint-up($responsive-layout-bp) {
267    display: none;
268  }
269}
270</style>
271