main
vue 201 lines 5.36 KB
Raw
1 <template>
2 <div class="flex flex-col gap-4">
3 <n-alert type="info">
4 SCA Overview provides real-time Security Configuration Assessment results from Wazuh Manager across all
5 agents with comprehensive compliance scoring.
6 </n-alert>
7
8 <ScaStats
9 :filters
10 class="my-8"
11 @update:min_score="selectMinScore"
12 @update:max_score="selectMaxScore"
13 @update:policy_id="selectPolicyID"
14 />
15
16 <div ref="header" class="flex items-center justify-end gap-2">
17 <n-pagination
18 v-model:page="currentPage"
19 v-model:page-size="pageSize"
20 :page-slot
21 :page-sizes
22 :item-count="totalCount"
23 :show-size-picker
24 />
25 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
26 <n-button size="small" secondary @click="showFiltersView = !showFiltersView">
27 <template #icon>
28 <Icon :name="FilterIcon" />
29 </template>
30 </n-button>
31 </n-badge>
32 </div>
33
34 <CollapseKeepAlive :show="showFiltersView" embedded arrow="top-right">
35 <ListFilters class="p-3" @submit="applyFilters" @mounted="filtersCTX = $event" />
36 </CollapseKeepAlive>
37
38 <!-- SCA Results List -->
39 <n-spin :show="loading">
40 <div class="my-3">
41 <div v-if="list.length" class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
42 <ScaCard v-for="item of list" :key="JSON.stringify(item)" :sca="item" />
43 </div>
44 <template v-else>
45 <n-empty v-if="!loading" description="No SCA results found" class="h-48 justify-center" />
46 </template>
47 </div>
48 </n-spin>
49
50 <div v-if="list.length >= 9" class="flex justify-end">
51 <n-pagination
52 v-model:page="currentPage"
53 v-model:page-size="pageSize"
54 :page-slot
55 :page-sizes
56 :item-count="totalCount"
57 :show-size-picker
58 />
59 </div>
60 </div>
61 </template>
62
63 <script setup lang="ts">
64 // TODO-FE: refactor
65 import type { ScaOverviewFilter, ScaOverviewFilterTypes } from "./types.d"
66 import type { AgentScaOverviewItem, ScaOverviewQuery } from "@/types/sca.d"
67 import { useResizeObserver, useStorage, watchDebounced } from "@vueuse/core"
68 import axios from "axios"
69 import _set from "lodash/set"
70 import _toNumber from "lodash/toSafeInteger"
71 import { NAlert, NBadge, NButton, NEmpty, NPagination, NSpin, useMessage } from "naive-ui"
72 import { computed, ref } from "vue"
73 import Api from "@/api"
74 import CollapseKeepAlive from "@/components/common/CollapseKeepAlive.vue"
75 import Icon from "@/components/common/Icon.vue"
76 import ListFilters from "./ListFilters.vue"
77 import ScaCard from "./ScaCard.vue"
78 import ScaStats from "./ScaStats.vue"
79
80 const loading = ref(false)
81 const message = useMessage()
82 const list = ref<AgentScaOverviewItem[]>([])
83 const pageSizes = [10, 25, 50, 100]
84 const pageSize = ref(pageSizes[1])
85 const pageSlot = ref(8)
86 const showSizePicker = ref(true)
87 const header = ref()
88 const showFiltersView = useStorage<boolean>("agents-sca-list-filters-view-state", false, localStorage)
89
90 const filtersCTX = ref<{ setFilter: (payload: ScaOverviewFilter[]) => void } | null>(null)
91 const filters = ref<ScaOverviewFilter[]>([])
92
93 const filtered = computed<boolean>(() => {
94 return !!filters.value.length
95 })
96
97 const totalCount = ref(0)
98 const currentPage = ref(1)
99
100 const FilterIcon = "carbon:filter-edit"
101
102 let abortController: AbortController | null = null
103
104 function getList() {
105 abortController?.abort()
106 abortController = new AbortController()
107
108 loading.value = true
109
110 const query: ScaOverviewQuery = {
111 page: currentPage.value,
112 page_size: pageSize.value
113 }
114
115 for (const key of ["customer_code", "policy_id", "policy_name", "agent_name"] as ScaOverviewFilterTypes[]) {
116 if (filters.value.find(o => o.type === key)?.value) {
117 _set(query, key, `${filters.value.find(o => o.type === key)?.value}`)
118 }
119 }
120 for (const key of ["min_score", "max_score"] as ScaOverviewFilterTypes[]) {
121 if (filters.value.find(o => o.type === key)?.value) {
122 _set(query, key, _toNumber(filters.value.find(o => o.type === key)?.value))
123 }
124 }
125
126 Api.sca
127 .searchScaOverview(query, abortController.signal)
128 .then(res => {
129 loading.value = false
130
131 if (res.data.success) {
132 list.value = res.data?.sca_results || []
133 totalCount.value = res.data?.total_count || 0
134 currentPage.value = res.data?.page || 1
135 } else {
136 message.warning(res.data?.message || "An error occurred. Please try again later.")
137 }
138 })
139 .catch(err => {
140 if (!axios.isCancel(err)) {
141 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
142 loading.value = false
143 }
144 })
145 }
146
147 function selectPolicyID(value: string) {
148 showFiltersView.value = true
149 filtersCTX.value?.setFilter([{ type: "policy_id", value }])
150 }
151
152 function selectMinScore(value: number) {
153 showFiltersView.value = true
154 filtersCTX.value?.setFilter([{ type: "min_score", value }])
155 }
156
157 function selectMaxScore(value: number) {
158 showFiltersView.value = true
159 filtersCTX.value?.setFilter([{ type: "max_score", value }])
160 }
161
162 function applyFilters(newFilters: ScaOverviewFilter[]) {
163 filters.value = newFilters
164 }
165
166 watchDebounced(
167 currentPage,
168 () => {
169 getList()
170 },
171 { debounce: 300 }
172 )
173
174 watchDebounced(
175 pageSize,
176 () => {
177 currentPage.value = 1
178 getList()
179 },
180 { debounce: 300 }
181 )
182
183 watchDebounced(
184 filters,
185 () => {
186 currentPage.value = 1
187 getList()
188 },
189 { deep: true, debounce: 300, immediate: true }
190 )
191
192 useResizeObserver(header, entries => {
193 const entry = entries[0]
194 if (!entry) return
195
196 const { width } = entry.contentRect
197
198 pageSlot.value = width < 700 ? 5 : 8
199 showSizePicker.value = width > 550
200 })
201 </script>