main
vue 133 lines 4.36 KB
Raw
1 <template>
2 <n-spin :show="loading" class="min-h-40">
3 <div class="flex flex-col gap-4">
4 <div v-if="!loading && reports.length < 2">
5 <n-empty
6 description="Only one report exists for this alert. Replay with a different template to generate a second report to compare."
7 class="min-h-40 justify-center"
8 />
9 </div>
10
11 <template v-else>
12 <div class="text-secondary text-sm">
13 Side-by-side view of two investigations for alert
14 <code class="text-primary">#{{ alertId }}</code>
15 . Pick any two runs below — defaults to the current report on the left and the next-most-recent run
16 on the right.
17 </div>
18
19 <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
20 <!-- Side A -->
21 <div class="flex flex-col gap-3">
22 <n-form-item label="Version A" :show-feedback="false">
23 <n-select
24 v-model:value="idA"
25 :options="reportOptions"
26 :render-label="renderOption"
27 size="large"
28 />
29 </n-form-item>
30 <ReportColumn v-if="reportA" v-model:show-full-report="showFullReport" :report="reportA" />
31 </div>
32
33 <!-- Side B -->
34 <div class="flex flex-col gap-3">
35 <n-form-item label="Version B" :show-feedback="false">
36 <n-select
37 v-model:value="idB"
38 :options="reportOptions"
39 :render-label="renderOption"
40 size="large"
41 />
42 </n-form-item>
43 <ReportColumn v-if="reportB" v-model:show-full-report="showFullReport" :report="reportB" />
44 </div>
45 </div>
46 </template>
47 </div>
48 </n-spin>
49 </template>
50
51 <script setup lang="ts">
52 import type { AiAnalystReport } from "@/types/aiAnalyst.d"
53 import { NEmpty, NFormItem, NSelect, NSpin, useMessage } from "naive-ui"
54 import { computed, h, ref, toRefs, watch } from "vue"
55 import Api from "@/api"
56 import { useSettingsStore } from "@/stores/settings"
57 import { formatDate } from "@/utils/format"
58 import ReportColumn from "./AlertReportCompareColumn.vue"
59
60 const props = defineProps<{
61 alertId: number
62 currentReportId?: number
63 }>()
64
65 const { alertId, currentReportId } = toRefs(props)
66
67 const message = useMessage()
68 const loading = ref(false)
69 const dFormats = useSettingsStore().dateFormat
70 const reports = ref<AiAnalystReport[]>([])
71
72 const idA = ref<number | null>(null)
73 const idB = ref<number | null>(null)
74
75 const showFullReport = ref(false)
76
77 const reportOptions = computed(() =>
78 reports.value.map(r => ({
79 label: r.id.toString(),
80 value: r.id,
81 severity: r.severity_assessment,
82 created_at: r.created_at
83 }))
84 )
85
86 const reportA = computed(() => reports.value.find(r => r.id === idA.value) ?? null)
87 const reportB = computed(() => reports.value.find(r => r.id === idB.value) ?? null)
88
89 // Render option with created_at + severity so the picker shows meaningful
90 // distinctions between runs rather than just bare IDs.
91 function renderOption(option: { label: string; value: number; severity?: string | null; created_at?: string }) {
92 const ts = option.created_at ? String(formatDate(option.created_at, dFormats.datetime)) : ""
93 const sev = option.severity ? ` · ${option.severity}` : ""
94 return h("div", { class: "flex flex-col leading-none py-2" }, [
95 h("span", `#${option.label}${sev}`),
96 h("span", { class: "text-secondary text-xs" }, ts)
97 ])
98 }
99
100 async function loadReports() {
101 loading.value = true
102
103 try {
104 const res = await Api.aiAnalyst.getReportsByAlert(alertId.value)
105 if (res.data.success) {
106 // Newest first — backend already sorts by created_at desc, but we
107 // re-sort defensively in case that changes.
108 const sorted = [...(res.data.reports || [])].sort(
109 (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
110 )
111 reports.value = sorted
112 if (sorted.length >= 2) {
113 // Default A to the currentReportId if provided (so the user always
114 // sees "this report" on the left), otherwise newest.
115 const preferA = currentReportId?.value
116 ? sorted.find(r => r.id === currentReportId.value)?.id
117 : sorted[0].id
118 idA.value = preferA ?? sorted[0].id
119 idB.value = sorted.find(r => r.id !== idA.value)?.id ?? sorted[1].id
120 }
121 } else {
122 message.warning(res.data.message || "Failed to load reports")
123 }
124 } catch (err: unknown) {
125 const e = err as { response?: { data?: { message?: string } }; message?: string }
126 message.error(e.response?.data?.message || e.message || "Failed to load reports")
127 } finally {
128 loading.value = false
129 }
130 }
131
132 watch(alertId, () => loadReports(), { immediate: true })
133 </script>