main
vue 204 lines 4.73 KB
Raw
1 <template>
2 <nav class="nav" :class="{ collapsed }">
3 <n-menu
4 ref="menu"
5 v-model:value="selectedKey"
6 :options="menuOptions"
7 :collapsed
8 :indent="18"
9 accordion
10 :collapsed-width
11 :dropdown-props="{
12 scrollable: true,
13 menuProps: () => ({
14 class: 'main-nav'
15 })
16 }"
17 :expanded-keys
18 @update:expanded-keys="handleUpdateExpandedKeys"
19 />
20 </nav>
21
22 <div class="hidden">
23 <StackProvisioningButton ref="stackProvisioningButton" />
24 <ActiveResponseWizardButton ref="activeResponseWizardButton" />
25 <ThreatIntelButton ref="threatIntelButton" />
26 </div>
27 </template>
28
29 <script lang="ts" setup>
30 import type { MenuInst } from "naive-ui"
31 import type { MenuMixedOption } from "naive-ui/es/menu/src/interface"
32 import type { RouteRecordNormalized } from "vue-router"
33 import _uniq from "lodash/uniq"
34 import { NMenu } from "naive-ui"
35 import { computed, onBeforeMount, ref, watch } from "vue"
36 import { useRoute, useRouter } from "vue-router"
37 import ActiveResponseWizardButton from "@/components/activeResponse/ActiveResponseWizardButton.vue"
38 import StackProvisioningButton from "@/components/stackProvisioning/StackProvisioningButton.vue"
39 import ThreatIntelButton from "@/components/threatIntel/ThreatIntelButton.vue"
40 import { useThemeStore } from "@/stores/theme"
41
42 import getItems from "./items"
43
44 const { collapsed = false } = defineProps<{
45 collapsed?: boolean
46 }>()
47
48 const route = useRoute()
49 const router = useRouter()
50 const selectedKey = ref<string | null>(null)
51 const menu = ref<MenuInst | null>(null)
52 const expandedKeys = ref<string[] | undefined>(undefined)
53
54 const themeStore = useThemeStore()
55 const menuOptions = computed<MenuMixedOption[]>(() => getItems())
56 const collapsedWidth = computed<number>(() => themeStore.sidebar.closeWidth)
57 const sidebarCollapsed = computed<boolean>(() => themeStore.sidebar.collapsed)
58
59 const stackProvisioningButton = ref<InstanceType<typeof StackProvisioningButton> | null>(null)
60 const threatIntelButton = ref<InstanceType<typeof ThreatIntelButton> | null>(null)
61 const activeResponseWizardButton = ref<InstanceType<typeof ActiveResponseWizardButton> | null>(null)
62
63 watch(selectedKey, val => {
64 handleMenuSelect(val)
65 })
66
67 function handleMenuSelect(key: string | null) {
68 switch (key) {
69 case "Tools-ThreatIntel":
70 threatIntelButton.value?.openDrawer()
71 break
72 case "Tools-StackProvisioning":
73 stackProvisioningButton.value?.openModal()
74 break
75 case "Tools-ActiveResponse":
76 activeResponseWizardButton.value?.openModal()
77 break
78 }
79 selectedKey.value = key
80 }
81
82 function setMenuKey(matched: RouteRecordNormalized[]) {
83 for (const match of matched) {
84 if (match.name && typeof match.name === "string") {
85 selectedKey.value = match.name?.toString() || null
86 if (selectedKey.value) {
87 menu.value?.showOption(selectedKey.value)
88 }
89 }
90 }
91 }
92
93 onBeforeMount(() => {
94 setMenuKey(route.matched)
95
96 router.afterEach(route => {
97 if (route?.matched?.length) {
98 setMenuKey(route.matched)
99
100 if (window.innerWidth <= 700 && !sidebarCollapsed.value) {
101 themeStore.closeSidebar()
102 }
103 }
104 })
105 })
106
107 function handleUpdateExpandedKeys(value: string[]) {
108 const submenu = "components"
109
110 if (value?.length && value.includes(submenu)) {
111 const lastKey = value.pop()
112 if (lastKey) {
113 expandedKeys.value = _uniq([submenu, lastKey])
114 }
115 } else {
116 expandedKeys.value = undefined
117 }
118 }
119 </script>
120
121 <style lang="scss" scoped>
122 .nav {
123 &.collapsed {
124 pointer-events: none;
125 }
126
127 :deep() {
128 .n-menu-item-group {
129 .n-menu-item-group-title {
130 white-space: nowrap;
131 overflow: hidden;
132 text-overflow: ellipsis;
133 }
134 }
135
136 .n-submenu-children {
137 --dash-width: 8px;
138 --dash-height: 2px;
139 --dash-offset: 29px;
140
141 position: relative;
142
143 &::before {
144 content: "";
145 display: block;
146 background-color: var(--border-color);
147 width: var(--dash-height);
148 position: absolute;
149 top: 0px;
150 bottom: 20px;
151 left: var(--dash-offset);
152 }
153
154 .n-menu-item-content {
155 &::after {
156 content: "";
157 display: block;
158 background-color: var(--border-color);
159 width: var(--dash-width);
160 height: var(--dash-height);
161 position: absolute;
162 z-index: -1;
163 top: calc(50% - calc(ar(--dash-height) / 2));
164 left: calc(var(--dash-offset) + var(--dash-height));
165 }
166 }
167
168 .n-menu-item-group {
169 .n-menu-item-group-title {
170 padding-left: 44px !important;
171 }
172 }
173
174 .n-submenu-children {
175 &::before {
176 display: none;
177 }
178 .n-menu-item-content {
179 &::after {
180 width: calc(var(--dash-width) * 3);
181 background: repeating-linear-gradient(
182 90deg,
183 var(--border-color) 0px,
184 var(--border-color) 5px,
185 transparent 5px,
186 transparent 8px
187 );
188 }
189 }
190 }
191 }
192 }
193 }
194
195 .direction-rtl {
196 .nav {
197 :deep() {
198 .n-submenu-children {
199 --dash-offset: 25px;
200 }
201 }
202 }
203 }
204 </style>