| 1 | <template> |
| 2 | <div class="grid-auto-fit-200 grid gap-2 p-6 pt-3"> |
| 3 | <CardKV v-for="(value, key) of asset" :key> |
| 4 | <template #key> |
| 5 | {{ key }} |
| 6 | </template> |
| 7 | <template #value> |
| 8 | <div v-if="key === 'id'">#{{ value }}</div> |
| 9 | <div v-else-if="key === 'customer_code'"> |
| 10 | <code |
| 11 | class="text-primary cursor-pointer" |
| 12 | @click.stop="routeCustomer({ code: asset.customer_code }).navigate()" |
| 13 | > |
| 14 | #{{ asset.customer_code }} |
| 15 | <Icon :name="LinkIcon" :size="14" class="relative top-0.5" /> |
| 16 | </code> |
| 17 | </div> |
| 18 | <div v-else-if="key === 'agent_id'"> |
| 19 | <code class="text-primary cursor-pointer" @click.stop="routeAgent(asset.agent_id).navigate()"> |
| 20 | {{ asset.agent_id }} |
| 21 | <Icon :name="LinkIcon" :size="14" class="relative top-0.5" /> |
| 22 | </code> |
| 23 | </div> |
| 24 | <div v-else-if="key === 'index_name'"> |
| 25 | <code class="text-primary cursor-pointer" @click.stop="routeIndex(asset.index_name).navigate()"> |
| 26 | {{ asset.index_name }} |
| 27 | <Icon :name="LinkIcon" :size="14" class="relative top-0.5" /> |
| 28 | </code> |
| 29 | </div> |
| 30 | <div v-else-if="key === 'index_id'"> |
| 31 | <code class="text-primary cursor-pointer" @click.stop="openAlertDetails()"> |
| 32 | {{ asset.index_id }} |
| 33 | <Icon :name="ViewIcon" :size="14" class="relative top-0.5" /> |
| 34 | </code> |
| 35 | </div> |
| 36 | <div v-else-if="key === 'alert_linked'"> |
| 37 | <div class="flex items-center gap-2"> |
| 38 | <span>{{ value }}</span> |
| 39 | <code class="text-primary cursor-pointer" @click.stop="openEventSearch()"> |
| 40 | View in Event Search |
| 41 | <Icon :name="LinkIcon" :size="14" class="relative top-0.5" /> |
| 42 | </code> |
| 43 | </div> |
| 44 | </div> |
| 45 | <div v-else> |
| 46 | {{ value === "" ? "-" : (value ?? "-") }} |
| 47 | </div> |
| 48 | </template> |
| 49 | </CardKV> |
| 50 | </div> |
| 51 | |
| 52 | <n-modal |
| 53 | v-model:show="showAlertDetails" |
| 54 | preset="card" |
| 55 | content-class="p-0!" |
| 56 | :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }" |
| 57 | :bordered="false" |
| 58 | title="Alert Details" |
| 59 | segmented |
| 60 | > |
| 61 | <n-spin :show="loading" class="min-h-40"> |
| 62 | <n-tabs type="line" animated :tabs-padding="24"> |
| 63 | <n-tab-pane name="Info" tab="Info" display-directive="show"> |
| 64 | <div class="grid-auto-fit-200 grid gap-2 p-6 pt-3"> |
| 65 | <CardKV v-for="(value, key) of alertDetailsInfo" :key> |
| 66 | <template #key> |
| 67 | {{ key }} |
| 68 | </template> |
| 69 | <template #value> |
| 70 | <div v-if="key === '_index'"> |
| 71 | <code |
| 72 | class="text-primary cursor-pointer" |
| 73 | @click.stop="routeIndex(alertDetailsInfo._index).navigate()" |
| 74 | > |
| 75 | {{ alertDetailsInfo._index }} |
| 76 | <Icon :name="LinkIcon" :size="14" class="relative top-0.5" /> |
| 77 | </code> |
| 78 | </div> |
| 79 | <div v-else> |
| 80 | {{ value === "" ? "-" : (value ?? "-") }} |
| 81 | </div> |
| 82 | </template> |
| 83 | </CardKV> |
| 84 | </div> |
| 85 | </n-tab-pane> |
| 86 | <n-tab-pane name="Source" tab="Source" display-directive="show"> |
| 87 | <div v-if="alertDetailsSource" class="p-6 pt-3"> |
| 88 | <CodeSource :code="alertDetailsSource" lang="json" /> |
| 89 | </div> |
| 90 | </n-tab-pane> |
| 91 | </n-tabs> |
| 92 | </n-spin> |
| 93 | </n-modal> |
| 94 | </template> |
| 95 | |
| 96 | <script setup lang="ts"> |
| 97 | import type { AlertAsset, AlertDetails } from "@/types/incidentManagement/alerts.d" |
| 98 | import _omit from "lodash/omit" |
| 99 | import { NModal, NSpin, NTabPane, NTabs, useMessage } from "naive-ui" |
| 100 | import { computed, defineAsyncComponent, ref, toRefs, watch } from "vue" |
| 101 | import Api from "@/api" |
| 102 | import CardKV from "@/components/common/cards/CardKV.vue" |
| 103 | import Icon from "@/components/common/Icon.vue" |
| 104 | import { useNavigation } from "@/composables/useNavigation" |
| 105 | |
| 106 | const props = defineProps<{ asset: AlertAsset }>() |
| 107 | |
| 108 | const CodeSource = defineAsyncComponent(() => import("@/components/common/CodeSource.vue")) |
| 109 | |
| 110 | const { asset } = toRefs(props) |
| 111 | |
| 112 | const LinkIcon = "carbon:launch" |
| 113 | const ViewIcon = "iconoir:eye-solid" |
| 114 | const { routeAgent, routeIndex, routeCustomer, routeEventSearch } = useNavigation() |
| 115 | const message = useMessage() |
| 116 | const loading = ref(false) |
| 117 | const showAlertDetails = ref(false) |
| 118 | const alertDetails = ref<AlertDetails | null>(null) |
| 119 | const alertDetailsInfo = computed(() => _omit(alertDetails.value, ["_source"])) |
| 120 | const alertDetailsSource = computed(() => { |
| 121 | if (!alertDetails.value?._source) return undefined |
| 122 | |
| 123 | const source = alertDetails.value._source |
| 124 | const priorityKeys = ["timestamp", "agent_name"] // Keys to show first |
| 125 | const endKeys = ["message"] // Keys to show last |
| 126 | |
| 127 | // Separate priority keys, end keys, and other keys |
| 128 | const existingPriorityKeys = priorityKeys.filter(key => key in source) |
| 129 | const existingEndKeys = endKeys.filter(key => key in source) |
| 130 | const otherKeys = Object.keys(source) |
| 131 | .filter(key => !priorityKeys.includes(key) && !endKeys.includes(key)) |
| 132 | .sort() // Sort alphabetically |
| 133 | |
| 134 | // Combine: priority keys first, then sorted other keys, then end keys |
| 135 | const sortedKeys = [...existingPriorityKeys, ...otherKeys, ...existingEndKeys] |
| 136 | |
| 137 | // Create new object with sorted keys |
| 138 | const sortedSource: Record<string, any> = {} |
| 139 | sortedKeys.forEach(key => { |
| 140 | sortedSource[key] = source[key] |
| 141 | }) |
| 142 | |
| 143 | return sortedSource |
| 144 | }) |
| 145 | |
| 146 | watch(showAlertDetails, val => { |
| 147 | if (val && !alertDetails.value) { |
| 148 | getAlertDetails(asset.value.index_id, asset.value.index_name) |
| 149 | } |
| 150 | }) |
| 151 | |
| 152 | function getAlertDetails(indexId: string, indexName: string) { |
| 153 | loading.value = true |
| 154 | |
| 155 | Api.incidentManagement.alerts |
| 156 | .getAlertDetails(indexId, indexName) |
| 157 | .then(res => { |
| 158 | if (res.data.success) { |
| 159 | alertDetails.value = res.data?.alert_details || null |
| 160 | } else { |
| 161 | closeAlertDetails() |
| 162 | message.warning(res.data?.message || "An error occurred. Please try again later.") |
| 163 | } |
| 164 | }) |
| 165 | .catch(err => { |
| 166 | closeAlertDetails() |
| 167 | message.error(err.response?.data?.message || "An error occurred. Please try again later.") |
| 168 | }) |
| 169 | .finally(() => { |
| 170 | loading.value = false |
| 171 | }) |
| 172 | } |
| 173 | |
| 174 | function openAlertDetails() { |
| 175 | showAlertDetails.value = true |
| 176 | } |
| 177 | |
| 178 | function closeAlertDetails() { |
| 179 | showAlertDetails.value = false |
| 180 | } |
| 181 | |
| 182 | function openEventSearch() { |
| 183 | const url = routeEventSearch({ |
| 184 | customer_code: asset.value.customer_code, |
| 185 | query: `alert_id:"${asset.value.alert_linked}"` |
| 186 | }).fullUrl() |
| 187 | window.open(url, "_blank") |
| 188 | } |
| 189 | </script> |