xref: /openbmc/webui-vue/src/components/AppNavigation/AppNavigation.vue (revision 7d6b44cb263da09e575c7cb28cab88c1eb339c7b)
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';
63
64export default {
65  name: 'AppNavigation',
66  mixins: [AppNavigationMixin],
67  data() {
68    return {
69      isNavigationOpen: false,
70      currentUserRole: null,
71    };
72  },
73  watch: {
74    $route: function () {
75      this.isNavigationOpen = false;
76    },
77    isNavigationOpen: function (isNavigationOpen) {
78      this.$root.$emit('change-is-navigation-open', isNavigationOpen);
79    },
80  },
81  mounted() {
82    this.getPrivilege();
83    this.$root.$on('toggle-navigation', () => this.toggleIsOpen());
84  },
85  methods: {
86    toggleIsOpen() {
87      this.isNavigationOpen = !this.isNavigationOpen;
88    },
89    getPrivilege() {
90      this.currentUserRole = this.$store?.getters['global/userPrivilege'];
91    },
92    filteredNavItem(navItem) {
93      if (this.currentUserRole) {
94        return navItem.filter(({ exclusiveToRoles }) => {
95          if (!exclusiveToRoles?.length) return true;
96          return exclusiveToRoles.includes(this.currentUserRole);
97        });
98      } else return navItem;
99    },
100  },
101};
102</script>
103
104<style lang="scss" scoped>
105@import '@/assets/styles/bmc/helpers/_index.scss';
106@import '@/assets/styles/bootstrap/_helpers.scss';
107
108svg {
109  fill: currentColor;
110  height: 1.2rem;
111  width: 1.2rem;
112  margin-left: 0 !important; //!important overriding button specificity
113  vertical-align: text-bottom;
114  &:not(.icon-expand) {
115    margin-right: $spacer;
116  }
117}
118
119.nav {
120  padding-top: $spacer / 4;
121  @include media-breakpoint-up($responsive-layout-bp) {
122    padding-top: $spacer;
123  }
124}
125
126.nav-item__nav {
127  list-style: none;
128  padding-left: 0;
129  margin-left: 0;
130
131  .nav-item {
132    outline: none;
133  }
134
135  .nav-link {
136    padding-left: $spacer * 4;
137    outline: none;
138
139    &:not(.nav-link--current) {
140      font-weight: normal;
141    }
142  }
143}
144
145.btn-link {
146  display: inline-block;
147  width: 100%;
148  text-align: left;
149  text-decoration: none !important;
150  border-radius: 0;
151
152  &.collapsed {
153    .icon-expand {
154      transform: rotate(180deg);
155    }
156  }
157}
158
159.icon-expand {
160  float: right;
161  margin-top: $spacer / 4;
162}
163
164.btn-link,
165.nav-link {
166  position: relative;
167  font-weight: $headings-font-weight;
168  padding-left: $spacer; // defining consistent padding for links and buttons
169  padding-right: $spacer;
170  color: theme-color('secondary');
171
172  &:hover {
173    background-color: theme-color-level(dark, -10.5);
174    color: theme-color('dark');
175  }
176
177  &:focus {
178    background-color: theme-color-level(light, 0);
179    box-shadow: inset 0 0 0 2px theme-color('primary');
180    color: theme-color('dark');
181    outline: 0;
182  }
183
184  &:active {
185    background-color: theme-color('secondary');
186    color: $white;
187  }
188}
189
190.nav-link--current {
191  font-weight: $headings-font-weight;
192  background-color: theme-color('secondary');
193  color: theme-color('light');
194  cursor: default;
195  box-shadow: none;
196
197  &::before {
198    content: '';
199    position: absolute;
200    top: 0;
201    bottom: 0;
202    left: 0;
203    width: 4px;
204    background-color: theme-color('primary');
205  }
206
207  &:hover,
208  &:focus {
209    background-color: theme-color('secondary');
210    color: theme-color('light');
211  }
212}
213
214.nav-container {
215  position: fixed;
216  width: $navigation-width;
217  top: $header-height;
218  bottom: 0;
219  left: 0;
220  z-index: $zindex-fixed;
221  overflow-y: auto;
222  background-color: theme-color('light');
223  transform: translateX(-$navigation-width);
224  transition: transform $exit-easing--productive $duration--moderate-02;
225  border-right: 1px solid theme-color-level('light', 2.85);
226
227  @include media-breakpoint-down(md) {
228    z-index: $zindex-fixed + 2;
229  }
230
231  &.open,
232  &:focus-within {
233    transform: translateX(0);
234    transition-timing-function: $entrance-easing--productive;
235  }
236
237  @include media-breakpoint-up($responsive-layout-bp) {
238    transition-duration: $duration--fast-01;
239    transform: translateX(0);
240  }
241}
242
243.nav-overlay {
244  position: fixed;
245  top: $header-height;
246  bottom: 0;
247  left: 0;
248  right: 0;
249  z-index: $zindex-fixed + 1;
250  background-color: $black;
251  opacity: 0.5;
252
253  &.fade-enter-active {
254    transition: opacity $duration--moderate-02 $entrance-easing--productive;
255  }
256
257  &.fade-leave-active {
258    transition: opacity $duration--fast-02 $exit-easing--productive;
259  }
260
261  &.fade-enter, // Remove this vue2 based only class when switching to vue3
262  &.fade-enter-from, // This is vue3 based only class modified from 'fade-enter'
263  &.fade-leave-to {
264    opacity: 0;
265  }
266
267  @include media-breakpoint-up($responsive-layout-bp) {
268    display: none;
269  }
270}
271</style>
272