| 1 | <template> |
| 2 | <div :id="`alert-${alert?.alert_id}`"> |
| 3 | <CardEntity |
| 4 | :loading |
| 5 | :loading-description="loadingDelete ? 'Deleting Soc Alert' : 'Loading Soc Alert'" |
| 6 | class="min-h-36" |
| 7 | clickable |
| 8 | hoverable |
| 9 | :status="isBookmark ? 'success' : undefined" |
| 10 | :embedded |
| 11 | :highlighted="!!highlight" |
| 12 | @click.stop="showDetails = true" |
| 13 | > |
| 14 | <template #headerMain> |
| 15 | <div class="flex items-center gap-2"> |
| 16 | <div v-if="showCheckbox" class="check-box mr-2"> |
| 17 | <n-checkbox v-model:checked="checked" size="large" /> |
| 18 | </div> |
| 19 | <span v-if="alert">#{{ alert.alert_id }} - {{ alert.alert_uuid }}</span> |
| 20 | <SocAlertItemBookmarkToggler |
| 21 | v-if="!hideBookmarkAction && alert" |
| 22 | :alert |
| 23 | :is-bookmark |
| 24 | @bookmark="emit('bookmark', $event)" |
| 25 | /> |
| 26 | </div> |
| 27 | </template> |
| 28 | <template v-if="alert" #headerExtra> |
| 29 | <SocAlertItemTime :alert /> |
| 30 | </template> |
| 31 | |
| 32 | <template v-if="alert" #default> |
| 33 | <div class="flex flex-col gap-1"> |
| 34 | {{ alert.alert_title }} |
| 35 | |
| 36 | <p v-if="alert.alert_description && alert.alert_title !== alert.alert_description" class=" "> |
| 37 | {{ alert.alert_description }} |
| 38 | </p> |
| 39 | </div> |
| 40 | </template> |
| 41 | |
| 42 | <template #mainExtra> |
| 43 | <div class="flex justify-between gap-3"> |
| 44 | <div> |
| 45 | <div |
| 46 | v-if="showBadgesToggle" |
| 47 | class="show-badges-toggle flex items-center gap-2" |
| 48 | @click.stop="showBadges = !showBadges" |
| 49 | > |
| 50 | {{ showBadges ? "Less info" : "More info" }} |
| 51 | <span class="flex items-center transition-transform" :class="{ 'rotate-90': showBadges }"> |
| 52 | <Icon :name="ChevronIcon" :size="14" /> |
| 53 | </span> |
| 54 | </div> |
| 55 | <n-collapse-transition :show="!showBadgesToggle || showBadges"> |
| 56 | <SocAlertItemBadges v-if="alert" :alert :users @updated="updateAlert" /> |
| 57 | </n-collapse-transition> |
| 58 | </div> |
| 59 | |
| 60 | <SocAlertItemActions |
| 61 | v-if="!hideSocCaseAction && alert" |
| 62 | class="flex flex-row flex-wrap gap-2" |
| 63 | size="small" |
| 64 | :case-id |
| 65 | :alert-id="alert.alert_id" |
| 66 | @case-created="caseCreated($event)" |
| 67 | @deleted="deleted()" |
| 68 | @start-deleting="loadingDelete = true" |
| 69 | /> |
| 70 | </div> |
| 71 | </template> |
| 72 | <template #footer> |
| 73 | <n-collapse> |
| 74 | <template #arrow> |
| 75 | <div class="mx-5 flex"> |
| 76 | <Icon :name="ChevronIcon" /> |
| 77 | </div> |
| 78 | </template> |
| 79 | <n-collapse-item> |
| 80 | <template #header> |
| 81 | <div class="-ml-2 py-3">Alert details</div> |
| 82 | </template> |
| 83 | <AlertItem :alert="alertObject" hide-actions class="-mt-4" /> |
| 84 | </n-collapse-item> |
| 85 | </n-collapse> |
| 86 | </template> |
| 87 | </CardEntity> |
| 88 | |
| 89 | <n-modal |
| 90 | v-model:show="showDetails" |
| 91 | preset="card" |
| 92 | content-class="p-0!" |
| 93 | :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }" |
| 94 | :bordered="false" |
| 95 | display-directive="show" |
| 96 | segmented |
| 97 | > |
| 98 | <template #header> |
| 99 | <div class="whitespace-nowrap">SOC Alert: {{ alert?.alert_id }}</div> |
| 100 | </template> |
| 101 | <template #header-extra> |
| 102 | <ArtifactRecommendation v-if="alert" :context="alert.alert_context" /> |
| 103 | </template> |
| 104 | <SocAlertItemDetails v-if="alert" :alert :users @updated="updateAlert" /> |
| 105 | </n-modal> |
| 106 | </div> |
| 107 | </template> |
| 108 | |
| 109 | <script setup lang="ts"> |
| 110 | import type { Alert } from "@/types/alerts.d" |
| 111 | import type { SocAlert } from "@/types/soc/alert.d" |
| 112 | import type { SocUser } from "@/types/soc/user.d" |
| 113 | import { NCheckbox, NCollapse, NCollapseItem, NCollapseTransition, NModal, useMessage } from "naive-ui" |
| 114 | import { computed, defineAsyncComponent, onBeforeMount, ref, toRefs, watch } from "vue" |
| 115 | import Api from "@/api" |
| 116 | import CardEntity from "@/components/common/cards/CardEntity.vue" |
| 117 | import Icon from "@/components/common/Icon.vue" |
| 118 | import SocAlertItemActions from "./SocAlertItemActions.vue" |
| 119 | import SocAlertItemBookmarkToggler from "./SocAlertItemBookmarkToggler.vue" |
| 120 | import SocAlertItemTime from "./SocAlertItemTime.vue" |
| 121 | |
| 122 | const props = defineProps<{ |
| 123 | alertData?: SocAlert |
| 124 | alertId?: string | number |
| 125 | isBookmark?: boolean |
| 126 | highlight?: boolean | null | undefined |
| 127 | embedded?: boolean |
| 128 | users?: SocUser[] |
| 129 | hideSocCaseAction?: boolean |
| 130 | hideBookmarkAction?: boolean |
| 131 | showBadgesToggle?: boolean |
| 132 | showCheckbox?: boolean |
| 133 | }>() |
| 134 | const emit = defineEmits<{ |
| 135 | (e: "bookmark", value: boolean): void |
| 136 | (e: "deleted"): void |
| 137 | (e: "checked"): void |
| 138 | (e: "unchecked"): void |
| 139 | (e: "check", value: boolean): void |
| 140 | }>() |
| 141 | const SocAlertItemDetails = defineAsyncComponent(() => import("./SocAlertItemDetails.vue")) |
| 142 | const SocAlertItemBadges = defineAsyncComponent(() => import("./SocAlertItemBadges.vue")) |
| 143 | const ArtifactRecommendation = defineAsyncComponent(() => import("@/components/artifacts/ArtifactRecommendation.vue")) |
| 144 | const AlertItem = defineAsyncComponent(() => import("@/components/alerts/Alert.vue")) |
| 145 | |
| 146 | const checked = defineModel<boolean>("checked", { default: false }) |
| 147 | |
| 148 | const { |
| 149 | alertData, |
| 150 | alertId, |
| 151 | isBookmark, |
| 152 | highlight, |
| 153 | users, |
| 154 | embedded, |
| 155 | hideSocCaseAction, |
| 156 | hideBookmarkAction, |
| 157 | showBadgesToggle, |
| 158 | showCheckbox |
| 159 | } = toRefs(props) |
| 160 | |
| 161 | const ChevronIcon = "carbon:chevron-right" |
| 162 | |
| 163 | const showDetails = ref(false) |
| 164 | const showBadges = ref(false) |
| 165 | const loadingDelete = ref(false) |
| 166 | const loadingData = ref(false) |
| 167 | const loadingBookmark = ref(false) |
| 168 | const message = useMessage() |
| 169 | |
| 170 | const alert = ref(alertData.value || null) |
| 171 | const alertObject = ref<Alert>({} as Alert) |
| 172 | const loading = computed(() => loadingBookmark.value || loadingData.value || loadingDelete.value) |
| 173 | const caseId = computed(() => (alert.value?.cases?.length ? alert.value?.cases[0] : null)) |
| 174 | |
| 175 | function updateAlert(alertUpdated: SocAlert) { |
| 176 | const ownerObject = alertUpdated.owner |
| 177 | const modificationHistory = alertUpdated.modification_history |
| 178 | |
| 179 | if (alert.value) { |
| 180 | alert.value.owner = ownerObject |
| 181 | alert.value.modification_history = modificationHistory |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | function getAlert(id: string | number, cb?: () => void) { |
| 186 | loadingData.value = true |
| 187 | |
| 188 | Api.soc |
| 189 | .getAlert(id.toString()) |
| 190 | .then(res => { |
| 191 | if (res.data.success) { |
| 192 | alert.value = res.data?.alert || null |
| 193 | if (cb) cb() |
| 194 | } else { |
| 195 | message.warning(res.data?.message || "An error occurred. Please try again later.") |
| 196 | } |
| 197 | }) |
| 198 | .catch(err => { |
| 199 | message.error(err.response?.data?.message || "An error occurred. Please try again later.") |
| 200 | }) |
| 201 | .finally(() => { |
| 202 | loadingData.value = false |
| 203 | }) |
| 204 | } |
| 205 | |
| 206 | function createAlertObject() { |
| 207 | alertObject.value = { |
| 208 | _index: "", |
| 209 | _id: alert.value?.alert_context.alert_id, |
| 210 | _source: alert.value?.alert_source_content |
| 211 | } as Alert |
| 212 | } |
| 213 | |
| 214 | function caseCreated(caseId: string | number) { |
| 215 | if (alert.value) { |
| 216 | alert.value.cases = [caseId] |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | function deleted() { |
| 221 | loadingDelete.value = false |
| 222 | emit("deleted") |
| 223 | } |
| 224 | |
| 225 | watch(checked, val => { |
| 226 | emit("check", val) |
| 227 | if (val) { |
| 228 | emit("checked") |
| 229 | } else { |
| 230 | emit("unchecked") |
| 231 | } |
| 232 | }) |
| 233 | |
| 234 | onBeforeMount(() => { |
| 235 | createAlertObject() |
| 236 | |
| 237 | if (!alertData.value && alertId.value) { |
| 238 | getAlert(alertId.value, () => { |
| 239 | createAlertObject() |
| 240 | }) |
| 241 | } |
| 242 | }) |
| 243 | </script> |
| 244 | |
| 245 | <style lang="scss" scoped> |
| 246 | .show-badges-toggle { |
| 247 | font-size: 14px; |
| 248 | cursor: pointer; |
| 249 | transition: color 0.2s var(--bezier-ease); |
| 250 | |
| 251 | &:hover { |
| 252 | color: var(--primary-color); |
| 253 | } |
| 254 | } |
| 255 | </style> |