main
vue 130 lines 3.77 KB
Raw
1 <template>
2 <n-spin :show="loading" class="min-h-[200px]">
3 <div v-if="reports.length" class="flex flex-col gap-4">
4 <div v-if="reports.length > 1" class="flex flex-col gap-2">
5 <n-select v-model:value="selectedIndex" :options="reportOptions" size="small" />
6 <div class="flex items-center justify-between gap-2">
7 <div>
8 <div v-if="report" class="flex flex-wrap items-center gap-2">
9 <n-tag v-if="report.severity_assessment" :type="severityType" size="small" round>
10 {{ report.severity_assessment }}
11 </n-tag>
12 <n-tag size="small" round>
13 {{ formatDate(report.created_at, dFormats.datetime) }}
14 </n-tag>
15 </div>
16 </div>
17
18 <span class="text-secondary shrink-0 text-xs">
19 {{ reports.length }} report{{ reports.length > 1 ? "s" : "" }}
20 </span>
21 </div>
22 </div>
23
24 <div v-if="report" class="flex flex-col gap-4">
25 <CardEntity v-if="report.summary" embedded size="small">
26 <div class="text-secondary mb-1 text-sm font-semibold uppercase">Summary</div>
27 <div class="text-sm">
28 {{ report.summary }}
29 </div>
30 </CardEntity>
31
32 <n-tabs type="line" animated :tabs-padding="0">
33 <n-tab-pane name="report" tab="Full Report" display-directive="show:lazy">
34 <div class="**:text-default pt-2 **:text-sm [&_*:last-child]:mb-0!">
35 <Markdown :source="report.report_markdown || 'No report content'" breaks />
36 </div>
37 </n-tab-pane>
38 <n-tab-pane name="actions" tab="Recommended Actions" display-directive="show:lazy">
39 <div class="**:text-default pt-2 **:text-sm [&_*:last-child]:mb-0!">
40 <Markdown :source="report.recommended_actions || 'No recommended actions'" breaks />
41 </div>
42 </n-tab-pane>
43 </n-tabs>
44 </div>
45 </div>
46
47 <div v-else-if="!loading" class="text-secondary flex flex-col items-center justify-center py-12 text-center">
48 <Icon name="carbon:bot" :size="40" class="mb-3 opacity-50" />
49 <p class="text-sm">No AI Analyst report available for this alert</p>
50 </div>
51 </n-spin>
52 </template>
53
54 <script setup lang="ts">
55 import type { TalonJobData } from "@/types/talon.d"
56 import { NSelect, NSpin, NTabPane, NTabs, NTag } from "naive-ui"
57 import { computed, onBeforeMount, ref } from "vue"
58 import Api from "@/api"
59 import CardEntity from "@/components/common/cards/CardEntity.vue"
60 import Icon from "@/components/common/Icon.vue"
61 import Markdown from "@/components/common/Markdown.vue"
62 import { useSettingsStore } from "@/stores/settings"
63 import dayjs from "@/utils/dayjs"
64 import { formatDate } from "@/utils/format"
65
66 const props = defineProps<{
67 alertId: number
68 }>()
69
70 const dFormats = useSettingsStore().dateFormat
71
72 const loading = ref(false)
73 const job = ref<TalonJobData | null>(null)
74 const selectedIndex = ref(0)
75
76 const reports = computed(() => {
77 const r = job.value?.reports || []
78 // Most recent first
79 return [...r].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
80 })
81
82 const reportOptions = computed(() =>
83 reports.value.map((r, i) => {
84 const chunks = []
85
86 chunks.push(dayjs(r.created_at).fromNow())
87
88 if (r.severity_assessment) {
89 chunks.push(r.severity_assessment)
90 }
91
92 if (r.job_id) {
93 chunks.push(`#${r.job_id}`)
94 }
95
96 return {
97 label: chunks.join(""),
98 value: i
99 }
100 })
101 )
102
103 const report = computed(() => reports.value[selectedIndex.value] || null)
104
105 const severityType = computed(() => {
106 const s = report.value?.severity_assessment?.toLowerCase()
107 if (s === "critical" || s === "high") return "error"
108 if (s === "medium") return "warning"
109 return "info"
110 })
111
112 function fetchJob() {
113 loading.value = true
114
115 Api.talon
116 .getJob(props.alertId)
117 .then(res => {
118 if (res.data.success && res.data.data) {
119 job.value = res.data.data
120 }
121 })
122 .finally(() => {
123 loading.value = false
124 })
125 }
126
127 onBeforeMount(() => {
128 fetchJob()
129 })
130 </script>