1
-<template>
2
- <n-spin
3
- :show="loading"
4
- :description="loadingDelete ? 'Deleting Soc Alert' : 'Loading Soc Alert'"
5
- class="soc-alert-item flex flex-col gap-0 min-h-36 pb-2"
6
- :class="{ bookmarked: isBookmark, highlight, embedded }"
7
- :id="'alert-' + alert?.alert_id"
8
- >
9
- <div class="soc-alert-info px-5 py-3 flex flex-col gap-3" v-if="alert">
10
- <div class="header-box flex justify-between">
11
- <div class="flex items-center gap-2 cursor-pointer">
12
- <div v-if="showCheckbox" class="check-box mr-2">
13
- <n-checkbox size="large" v-model:checked="checked" />
14
- </div>
15
- <div class="id flex items-center gap-2 cursor-pointer" @click="showDetails = true">
16
- <span>#{{ alert.alert_id }} - {{ alert.alert_uuid }}</span>
17
- <Icon :name="InfoIcon" :size="16"></Icon>
18
- </div>
19
- <Icon
20
- v-if="!hideBookmarkAction"
21
- :name="loadingBookmark ? LoadingIcon : isBookmark ? StarActiveIcon : StarIcon"
22
- :size="16"
23
- @click="toggleBookmark()"
24
- class="toggler-bookmark"
25
- :class="{ active: isBookmark }"
26
- ></Icon>
27
- </div>
28
- <div class="time">
29
- <n-popover overlap placement="top-end" style="max-height: 240px" scrollable to="body">
30
- <template #trigger>
31
- <div class="flex items-center gap-2 cursor-help">
32
- <span>
33
- {{ formatDate(alert.alert_creation_time) }}
34
- </span>
35
- <Icon :name="TimeIcon" :size="16"></Icon>
36
- </div>
37
- </template>
38
- <div class="flex flex-col py-2 px-1">
39
- <SocAlertTimeline :alert="alert" />
40
- </div>
41
- </n-popover>
42
- </div>
43
- </div>
44
- <div class="main-box flex justify-between gap-4">
45
- <div class="content">
46
- <div class="title">{{ alert.alert_title }}</div>
47
- <div
48
- class="description mb-2"
49
- v-if="alert.alert_description && alert.alert_title !== alert.alert_description"
50
- >
51
- {{ alert.alert_description }}
52
- </div>
53
- </div>
54
- <SocAlertItemActions
55
- v-if="!hideSocCaseAction"
56
- class="actions-box"
57
- :caseId="caseId"
58
- :alertId="alert.alert_id"
59
- @caseCreated="caseCreated($event)"
60
- @deleted="deleted()"
61
- @startDeleting="loadingDelete = true"
62
- />
63
- </div>
64
-
65
- <div>
66
- <div
67
- class="show-badges-toggle flex items-center gap-2"
68
- v-if="showBadgesToggle"
69
- @click="showBadges = !showBadges"
70
- >
71
- {{ showBadges ? "Less info" : "More info" }}
72
- <span class="transition-transform flex items-center" :class="{ 'rotate-90': showBadges }">
73
- <Icon :name="ChevronIcon" :size="14"></Icon>
74
- </span>
75
- </div>
76
- <n-collapse-transition :show="!showBadgesToggle || showBadges">
77
- <div class="badges-box flex flex-wrap items-center gap-3 mt-3">
78
- <n-tooltip placement="top-start" trigger="hover">
79
- <template #trigger>
80
- <Badge type="splitted" hint-cursor>
81
- <template #iconLeft>
82
- <Icon :name="StatusIcon" :size="14"></Icon>
83
- </template>
84
- <template #label>Status</template>
85
- <template #value>{{ alert.status?.status_name || "-" }}</template>
86
- </Badge>
87
- </template>
88
- {{ alert.status.status_description }}
89
- </n-tooltip>
90
- <Badge type="splitted" :color="alert.severity?.severity_id === 5 ? 'danger' : undefined">
91
- <template #iconLeft>
92
- <Icon :name="SeverityIcon" :size="13"></Icon>
93
- </template>
94
- <template #label>Severity</template>
95
- <template #value>{{ alert.severity?.severity_name || "-" }}</template>
96
- </Badge>
97
- <Badge type="splitted" class="hide-on-small">
98
- <template #iconLeft>
99
- <Icon :name="SourceIcon" :size="13"></Icon>
100
- </template>
101
- <template #label>Source</template>
102
- <template #value>{{ alert.alert_source || "-" }}</template>
103
- </Badge>
104
- <Badge type="splitted" class="hide-on-small">
105
- <template #iconLeft>
106
- <Icon :name="CustomerIcon" :size="13"></Icon>
107
- </template>
108
- <template #label>Customer</template>
109
- <template #value>
110
- <template
111
- v-if="
112
- alert.customer?.customer_code &&
113
- alert.customer.customer_code !== 'Customer Not Found'
114
- "
115
- >
116
- <code
117
- class="cursor-pointer text-primary-color"
118
- @click="gotoCustomer({ code: alert.customer.customer_code })"
119
- >
120
- {{ alert.customer?.customer_name || alert.customer.customer_code || "-" }}
121
- <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
122
- </code>
123
- </template>
124
- <template v-else>
125
- {{ alert.customer?.customer_name || "-" }}
126
- </template>
127
- </template>
128
- </Badge>
129
-
130
- <SocAssignUser :alert="alert" :users="users" v-slot="{ loading }" @updated="updateAlert">
131
- <Badge type="active" class="cursor-pointer">
132
- <template #iconLeft>
133
- <n-spin :size="16" :show="loading">
134
- <Icon :name="OwnerIcon" :size="16"></Icon>
135
- </n-spin>
136
- </template>
137
- <template #label>Owner</template>
138
- <template #value>{{ ownerName || "n/d" }}</template>
139
- </Badge>
140
- </SocAssignUser>
141
-
142
- <Badge
143
- v-if="alert.alert_source_link"
144
- type="active"
145
- :href="alert.alert_source_link"
146
- target="_blank"
147
- alt="Source link"
148
- rel="nofollow noopener noreferrer"
149
- >
150
- <template #iconRight>
151
- <Icon :name="LinkIcon" :size="14"></Icon>
152
- </template>
153
- <template #label>Source link</template>
154
- </Badge>
155
- </div>
156
- </n-collapse-transition>
157
- </div>
158
-
159
- <div class="footer-box flex justify-between items-center gap-4">
160
- <SocAlertItemActions
161
- v-if="!hideSocCaseAction"
162
- class="actions-box grow !flex-wrap !justify-start"
163
- style="flex-direction: initial"
164
- size="small"
165
- :caseId="caseId"
166
- :alertId="alert.alert_id"
167
- @caseCreated="caseCreated($event)"
168
- @deleted="deleted()"
169
- @startDeleting="loadingDelete = true"
170
- />
171
- <div class="time">{{ formatDate(alert.alert_creation_time) }}</div>
172
- </div>
173
- </div>
174
- <n-collapse>
175
- <template #arrow>
176
- <div class="mx-5 flex">
177
- <Icon :name="ChevronIcon"></Icon>
178
- </div>
179
- </template>
180
- <n-collapse-item>
181
- <template #header>
182
- <div class="py-3 -ml-2">Alert details</div>
183
- </template>
184
- <AlertItem :alert="alertObject" :hide-actions="true" class="-mt-4" />
185
- </n-collapse-item>
186
- </n-collapse>
187
-
188
- <n-modal
189
- v-model:show="showDetails"
190
- preset="card"
191
- content-class="!p-0"
192
- :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
193
- :title="`SOC Alert: #${alert?.alert_id} - ${alert?.alert_uuid}`"
194
- :bordered="false"
195
- segmented
196
- >
197
- <n-tabs type="line" animated :tabs-padding="24" v-if="alert">
198
- <n-tab-pane name="Context" tab="Context" display-directive="show:lazy">
199
- <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4">
200
- <KVCard v-for="(value, key) of alert.alert_context" :key="key">
201
- <template #key>{{ key }}</template>
202
- <template #value>{{ value ?? "-" }}</template>
203
- </KVCard>
204
- </div>
205
- </n-tab-pane>
206
- <n-tab-pane name="Note" tab="Note" display-directive="show:lazy">
207
- <div class="p-7 pt-4">
208
- {{ alert.alert_note ?? "No notes for this alert" }}
209
- </div>
210
- </n-tab-pane>
211
- <n-tab-pane name="Customer" tab="Customer" display-directive="show:lazy">
212
- <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4">
213
- <KVCard v-for="(value, key) of alert.customer" :key="key">
214
- <template #key>{{ key }}</template>
215
- <template #value>
216
- <template v-if="key === 'customer_code' && value && value !== 'Customer Not Found'">
217
- <code
218
- class="cursor-pointer text-primary-color"
219
- @click="gotoCustomer({ code: value })"
220
- >
221
- #{{ value }}
222
- <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
223
- </code>
224
- </template>
225
- <template v-else>
226
- {{ value || "-" }}
227
- </template>
228
- </template>
229
- </KVCard>
230
- </div>
231
- </n-tab-pane>
232
- <n-tab-pane name="Owner" tab="Owner" display-directive="show:lazy">
233
- <div class="grid gap-2 px-7 pt-4">
234
- <Badge
235
- type="active"
236
- style="max-width: 145px"
237
- class="cursor-pointer"
238
- @click="gotoSocUsers(ownerId)"
239
- >
240
- <template #iconRight>
241
- <Icon :name="LinkIcon" :size="14"></Icon>
242
- </template>
243
- <template #label>Go to users page</template>
244
- </Badge>
245
- </div>
246
- <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4">
247
- <KVCard>
248
- <template #key>user_login</template>
249
- <template #value>
250
- <SocAssignUser
251
- :alert="alert"
252
- :users="users"
253
- v-slot="{ loading }"
254
- @updated="updateAlert"
255
- >
256
- <div class="flex items-center gap-2 cursor-pointer text-primary-color">
257
- <n-spin :size="16" :show="loading">
258
- <Icon :name="EditIcon" :size="16"></Icon>
259
- </n-spin>
260
- <span>{{ ownerName || "Assign a user" }}</span>
261
- </div>
262
- </SocAssignUser>
263
- </template>
264
- </KVCard>
265
- <KVCard v-if="alert.owner">
266
- <template #key>user_name</template>
267
- <template #value>
268
- <span>#{{ alert.owner.id }}</span>
269
- {{ alert.owner.user_name }}
270
- </template>
271
- </KVCard>
272
- <KVCard v-if="alert.owner">
273
- <template #key>user_email</template>
274
- <template #value>
275
- {{ alert.owner.user_email }}
276
- </template>
277
- </KVCard>
278
- </div>
279
- </n-tab-pane>
280
- <n-tab-pane name="History" tab="History" display-directive="show:lazy">
281
- <div class="p-7 pt-4">
282
- <SocAlertTimeline :alert="alert" />
283
- </div>
284
- </n-tab-pane>
285
- <n-tab-pane name="Details" tab="Details" display-directive="show:lazy">
286
- <div class="p-7 pt-4">
287
- <SimpleJsonViewer
288
- class="vuesjv-override"
289
- :model-value="socAlertDetail"
290
- :initialExpandedDepth="1"
291
- />
292
- </div>
293
- </n-tab-pane>
294
- <n-tab-pane name="Assets" tab="Assets" display-directive="show:lazy">
295
- <SocAlertAssetsList v-if="alert" :alert-id="alert.alert_id" />
296
- </n-tab-pane>
297
- </n-tabs>
298
- </n-modal>
299
- </n-spin>
300
-</template>
301
-
302
-<script setup lang="ts">
303
-import AlertItem from "@/components/alerts/Alert.vue"
304
-import type { SocAlert } from "@/types/soc/alert.d"
305
-import type { Alert } from "@/types/alerts.d"
306
-import Icon from "@/components/common/Icon.vue"
307
-import Badge from "@/components/common/Badge.vue"
308
-import { computed, onBeforeMount, ref, toRefs, watch } from "vue"
309
-import { SimpleJsonViewer } from "vue-sjv"
310
-import KVCard from "@/components/common/KVCard.vue"
311
-import SocAlertTimeline from "./SocAlertTimeline.vue"
312
-import SocAssignUser from "./SocAssignUser.vue"
313
-import SocAlertItemActions from "./SocAlertItemActions.vue"
314
-import SocAlertAssetsList from "./SocAlertAssetsList.vue"
315
-import "@/assets/scss/vuesjv-override.scss"
316
-import Api from "@/api"
317
-import {
318
- NCollapse,
319
- useMessage,
320
- NCollapseItem,
321
- NPopover,
322
- NModal,
323
- NTabs,
324
- NTabPane,
325
- NSpin,
326
- NCheckbox,
327
- NTooltip,
328
- NCollapseTransition
329
-} from "naive-ui"
330
-import { useSettingsStore } from "@/stores/settings"
331
-import dayjs from "@/utils/dayjs"
332
-import type { SocUser } from "@/types/soc/user.d"
333
-import { useGoto } from "@/composables/useGoto"
334
-
335
-const checked = defineModel<boolean>("checked", { default: false })
336
-
337
-const emit = defineEmits<{
338
- (e: "bookmark", value: boolean): void
339
- (e: "deleted"): void
340
- (e: "checked"): void
341
- (e: "unchecked"): void
342
- (e: "check", value: boolean): void
343
-}>()
344
-
345
-const props = defineProps<{
346
- alertData?: SocAlert
347
- alertId?: string | number
348
- isBookmark?: boolean
349
- highlight?: boolean | null | undefined
350
- embedded?: boolean
351
- users?: SocUser[]
352
- hideSocCaseAction?: boolean
353
- hideBookmarkAction?: boolean
354
- showBadgesToggle?: boolean
355
- showCheckbox?: boolean
356
-}>()
357
-const {
358
- alertData,
359
- alertId,
360
- isBookmark,
361
- highlight,
362
- users,
363
- embedded,
364
- hideSocCaseAction,
365
- hideBookmarkAction,
366
- showBadgesToggle,
367
- showCheckbox
368
-} = toRefs(props)
369
-
370
-const ChevronIcon = "carbon:chevron-right"
371
-const InfoIcon = "carbon:information"
372
-const TimeIcon = "carbon:time"
373
-const LinkIcon = "carbon:launch"
374
-const StatusIcon = "fluent:status-20-regular"
375
-const SeverityIcon = "bi:shield-exclamation"
376
-const SourceIcon = "lucide:arrow-down-right-from-circle"
377
-const CustomerIcon = "carbon:user"
378
-const StarActiveIcon = "carbon:star-filled"
379
-const OwnerIcon = "carbon:user-military"
380
-const StarIcon = "carbon:star"
381
-const EditIcon = "uil:edit-alt"
382
-const LoadingIcon = "eos-icons:loading"
383
-
384
-const showDetails = ref(false)
385
-const showBadges = ref(false)
386
-const loadingDelete = ref(false)
387
-const loadingData = ref(false)
388
-const loadingBookmark = ref(false)
389
-const message = useMessage()
390
-const { gotoCustomer, gotoSocUsers } = useGoto()
391
-
392
-const alert = ref(alertData.value || null)
393
-
394
-const alertObject = ref<Alert>({} as Alert)
395
-
396
-const loading = computed(() => loadingBookmark.value || loadingData.value || loadingDelete.value)
397
-const ownerName = computed(() => alert.value?.owner?.user_login)
398
-const ownerId = computed(() => alert.value?.owner?.id)
399
-const caseId = computed<number | null>(() => (alert.value?.cases?.length ? alert.value?.cases[0] : null))
400
-
401
-const socAlertDetail = computed<Partial<SocAlert>>(() => {
402
- const clone: Partial<SocAlert> = JSON.parse(JSON.stringify(alert.value))
403
-
404
- delete clone.alert_context
405
- delete clone.alert_source_content
406
- delete clone.customer
407
- delete clone.modification_history
408
- delete clone.alert_note
409
- delete clone.alert_source_link
410
-
411
- return clone
412
-})
413
-
414
-const dFormats = useSettingsStore().dateFormat
415
-
416
-function formatDate(timestamp: string | number, utc: boolean = true): string {
417
- return dayjs(timestamp).utc(utc).format(dFormats.datetimesec)
418
-}
419
-
420
-function toggleBookmark() {
421
- if (alert.value?.alert_id) {
422
- loadingBookmark.value = true
423
-
424
- const method = isBookmark.value ? "removeAlertBookmark" : "addAlertBookmark"
425
-
426
- Api.soc[method](alert.value.alert_id.toString())
427
- .then(res => {
428
- if (res.data.success) {
429
- emit("bookmark", method === "removeAlertBookmark" ? false : true)
430
- message.success(res.data?.message || "Stream started.")
431
- } else {
432
- message.warning(res.data?.message || "An error occurred. Please try again later.")
433
- }
434
- })
435
- .catch(err => {
436
- message.error(err.response?.data?.message || "An error occurred. Please try again later.")
437
- })
438
- .finally(() => {
439
- loadingBookmark.value = false
440
- })
441
- }
442
-}
443
-
444
-function updateAlert(alertUpdated: SocAlert) {
445
- const ownerObject = alertUpdated.owner
446
- const modificationHistory = alertUpdated.modification_history
447
-
448
- if (alert.value) {
449
- alert.value.owner = ownerObject
450
- alert.value.modification_history = modificationHistory
451
- }
452
-}
453
-
454
-function getAlert(id: string | number, cb?: () => void) {
455
- loadingData.value = true
456
-
457
- Api.soc
458
- .getAlert(id.toString())
459
- .then(res => {
460
- if (res.data.success) {
461
- alert.value = res.data?.alert || null
462
- if (cb) cb()
463
- } else {
464
- message.warning(res.data?.message || "An error occurred. Please try again later.")
465
- }
466
- })
467
- .catch(err => {
468
- message.error(err.response?.data?.message || "An error occurred. Please try again later.")
469
- })
470
- .finally(() => {
471
- loadingData.value = false
472
- })
473
-}
474
-
475
-function createAlertObject() {
476
- alertObject.value = {
477
- _index: "",
478
- _id: alert.value?.alert_context.alert_id,
479
- _source: alert.value?.alert_source_content
480
- } as Alert
481
-}
482
-
483
-function caseCreated(caseId: string | number) {
484
- if (alert.value) {
485
- alert.value.cases = [caseId]
486
- }
487
-}
488
-
489
-function deleted() {
490
- loadingDelete.value = false
491
- emit("deleted")
492
-}
493
-
494
-watch(checked, val => {
495
- emit("check", val)
496
- if (val) {
497
- emit("checked")
498
- } else {
499
- emit("unchecked")
500
- }
501
-})
502
-
503
-onBeforeMount(() => {
504
- createAlertObject()
505
-
506
- if (!alertData.value && alertId.value) {
507
- getAlert(alertId.value, () => {
508
- createAlertObject()
509
- })
510
- }
511
-})
512
-</script>
513
-
514
-<style lang="scss" scoped>
515
-.soc-alert-item {
516
- &:not(.embedded) {
517
- border-radius: var(--border-radius);
518
- background-color: var(--bg-color);
519
- border: var(--border-small-050);
520
- }
521
- transition: all 0.2s var(--bezier-ease);
522
-
523
- .soc-alert-info {
524
- border-bottom: var(--border-small-050);
525
-
526
- .header-box {
527
- font-family: var(--font-family-mono);
528
- font-size: 13px;
529
- .id {
530
- word-break: break-word;
531
- color: var(--fg-secondary-color);
532
- line-height: 1.2;
533
-
534
- &:hover {
535
- color: var(--primary-color);
536
- }
537
- }
538
-
539
- .toggler-bookmark {
540
- &.active {
541
- color: var(--primary-color);
542
- }
543
- &:hover {
544
- color: var(--primary-color);
545
- }
546
- }
547
- .time {
548
- color: var(--fg-secondary-color);
549
-
550
- &:hover {
551
- color: var(--primary-color);
552
- }
553
- }
554
- }
555
-
556
- .main-box {
557
- .content {
558
- word-break: break-word;
559
-
560
- .description {
561
- color: var(--fg-secondary-color);
562
- font-size: 13px;
563
- }
564
- }
565
- }
566
-
567
- .show-badges-toggle {
568
- font-size: 14px;
569
- cursor: pointer;
570
- transition: color 0.2s var(--bezier-ease);
571
-
572
- &:hover {
573
- color: var(--primary-color);
574
- }
575
- }
576
-
577
- .footer-box {
578
- font-size: 13px;
579
- margin-top: 10px;
580
- display: none;
581
-
582
- .time {
583
- font-family: var(--font-family-mono);
584
- text-align: right;
585
- color: var(--fg-secondary-color);
586
- }
587
- }
588
- }
589
-
590
- &.bookmarked {
591
- background-color: var(--primary-005-color);
592
- border-color: var(--primary-030-color);
593
- }
594
-
595
- &:not(.embedded) {
596
- &:hover,
597
- &.highlight {
598
- border-color: var(--primary-color);
599
- }
600
- }
601
-
602
- @container (max-width: 650px) {
603
- .soc-alert-info {
604
- .header-box {
605
- .time {
606
- display: none;
607
- }
608
- }
609
-
610
- .main-box {
611
- .actions-box {
612
- display: none;
613
- }
614
- .badges-box {
615
- .badge {
616
- &.hide-on-small {
617
- display: none;
618
- }
619
- }
620
- }
621
- }
622
- .footer-box {
623
- display: flex;
624
- }
625
- }
626
- }
627
-}
628
-</style>