main
vue 160 lines 4.09 KB
Raw
1 <template>
2 <div class="flex flex-col gap-4">
3 <div class="flex flex-col">
4 <div ref="header" class="header flex items-center justify-end gap-2">
5 <div class="info flex grow gap-2">
6 <n-popover overlap placement="bottom-start">
7 <template #trigger>
8 <div class="bg-default rounded-lg">
9 <n-button size="small" class="cursor-help!">
10 <template #icon>
11 <Icon :name="InfoIcon" />
12 </template>
13 </n-button>
14 </div>
15 </template>
16 <div class="flex flex-col gap-2">
17 <div class="box">
18 Total:
19 <code>{{ total }}</code>
20 </div>
21 </div>
22 </n-popover>
23
24 <n-select
25 v-model:value="osCategory"
26 :options="osCategoryOptions"
27 clearable
28 size="small"
29 placeholder="OS Category"
30 class="max-w-32"
31 />
32 </div>
33 <n-pagination
34 v-model:page="currentPage"
35 v-model:page-size="pageSize"
36 :item-count="total"
37 :page-slot
38 :show-size-picker
39 :page-sizes
40 :simple="simpleMode"
41 />
42 </div>
43
44 <n-spin :show="loading">
45 <div class="my-3 flex min-h-28 flex-col gap-2">
46 <template v-if="list.length">
47 <TechniqueCard v-for="item of list" :key="item.technique_id" :entity="item" />
48 </template>
49 <template v-else>
50 <n-empty v-if="!loading" description="No items found" class="h-48 justify-center" />
51 </template>
52 </div>
53 </n-spin>
54 <div class="flex justify-end">
55 <n-pagination
56 v-if="list.length > 3"
57 v-model:page="currentPage"
58 :page-size
59 :item-count="total"
60 :page-slot="6"
61 />
62 </div>
63 </div>
64 </div>
65 </template>
66
67 <script setup lang="ts">
68 // TODO-FE: refactor
69 import type { MitreAtomicOsCategory, MitreAtomicTestsQuery } from "@/api/endpoints/wazuh/mitre"
70 import type { MitreAtomicTest } from "@/types/mitre.d"
71 import { useResizeObserver, watchDebounced } from "@vueuse/core"
72 import axios from "axios"
73 import { NButton, NEmpty, NPagination, NPopover, NSelect, NSpin, useMessage } from "naive-ui"
74 import { computed, ref } from "vue"
75 import Api from "@/api"
76 import Icon from "@/components/common/Icon.vue"
77 import TechniqueCard from "./TechniqueCard.vue"
78
79 const loading = ref(false)
80 const message = useMessage()
81 const list = ref<MitreAtomicTest[]>([])
82 const header = ref()
83 const currentPage = ref(1)
84 const total = ref(0)
85 const compactMode = ref(false)
86 const simpleMode = ref(false)
87 const showSizePicker = computed(() => !compactMode.value)
88 const pageSizes = [25, 50, 100, 150, 200]
89 const pageSize = ref(pageSizes[0])
90 const pageSlot = ref(8)
91 const osCategory = ref<MitreAtomicOsCategory | null>(null)
92 const InfoIcon = "carbon:information"
93
94 const osCategoryOptions: { label: string; value: MitreAtomicOsCategory }[] = ["windows", "linux", "macos"].map(o => ({
95 label: o,
96 value: o as MitreAtomicOsCategory
97 }))
98
99 let abortController: AbortController | null = null
100
101 function getList() {
102 abortController?.abort()
103 abortController = new AbortController()
104
105 loading.value = true
106
107 const query: MitreAtomicTestsQuery = {
108 size: pageSize.value,
109 page: currentPage.value,
110 os_category: osCategory.value || undefined
111 }
112
113 Api.wazuh.mitre
114 .getMitreAtomicTests(query, abortController.signal)
115 .then(res => {
116 loading.value = false
117
118 if (res.data.success) {
119 list.value = res.data?.tests || []
120 total.value = res.data?.total_techniques || 0
121 } else {
122 message.warning(res.data?.message || "An error occurred. Please try again later.")
123 }
124 })
125 .catch(err => {
126 if (!axios.isCancel(err)) {
127 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
128 loading.value = false
129 }
130 })
131 }
132
133 useResizeObserver(header, entries => {
134 const entry = entries[0]
135 if (!entry) return
136
137 const { width } = entry.contentRect
138
139 if (width < 650) {
140 compactMode.value = true
141 pageSlot.value = 5
142 } else {
143 compactMode.value = false
144 pageSlot.value = 8
145 }
146
147 simpleMode.value = width < 450
148 })
149
150 watchDebounced([currentPage, pageSize, osCategory], getList, {
151 deep: true,
152 debounce: 300,
153 immediate: true
154 })
155 // MOCK
156 /*
157 list.value = techniqueAlertsResponse.alerts
158 total.value = techniqueAlertsResponse.total_alerts
159 */
160 </script>