main
vue 410 lines 11.4 KB
Raw
1 <template>
2 <div>
3 <CardEntity
4 :loading
5 :embedded
6 hoverable
7 :size="compact ? 'small' : 'medium'"
8 :clickable="compact"
9 :highlighted="highlight"
10 @click="compact ? openDetails() : undefined"
11 >
12 <template v-if="alert" #headerMain>
13 <div class="flex items-center gap-4">
14 <div v-if="selectable">
15 <n-checkbox :checked @click.stop="emit('check', !checked)" />
16 </div>
17 <div
18 class="flex cursor-pointer items-center gap-2"
19 :class="{ 'hover:text-primary cursor-pointer': !compact }"
20 @click="compact ? undefined : openDetails()"
21 >
22 <span>#{{ alert.id }} - {{ alert.source }}</span>
23 <Icon v-if="!compact" :name="InfoIcon" :size="16" />
24 </div>
25 </div>
26 </template>
27
28 <template v-if="alert?.alert_creation_time" #headerExtra>
29 <div :class="{ 'hidden sm:block': !compact }">
30 <n-popover
31 v-if="!compact"
32 overlap
33 placement="top-end"
34 style="max-height: 240px"
35 scrollable
36 to="body"
37 >
38 <template #trigger>
39 <div class="flex cursor-help items-center gap-2">
40 <span v-if="alert.alert_creation_time">
41 {{ formatDate(alert.alert_creation_time, dFormats.datetime) }}
42 </span>
43 <Icon :name="TimeIcon" :size="16" />
44 </div>
45 </template>
46 <div class="flex flex-col px-1 py-2">
47 <AlertTimeline v-if="alert" :alert />
48 </div>
49 </n-popover>
50 <span v-if="compact && alert.alert_creation_time">
51 {{ formatDate(alert.alert_creation_time, dFormats.datetime) }}
52 </span>
53 </div>
54 </template>
55
56 <template v-if="alert" #default>
57 {{ alert.alert_name }}
58 </template>
59
60 <template v-if="alert" #mainExtra>
61 <div v-if="compact" class="flex flex-wrap items-center gap-3">
62 <Badge
63 type="splitted"
64 class="cursor-pointer"
65 bright
66 :color="
67 alert.status === 'OPEN' ? 'danger' : alert.status === 'IN_PROGRESS' ? 'warning' : 'success'
68 "
69 >
70 <template #iconLeft>
71 <StatusIcon :status="alert.status" />
72 </template>
73 <template #label>Status</template>
74 <template #value>
75 <div class="flex items-center gap-2">
76 {{ alert.status || "n/d" }}
77 </div>
78 </template>
79 </Badge>
80
81 <Badge
82 type="splitted"
83 class="cursor-pointer"
84 bright
85 :color="alert.assigned_to ? 'success' : undefined"
86 >
87 <template #iconLeft>
88 <AssigneeIcon :assignee="alert.assigned_to" />
89 </template>
90 <template #label>Assignee</template>
91 <template #value>
92 <div class="flex items-center gap-2">
93 {{ alert.assigned_to || "n/d" }}
94 </div>
95 </template>
96 </Badge>
97 </div>
98 <div v-else class="flex flex-wrap items-center gap-3">
99 <AlertStatusSwitch v-slot="{ loading: loadingStatus }" :alert @updated="updateAlert($event)">
100 <Badge
101 type="splitted"
102 class="cursor-pointer"
103 bright
104 :color="
105 alert.status === 'OPEN'
106 ? 'danger'
107 : alert.status === 'IN_PROGRESS'
108 ? 'warning'
109 : 'success'
110 "
111 >
112 <template #iconLeft>
113 <n-spin :size="12" :show="loadingStatus" content-class="flex flex-col justify-center">
114 <StatusIcon :status="alert.status" />
115 </n-spin>
116 </template>
117 <template #label>Status</template>
118 <template #value>
119 <div class="flex items-center gap-2">
120 {{ alert.status || "n/d" }}
121 <Icon :name="EditIcon" :size="13" />
122 </div>
123 </template>
124 </Badge>
125 </AlertStatusSwitch>
126
127 <AlertAssignUser v-slot="{ loading: loadingAssignee }" :alert @updated="updateAlert($event)">
128 <Badge
129 type="splitted"
130 class="cursor-pointer"
131 bright
132 :color="alert.assigned_to ? 'success' : undefined"
133 >
134 <template #iconLeft>
135 <n-spin :size="12" :show="loadingAssignee" content-class="flex flex-col justify-center">
136 <AssigneeIcon :assignee="alert.assigned_to" />
137 </n-spin>
138 </template>
139 <template #label>Assignee</template>
140 <template #value>
141 <div class="flex items-center gap-2">
142 {{ alert.assigned_to || "n/d" }}
143 <Icon :name="EditIcon" :size="13" />
144 </div>
145 </template>
146 </Badge>
147 </AlertAssignUser>
148
149 <Badge v-if="alert.customer_code" type="splitted" class="hidden! sm:flex!">
150 <template #label>Customer</template>
151 <template #value>
152 <div class="flex h-full items-center">
153 <code
154 class="text-primary cursor-pointer leading-none"
155 @click.stop="routeCustomer({ code: alert.customer_code }).navigate()"
156 >
157 #{{ alert.customer_code }}
158 <Icon :name="LinkIcon" :size="14" class="relative top-0.5" />
159 </code>
160 </div>
161 </template>
162 </Badge>
163
164 <Badge v-if="alert.assets?.length" type="splitted" fluid class="hidden! sm:flex!">
165 <template #label>Assets</template>
166 <template #value>
167 <div class="flex flex-wrap gap-1">
168 <AlertAssetItem v-for="asset of alert.assets" :key="asset.id" :asset badge />
169 </div>
170 </template>
171 </Badge>
172
173 <Badge type="splitted" class="hidden! sm:flex!">
174 <template #label>Linked Cases</template>
175 <template #value>
176 <AlertLinkedCases v-if="alert.linked_cases?.length" :alert @updated="updateAlert($event)" />
177 <span v-else>n/d</span>
178 </template>
179 </Badge>
180 </div>
181 </template>
182
183 <template v-if="alert && !compact" #footerMain>
184 <div class="flex flex-wrap items-center gap-3">
185 <Badge v-if="alert.alert_creation_time" type="splitted" :class="{ 'flex sm:hidden!': !compact }">
186 <template #iconLeft>
187 <Icon :name="TimeIcon" :size="16" />
188 </template>
189 <template #value>
190 {{ formatDate(alert.alert_creation_time, dFormats.datetime) }}
191 </template>
192 </Badge>
193
194 <n-tooltip trigger="hover">
195 <template #trigger>
196 <Badge type="splitted" class="xs:flex! hidden!">
197 <template #iconLeft>
198 <Icon :name="AssetsIcon" :size="16" />
199 </template>
200 <template #value>
201 {{ alert.assets?.length || 0 }}
202 </template>
203 </Badge>
204 </template>
205 Assets
206 </n-tooltip>
207
208 <n-tooltip trigger="hover">
209 <template #trigger>
210 <Badge type="splitted" class="xs:flex! hidden!">
211 <template #iconLeft>
212 <Icon :name="CommentsIcon" :size="16" />
213 </template>
214 <template #value>
215 {{ alert.comments?.length || 0 }}
216 </template>
217 </Badge>
218 </template>
219 Comments
220 </n-tooltip>
221
222 <n-tooltip trigger="hover">
223 <template #trigger>
224 <Badge type="splitted" class="xs:flex! hidden!">
225 <template #iconLeft>
226 <Icon :name="IoCsIcon" :size="16" />
227 </template>
228 <template #value>
229 {{ alert.iocs?.length || 0 }}
230 </template>
231 </Badge>
232 </template>
233 IoC
234 </n-tooltip>
235
236 <span
237 v-for="tag of alert.tags"
238 :key="tag.tag"
239 class="text-secondary"
240 :class="{ 'hidden sm:inline-block': !compact }"
241 >
242 #{{ tag.tag }}
243 </span>
244 </div>
245 </template>
246
247 <template v-if="alert && !compact" #footerExtra>
248 <n-button quaternary size="tiny" @click.stop="handleDelete()">Delete</n-button>
249 </template>
250 </CardEntity>
251
252 <n-modal
253 v-model:show="showDetails"
254 :style="{ maxWidth: 'min(850px, 90vw)', minHeight: 'min(540px, 90vh)', overflow: 'hidden' }"
255 display-directive="show"
256 >
257 <n-card
258 content-class="flex flex-col p-0!"
259 :title="alertNameTruncated"
260 closable
261 :bordered="false"
262 segmented
263 role="modal"
264 @close="closeDetails()"
265 >
266 <AlertDetails v-if="alert" :alert-data="alert" @deleted="emitDelete()" @updated="updateAlert($event)" />
267 </n-card>
268 </n-modal>
269 </div>
270 </template>
271
272 <script setup lang="ts">
273 import type { Alert } from "@/types/incidentManagement/alerts.d"
274 import _clone from "lodash/cloneDeep"
275 import _truncate from "lodash/truncate"
276 import { NButton, NCard, NCheckbox, NModal, NPopover, NSpin, NTooltip, useDialog, useMessage } from "naive-ui"
277 import { computed, defineAsyncComponent, onMounted, ref, toRefs, watch } from "vue"
278 import Api from "@/api"
279 import Badge from "@/components/common/Badge.vue"
280 import CardEntity from "@/components/common/cards/CardEntity.vue"
281 import Icon from "@/components/common/Icon.vue"
282 import { useNavigation } from "@/composables/useNavigation"
283 import { useSettingsStore } from "@/stores/settings"
284 import { formatDate } from "@/utils/format"
285 import AssigneeIcon from "../common/AssigneeIcon.vue"
286 import StatusIcon from "../common/StatusIcon.vue"
287 import AlertAssignUser from "./AlertAssignUser.vue"
288 import AlertDetails from "./AlertDetails.vue"
289 import AlertStatusSwitch from "./AlertStatusSwitch.vue"
290 import AlertTimeline from "./AlertTimeline.vue"
291 import { handleDeleteAlert } from "./utils"
292
293 const props = defineProps<{
294 alertData?: Alert
295 alertId?: number
296 compact?: boolean
297 embedded?: boolean
298 detailsOnMounted?: boolean
299 highlight?: boolean
300 selectable?: boolean
301 checked?: boolean
302 }>()
303
304 const emit = defineEmits<{
305 (e: "check", value: boolean): void
306 (e: "opened"): void
307 (e: "deleted"): void
308 (e: "updated", value: Alert): void
309 }>()
310
311 const AlertAssetItem = defineAsyncComponent(() => import("./AlertAsset.vue"))
312 const AlertLinkedCases = defineAsyncComponent(() => import("./AlertLinkedCases.vue"))
313
314 const { alertData, alertId, compact, embedded, detailsOnMounted, highlight, selectable, checked } = toRefs(props)
315
316 const InfoIcon = "carbon:information"
317 const LinkIcon = "carbon:launch"
318 const TimeIcon = "carbon:time"
319 const EditIcon = "uil:edit-alt"
320 const CommentsIcon = "carbon:chat"
321 const AssetsIcon = "carbon:document-security"
322 const IoCsIcon = "carbon:ibm-watson-discovery"
323
324 const { routeCustomer } = useNavigation()
325 const dialog = useDialog()
326 const message = useMessage()
327 const loading = ref(false)
328 const showDetails = ref(false)
329 const dFormats = useSettingsStore().dateFormat
330 const alert = ref<Alert | null>(null)
331 const alertNameTruncated = computed(() => _truncate(alert.value?.alert_name, { length: 50 }))
332
333 function updateAlert(updatedAlert: Alert) {
334 alert.value = updatedAlert
335 emit("updated", updatedAlert)
336 }
337
338 function getAlert(alertId: number) {
339 loading.value = true
340
341 Api.incidentManagement.alerts
342 .getAlert(alertId)
343 .then(res => {
344 if (res.data.success) {
345 alert.value = res.data?.alerts?.[0] || null
346 } else {
347 message.warning(res.data?.message || "An error occurred. Please try again later.")
348 }
349 })
350 .catch(err => {
351 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
352 })
353 .finally(() => {
354 loading.value = false
355 })
356 }
357
358 function handleDelete() {
359 if (alert.value) {
360 handleDeleteAlert({
361 alert: alert.value,
362 cbBefore: () => {
363 loading.value = true
364 },
365 cbSuccess: () => {
366 emitDelete()
367 },
368 cbAfter: () => {
369 loading.value = false
370 },
371 message,
372 dialog
373 })
374 }
375 }
376
377 function emitDelete() {
378 showDetails.value = false
379 emit("deleted")
380 }
381
382 function openDetails() {
383 emit("opened")
384 showDetails.value = true
385 }
386
387 function closeDetails() {
388 showDetails.value = false
389 }
390
391 watch(
392 [alertId, alertData],
393 () => {
394 if (alertId.value) {
395 getAlert(alertId.value)
396 } else if (alertData.value?.id && alertData.value?.linked_cases === undefined) {
397 getAlert(alertData.value.id)
398 } else if (alertData.value) {
399 alert.value = _clone(alertData.value)
400 }
401 },
402 { immediate: true }
403 )
404
405 onMounted(() => {
406 if (detailsOnMounted.value) {
407 openDetails()
408 }
409 })
410 </script>