main
vue 397 lines 9.71 KB
Raw
1 <template>
2 <div class="soc-alerts-list">
3 <div ref="header" class="flex h-14 items-center justify-end gap-2">
4 <slot name="header"></slot>
5 <div class="grow">
6 <n-input v-model:value="alertTitle" size="small" placeholder="Search by title..." clearable />
7 </div>
8 <div class="delete-box">
9 <n-popover v-if="checkedCount" :width="200" placement="bottom" style="max-height: 240px" scrollable>
10 <template #trigger>
11 <n-button
12 size="small"
13 type="error"
14 ghost
15 :loading="loadingPurge"
16 :disabled="loadingAlerts"
17 @click="handleDelete()"
18 >
19 <div class="flex items-center gap-2">
20 <Icon :name="TrashIcon" :size="16" />
21 <span class="flex items-center gap-2">
22 <span v-if="!compactMode">Delete Alerts:</span>
23 <span class="font-mono">
24 {{ checkedCount }}
25 </span>
26 </span>
27 </div>
28 </n-button>
29 </template>
30 <template #header>Selected Alerts</template>
31 <template #footer>
32 <div class="flex justify-end">
33 <n-button size="tiny" @click="clearChecked()">Clear selection</n-button>
34 </div>
35 </template>
36 <div class="checked-list flex flex-col gap-2">
37 <div v-for="alertId of checkedList" :key="alertId" class="w-full">
38 <n-button size="small" class="w-full! justify-start!" @click="clearChecked(alertId)">
39 <template #icon>
40 <Icon :name="CloseIcon" :size="18" />
41 </template>
42 <span class="font-mono">#{{ alertId }}</span>
43 </n-button>
44 </div>
45 </div>
46 </n-popover>
47 <n-button v-else size="small" type="error" ghost :loading="loadingPurge" @click="handlePurge()">
48 <div class="flex items-center gap-2">
49 <Icon :name="TrashIcon" :size="16" />
50 <span class="xs:block hidden">Purge</span>
51 </div>
52 </n-button>
53 </div>
54 <PaginationIndeterminate
55 v-model:page="page"
56 v-model:page-size="pageSize"
57 v-model:sort="sort"
58 :page-sizes
59 :show-page-sizes="!compactMode"
60 :show-sort="!smallDeviceMode"
61 />
62 </div>
63
64 <n-spin :show="loadingAlerts || loadingPurge">
65 <div class="min-h-52">
66 <template v-if="alertsList.length">
67 <SocAlertItem
68 v-for="alert of alertsList"
69 :key="alert.id"
70 v-model:checked="alert.checked"
71 :alert-data="alert.data"
72 class="item-appear item-appear-bottom item-appear-005 mb-2"
73 :is-bookmark="isBookmarked(alert.data)"
74 :users="usersList"
75 :highlight="alert.id === highlight"
76 show-badges-toggle
77 show-checkbox
78 @check="toggleCheckedList(alert.id, $event)"
79 @bookmark="bookmark()"
80 @deleted="itemDeleted(alert.id)"
81 />
82 </template>
83 <template v-else>
84 <n-empty v-if="!loadingAlerts" description="No items found" class="h-48 justify-center" />
85 </template>
86 </div>
87 </n-spin>
88 </div>
89 </template>
90
91 <script setup lang="ts">
92 import type { AlertsFilter } from "@/api/endpoints/soc"
93 import type { SocAlert } from "@/types/soc/alert.d"
94 import type { SocUser } from "@/types/soc/user.d"
95 import { useResizeObserver, watchDebounced } from "@vueuse/core"
96 import axios from "axios"
97 import { NButton, NEmpty, NInput, NPopover, NSpin, useDialog, useMessage } from "naive-ui"
98 import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, toRefs, watch } from "vue"
99 import Api from "@/api"
100 import Icon from "@/components/common/Icon.vue"
101 import PaginationIndeterminate from "@/components/common/PaginationIndeterminate.vue"
102 import SocAlertItem from "./SocAlertItem/SocAlertItem.vue"
103 // MOCK
104 // import { alerts as alertsMock } from "./mock"
105
106 const props = defineProps<{
107 highlight: string | null | undefined
108 bookmarksList?: SocAlert[]
109 usersList?: SocUser[]
110 }>()
111 const emit = defineEmits<{
112 (e: "bookmark"): void
113 (e: "deleted", value?: string): void
114 (
115 e: "mounted",
116 value: {
117 reload: () => void
118 itemDeleted: (alertId: string, noEmit?: boolean) => void
119 }
120 ): void
121 }>()
122
123 const { highlight, bookmarksList, usersList } = toRefs(props)
124
125 const TrashIcon = "carbon:trash-can"
126 const CloseIcon = "carbon:close"
127
128 let reloadTimeout: NodeJS.Timeout | null = null
129 const dialog = useDialog()
130 const message = useMessage()
131 const loadingPurge = ref(false)
132 const loadingAlerts = ref(false)
133 const alertsList = ref<{ checked: boolean; id: string; data: SocAlert }[]>([])
134 const checkedList = ref<string[]>([])
135
136 const pageSize = ref(50)
137 const pageSizes = [25, 50, 100, 150, 200]
138 const page = ref(1)
139 const sort = ref<"desc" | "asc">("desc")
140 const alertTitle = ref("")
141 const header = ref()
142 const compactMode = ref(false)
143 const smallDeviceMode = ref(false)
144
145 let abortController: AbortController | null = null
146
147 const checkedCount = computed(() => checkedList.value.length)
148
149 function isBookmarked(alert: SocAlert): boolean {
150 return !!(bookmarksList.value || []).filter(o => o.alert_id === alert.alert_id).length
151 }
152
153 function bookmark() {
154 emit("bookmark")
155 safeReload()
156 }
157
158 function getAlerts() {
159 loadingAlerts.value = true
160
161 abortController = new AbortController()
162
163 const filter: Partial<AlertsFilter> = {}
164 if (pageSize.value) {
165 filter.pageSize = pageSize.value
166 }
167 if (page.value) {
168 filter.page = page.value
169 }
170 if (sort.value) {
171 filter.sort = sort.value
172 }
173 if (alertTitle.value) {
174 filter.alertTitle = alertTitle.value
175 }
176
177 Api.soc
178 .getAlerts(filter, abortController.signal)
179 .then(res => {
180 if (res.data.success) {
181 alertsList.value = (res.data?.alerts || []).map(o => ({
182 checked: isChecked(o.alert_id.toString()),
183 data: o,
184 id: o.alert_id.toString()
185 }))
186 } else {
187 message.warning(res.data?.message || "An error occurred. Please try again later.")
188 }
189 })
190 .catch(err => {
191 if (!axios.isCancel(err)) {
192 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
193 }
194 })
195 .finally(() => {
196 loadingAlerts.value = false
197 })
198 }
199
200 function scrollToItem(id: string) {
201 const element = document.getElementById(`alert-${id}`)
202 const scrollContent = document.querySelector("#main > .n-scrollbar > .n-scrollbar-container") as HTMLElement
203
204 if (element && scrollContent) {
205 const wrap: HTMLElement = scrollContent
206 const middle = element.offsetTop - wrap.offsetHeight / 2
207 scrollContent?.scrollTo({ top: middle, behavior: "smooth" })
208 }
209 }
210
211 function safeReload() {
212 abortController?.abort()
213
214 if (reloadTimeout) {
215 clearTimeout(reloadTimeout)
216 }
217
218 reloadTimeout = setTimeout(() => {
219 getAlerts()
220 }, 200)
221 }
222
223 function handleDelete() {
224 dialog.warning({
225 title: "Confirm",
226 content: "This will remove latest 1000 Soc Alerts, are you sure you want to proceed?",
227 positiveText: "Yes I'm sure",
228 negativeText: "Cancel",
229 onPositiveClick: () => {
230 deleteMultipleAlerts()
231 },
232 onNegativeClick: () => {
233 message.info("Purge canceled")
234 }
235 })
236 }
237
238 function handlePurge() {
239 dialog.warning({
240 title: "Confirm",
241 content: "This will remove 1000 Soc Alerts, are you sure you want to proceed?",
242 positiveText: "Yes I'm sure",
243 negativeText: "Cancel",
244 onPositiveClick: () => {
245 purge()
246 },
247 onNegativeClick: () => {
248 message.info("Purge canceled")
249 }
250 })
251 }
252
253 function deleteMultipleAlerts() {
254 loadingPurge.value = true
255
256 Api.soc
257 .deleteMultipleAlerts(checkedList.value)
258 .then(res => {
259 if (res.data.success) {
260 clearChecked()
261 getAlerts()
262 emit("deleted")
263 message.success(res.data?.message || "SOC Alerts purged successfully")
264 } else {
265 message.warning(res.data?.message || "An error occurred. Please try again later.")
266 }
267 })
268 .catch(err => {
269 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
270 })
271 .finally(() => {
272 loadingPurge.value = false
273 })
274 }
275
276 function purge() {
277 loadingPurge.value = true
278
279 Api.soc
280 .purgeAlerts()
281 .then(res => {
282 if (res.data.success) {
283 getAlerts()
284 emit("deleted")
285 message.success(res.data?.message || "SOC Alerts purged successfully")
286 } else {
287 message.warning(res.data?.message || "An error occurred. Please try again later.")
288 }
289 })
290 .catch(err => {
291 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
292 })
293 .finally(() => {
294 loadingPurge.value = false
295 })
296 }
297
298 function itemDeleted(alertId: string, noEmit = false) {
299 clearChecked(alertId)
300 getAlerts()
301 if (!noEmit) {
302 emit("deleted", alertId)
303 }
304 }
305
306 function toggleCheckedList(alertId: string, value: boolean) {
307 if (value) {
308 checkedList.value.push(alertId)
309 } else {
310 checkedList.value = checkedList.value.filter(o => o !== alertId)
311 }
312 }
313
314 function isChecked(alertId: string) {
315 return checkedList.value.filter(o => o === alertId).length !== 0
316 }
317
318 function clearChecked(alertId?: string) {
319 if (alertId) {
320 const alert = alertsList.value.find(o => o.id === alertId)
321 if (alert) {
322 alert.checked = false
323 }
324 checkedList.value = checkedList.value.filter(o => o !== alertId)
325 } else {
326 for (const alert of alertsList.value) {
327 alert.checked = false
328 }
329 checkedList.value = []
330 }
331 }
332
333 watch(loadingAlerts, val => {
334 if (!val) {
335 nextTick(() => {
336 setTimeout(() => {
337 if (highlight.value) {
338 scrollToItem(highlight.value)
339 }
340 }, 300)
341 })
342 }
343 })
344
345 watch(highlight, val => {
346 if (val) {
347 nextTick(() => {
348 setTimeout(() => {
349 scrollToItem(val)
350 })
351 })
352 }
353 })
354
355 watchDebounced(
356 [page, pageSize, sort, alertTitle],
357 () => {
358 safeReload()
359 },
360 { debounce: 300 }
361 )
362
363 useResizeObserver(header, entries => {
364 const entry = entries[0]
365 if (!entry) return
366
367 const { width } = entry.contentRect
368
369 if (width < 600) {
370 compactMode.value = true
371 pageSize.value = pageSizes[0] ?? 50
372 } else {
373 compactMode.value = false
374 }
375
376 smallDeviceMode.value = width < 450
377 })
378
379 onBeforeMount(() => {
380 // MOCK
381 // alertsList.value = alertsMock as unknown as SocAlert[]
382 getAlerts()
383 })
384
385 onMounted(() => {
386 emit("mounted", {
387 reload: () => {
388 safeReload()
389 },
390 itemDeleted
391 })
392 })
393
394 onBeforeUnmount(() => {
395 abortController?.abort()
396 })
397 </script>