main
vue 224 lines 6.5 KB
Raw
1 <template>
2 <div class="flex flex-col gap-4">
3 <EventSearchFilters
4 v-model:query="query"
5 :loading-events
6 @search="handleFiltersSearch"
7 @loaded="handleFiltersLoaded"
8 @field-mappings="fieldMappings = $event"
9 @field-mappings-loading="fieldMappingsLoading = $event"
10 />
11
12 <EventSearchResults
13 :events
14 :total-events
15 :loading-events
16 :loading-more
17 :has-searched
18 :scroll-id
19 :event-source="selectedEventSource"
20 @load-more="loadMoreEvents"
21 @configure-columns="showColumnConfig = true"
22 @row-select="openEventDetail"
23 />
24
25 <n-drawer
26 v-model:show="showDetailDrawer"
27 display-directive="show"
28 :trap-focus="false"
29 :style="{ width: 'min(600px, 90vw)' }"
30 >
31 <n-drawer-content title="Event Details" closable :native-scrollbar="false">
32 <EventDetail
33 :event="selectedEvent"
34 @filter-add="addFilterFromDetail"
35 @filter-exclude="excludeFilterFromDetail"
36 />
37 </n-drawer-content>
38 </n-drawer>
39
40 <n-modal
41 v-model:show="showColumnConfig"
42 preset="card"
43 display-directive="show"
44 :title="`Configure columns: ${selectedEventSource?.name ?? ''}`"
45 :style="{ width: 'min(720px, 92vw)' }"
46 :mask-closable="false"
47 >
48 <ColumnConfig
49 ref="columnConfigRef"
50 :open="showColumnConfig"
51 :event-source="selectedEventSource"
52 :field-mappings
53 :loading-field-mappings="fieldMappingsLoading"
54 @close="showColumnConfig = false"
55 @saved="onColumnsSaved"
56 />
57
58 <template #footer>
59 <div class="flex items-center justify-between gap-2">
60 <n-button text @click="columnConfigRef?.resetToDefaults()">Reset to defaults</n-button>
61 <div class="flex gap-2">
62 <n-button @click="showColumnConfig = false">Cancel</n-button>
63 <n-button type="primary" :loading="columnConfigRef?.saving" @click="columnConfigRef?.onSave()">
64 Save
65 </n-button>
66 </div>
67 </div>
68 </template>
69 </n-modal>
70 </div>
71 </template>
72
73 <script setup lang="ts">
74 import type { EventSearchFiltersLoad, EventSearchFiltersParams } from "./EventSearchFilters.vue"
75 import type { EventSearchResult, FieldMapping } from "@/types/events.d"
76 import type { DisplayColumn } from "@/types/eventSources.d"
77 import { NButton, NDrawer, NDrawerContent, NModal, useMessage } from "naive-ui"
78 import { computed, ref, useTemplateRef } from "vue"
79 import Api from "@/api"
80 import ColumnConfig from "./ColumnConfig.vue"
81 import EventDetail from "./EventDetail.vue"
82 import EventSearchFilters from "./EventSearchFilters.vue"
83 import EventSearchResults from "./EventSearchResults.vue"
84
85 const message = useMessage()
86
87 const searchFormParams = ref<EventSearchFiltersParams | null>(null)
88 const searchFormLoad = ref<EventSearchFiltersLoad | null>(null)
89 const query = ref("")
90 const fieldMappings = ref<FieldMapping[]>([])
91 const fieldMappingsLoading = ref(false)
92
93 const events = ref<EventSearchResult[]>([])
94 const totalEvents = ref(0)
95 const scrollId = ref<string | null>(null)
96 const loadingEvents = ref(false)
97 const loadingMore = ref(false)
98 const hasSearched = ref(false)
99
100 const showColumnConfig = ref(false)
101 const showDetailDrawer = ref(false)
102 const selectedEvent = ref<EventSearchResult | null>(null)
103 const columnConfigRef = useTemplateRef("columnConfigRef")
104
105 const selectedEventSource = computed(() => {
106 const sourceName = searchFormParams.value?.sourceName
107 const sources = searchFormLoad.value?.eventSources
108 if (!sourceName || !sources) return null
109 return sources.find(s => s.name === sourceName) ?? null
110 })
111
112 function handleFiltersSearch(params: EventSearchFiltersParams) {
113 searchFormParams.value = params
114 fieldMappings.value = params.fieldMappings
115 searchEvents()
116 }
117
118 function handleFiltersLoaded(load: EventSearchFiltersLoad) {
119 searchFormLoad.value = load
120 }
121
122 function resetResults() {
123 events.value = []
124 totalEvents.value = 0
125 scrollId.value = null
126 hasSearched.value = false
127 }
128
129 function searchEvents() {
130 const params = searchFormParams.value
131 if (!params?.customerCode || !params.sourceName) return
132
133 loadingEvents.value = true
134 hasSearched.value = true
135 resetResults()
136
137 const apiParams: { timerange?: string; page_size: number; query?: string; time_from?: string; time_to?: string } = {
138 page_size: params.pageSize,
139 query: params.query || undefined
140 }
141 if (params.timeMode === "absolute" && params.timeFrom && params.timeTo) {
142 apiParams.time_from = new Date(params.timeFrom).toISOString()
143 apiParams.time_to = new Date(params.timeTo).toISOString()
144 } else {
145 apiParams.timerange = params.timerange
146 }
147
148 Api.siem
149 .queryEvents(params.customerCode, params.sourceName, apiParams)
150 .then(res => {
151 if (res.data.success) {
152 events.value = res.data.events || []
153 totalEvents.value = res.data.total
154 scrollId.value = res.data.scroll_id
155 } else {
156 message.warning(res.data?.message || "An error occurred. Please try again later.")
157 }
158 })
159 .catch(err => {
160 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
161 })
162 .finally(() => {
163 loadingEvents.value = false
164 })
165 }
166
167 function loadMoreEvents() {
168 const params = searchFormParams.value
169 if (!params?.customerCode || !params.sourceName || !scrollId.value) return
170
171 loadingMore.value = true
172
173 Api.siem
174 .queryEvents(params.customerCode, params.sourceName, {
175 scroll_id: scrollId.value
176 })
177 .then(res => {
178 if (res.data.success) {
179 events.value.push(...(res.data.events || []))
180 scrollId.value = res.data.scroll_id
181 } else {
182 message.warning(res.data?.message || "An error occurred. Please try again later.")
183 }
184 })
185 .catch(err => {
186 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
187 })
188 .finally(() => {
189 loadingMore.value = false
190 })
191 }
192
193 function openEventDetail(event: EventSearchResult) {
194 selectedEvent.value = event
195 showDetailDrawer.value = true
196 }
197
198 function onColumnsSaved(saved: DisplayColumn[] | null) {
199 const source = selectedEventSource.value
200 if (source) {
201 source.displayed_columns = saved
202 }
203 }
204
205 function addFilterFromDetail(field: string, value: string) {
206 const filterExpr = `${field}:"${value}"`
207 query.value = query.value ? `${query.value} AND ${filterExpr}` : filterExpr
208 if (searchFormParams.value) {
209 searchFormParams.value = { ...searchFormParams.value, query: query.value }
210 }
211 showDetailDrawer.value = false
212 searchEvents()
213 }
214
215 function excludeFilterFromDetail(field: string, value: string) {
216 const filterExpr = `NOT ${field}:"${value}"`
217 query.value = query.value ? `${query.value} AND ${filterExpr}` : filterExpr
218 if (searchFormParams.value) {
219 searchFormParams.value = { ...searchFormParams.value, query: query.value }
220 }
221 showDetailDrawer.value = false
222 searchEvents()
223 }
224 </script>