| 1 | <template> |
| 2 | <n-spin :show="loadingDetails"> |
| 3 | <div class="@container min-h-50"> |
| 4 | <n-alert v-if="detailsError" title="Error" type="error" :description="detailsError" /> |
| 5 | |
| 6 | <n-tabs v-else-if="alert" type="line" animated> |
| 7 | <n-tab-pane name="overview" tab="Overview"> |
| 8 | <AlertOverview :alert @status-updated="handleStatusUpdated" /> |
| 9 | </n-tab-pane> |
| 10 | |
| 11 | <n-tab-pane name="assets" tab="Assets"> |
| 12 | <AlertAssets :alert /> |
| 13 | </n-tab-pane> |
| 14 | |
| 15 | <n-tab-pane |
| 16 | name="linked-cases" |
| 17 | :tab="`Linked Cases (${alert?.linked_cases?.length || alert?.case_ids?.length || 0})`" |
| 18 | > |
| 19 | <AlertCases |
| 20 | :alert |
| 21 | @created="handleCaseCreated" |
| 22 | @updated="handleCaseCreated" |
| 23 | @unlinked="handleCaseUnlinked" |
| 24 | @linked="handleCaseLinked" |
| 25 | /> |
| 26 | </n-tab-pane> |
| 27 | |
| 28 | <n-tab-pane name="iocs" tab="Indicators of Compromise (IoCs)"> |
| 29 | <AlertIocs :alert /> |
| 30 | </n-tab-pane> |
| 31 | |
| 32 | <n-tab-pane name="comments" :tab="`Comments (${alert.comments?.length || 0})`"> |
| 33 | <AlertComments |
| 34 | :alert |
| 35 | @added="handleCommentAdded" |
| 36 | @updated="handleCommentUpdated" |
| 37 | @deleted="handleCommentDeleted" |
| 38 | /> |
| 39 | </n-tab-pane> |
| 40 | </n-tabs> |
| 41 | </div> |
| 42 | </n-spin> |
| 43 | </template> |
| 44 | |
| 45 | <script setup lang="ts"> |
| 46 | import type { AlertStatusUpdateSuccessPayload } from "../AlertStatusSelect.vue" |
| 47 | import type { Alert } from "@/types/alerts" |
| 48 | import type { CommentItem } from "@/types/comments" |
| 49 | import type { ApiError } from "@/types/common" |
| 50 | import { NAlert, NSpin, NTabPane, NTabs } from "naive-ui" |
| 51 | import { ref, watch } from "vue" |
| 52 | import Api from "@/api" |
| 53 | import { getApiErrorMessage } from "@/utils" |
| 54 | import AlertAssets from "./AlertAssets.vue" |
| 55 | import AlertCases from "./AlertCases.vue" |
| 56 | import AlertComments from "./AlertComments.vue" |
| 57 | import AlertIocs from "./AlertIocs.vue" |
| 58 | import AlertOverview from "./AlertOverview.vue" |
| 59 | |
| 60 | const props = defineProps<{ |
| 61 | alertId: number | null |
| 62 | }>() |
| 63 | |
| 64 | const emit = defineEmits<{ |
| 65 | (e: "statusUpdated", value: AlertStatusUpdateSuccessPayload): void |
| 66 | }>() |
| 67 | |
| 68 | const alert = ref<Alert | null>(null) |
| 69 | const detailsError = ref<string | null>(null) |
| 70 | const loadingDetails = ref(false) |
| 71 | |
| 72 | async function loadAlertDetails() { |
| 73 | if (props.alertId === null) return |
| 74 | |
| 75 | loadingDetails.value = true |
| 76 | detailsError.value = null |
| 77 | |
| 78 | try { |
| 79 | const response = await Api.alerts.getAlert(props.alertId) |
| 80 | alert.value = response.data.alerts[0] || null |
| 81 | if (!alert.value) { |
| 82 | detailsError.value = "Alert not found." |
| 83 | } |
| 84 | } catch (err) { |
| 85 | detailsError.value = getApiErrorMessage(err as ApiError) |
| 86 | } finally { |
| 87 | loadingDetails.value = false |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | function handleCommentAdded(comment: CommentItem) { |
| 92 | if (!alert.value) return |
| 93 | |
| 94 | if (!alert.value.comments) { |
| 95 | alert.value.comments = [] |
| 96 | } |
| 97 | alert.value.comments.push(comment) |
| 98 | } |
| 99 | |
| 100 | function handleCommentUpdated(comment: CommentItem) { |
| 101 | if (!alert.value) return |
| 102 | alert.value.comments = alert.value.comments.map(c => (c.id === comment.id ? comment : c)) |
| 103 | } |
| 104 | |
| 105 | function handleCommentDeleted(commentId: number) { |
| 106 | if (!alert.value) return |
| 107 | alert.value.comments = alert.value.comments.filter(c => c.id !== commentId) |
| 108 | } |
| 109 | |
| 110 | function handleCaseCreated() { |
| 111 | loadAlertDetails() |
| 112 | } |
| 113 | |
| 114 | function handleCaseUnlinked() { |
| 115 | loadAlertDetails() |
| 116 | } |
| 117 | |
| 118 | function handleCaseLinked() { |
| 119 | loadAlertDetails() |
| 120 | } |
| 121 | |
| 122 | function handleStatusUpdated(payload: AlertStatusUpdateSuccessPayload) { |
| 123 | if (!alert.value) return |
| 124 | alert.value.status = payload.status |
| 125 | emit("statusUpdated", payload) |
| 126 | } |
| 127 | |
| 128 | watch( |
| 129 | () => props.alertId, |
| 130 | async newAlertId => { |
| 131 | alert.value = null |
| 132 | detailsError.value = null |
| 133 | |
| 134 | if (newAlertId !== null) { |
| 135 | await loadAlertDetails() |
| 136 | } |
| 137 | }, |
| 138 | { immediate: true } |
| 139 | ) |
| 140 | </script> |