main
vue 386 lines 10 KB
Raw
1 <template>
2 <div class="vulnerability-reports">
3 <div class="header mb-6 flex items-center justify-between">
4 <div>
5 <h2 class="mb-2 text-2xl font-bold">Vulnerability Reports</h2>
6 <p class="text-secondary">Generate, manage, and download vulnerability reports</p>
7 </div>
8 <n-button type="primary" size="large" @click="showGenerateModal = true">
9 <template #icon>
10 <Icon :name="AddIcon" />
11 </template>
12 Generate Report
13 </n-button>
14 </div>
15
16 <!-- Filters -->
17 <n-card class="mb-6">
18 <div class="flex gap-4">
19 <n-select
20 v-model:value="filterCustomerCode"
21 :options="customerOptions"
22 placeholder="Filter by Customer"
23 clearable
24 filterable
25 class="flex-1"
26 @update:value="loadReports"
27 />
28 <n-button :loading @click="loadReports">
29 <template #icon>
30 <Icon :name="RefreshIcon" />
31 </template>
32 Refresh
33 </n-button>
34 </div>
35 </n-card>
36
37 <!-- Reports Table -->
38 <n-card>
39 <n-data-table
40 :columns
41 :data="reports"
42 :loading
43 :pagination
44 :row-key="(row: VulnerabilityReport) => row.id"
45 />
46 </n-card>
47
48 <!-- Generate Report Modal -->
49 <n-modal
50 v-model:show="showGenerateModal"
51 preset="card"
52 title="Generate Vulnerability Report"
53 style="width: 600px; max-width: 90vw"
54 closable
55 >
56 <GenerateReportForm
57 :customers
58 :loading="generating"
59 @generate="handleGenerateReport"
60 @cancel="showGenerateModal = false"
61 />
62 </n-modal>
63
64 <!-- Delete Confirmation Modal -->
65 <n-modal
66 v-model:show="showDeleteModal"
67 preset="dialog"
68 title="Delete Report"
69 positive-text="Delete"
70 negative-text="Cancel"
71 @positive-click="confirmDelete"
72 @negative-click="showDeleteModal = false"
73 >
74 <p>Are you sure you want to delete the report "{{ reportToDelete?.report_name }}"?</p>
75 <p class="text-secondary mt-2">This action cannot be undone.</p>
76 </n-modal>
77 </div>
78 </template>
79
80 <script setup lang="ts">
81 // TODO-FE: review VulnerabilityReports (this component)
82 import type { DataTableColumns } from "naive-ui"
83 import type { VulnerabilityReport, VulnerabilityReportGenerateRequest } from "@/types/vulnerabilities.d"
84 import { NButton, NCard, NDataTable, NModal, NSelect, NSpace, NTag, NTooltip, useMessage } from "naive-ui"
85 import { computed, h, onBeforeMount, ref } from "vue"
86 import Api from "@/api"
87 import Icon from "@/components/common/Icon.vue"
88 import { useSettingsStore } from "@/stores/settings"
89 import { formatBytes, formatDate } from "@/utils/format"
90 import GenerateReportForm from "./GenerateReportForm.vue"
91
92 const AddIcon = "carbon:document-add"
93 const RefreshIcon = "carbon:renew"
94 const DownloadIcon = "carbon:download"
95 const DeleteIcon = "carbon:trash-can"
96 const CheckIcon = "carbon:checkmark-filled"
97 const ErrorIcon = "carbon:warning-filled"
98 const LoadingIcon = "eos-icons:loading"
99
100 const message = useMessage()
101 const dFormats = useSettingsStore().dateFormat
102
103 const loading = ref(false)
104 const generating = ref(false)
105 const reports = ref<VulnerabilityReport[]>([])
106 const showGenerateModal = ref(false)
107 const showDeleteModal = ref(false)
108 const reportToDelete = ref<VulnerabilityReport | null>(null)
109 const filterCustomerCode = ref<string | null>(null)
110 const customers = ref<Array<{ label: string; value: string }>>([])
111
112 const customerOptions = computed(() => [...customers.value])
113
114 const pagination = {
115 pageSize: 20,
116 showSizePicker: true,
117 pageSizes: [10, 20, 50, 100]
118 }
119
120 const columns: DataTableColumns<VulnerabilityReport> = [
121 {
122 title: "Report Name",
123 key: "report_name",
124 ellipsis: {
125 tooltip: true
126 }
127 },
128 {
129 title: "Customer",
130 key: "customer_code",
131 width: 120
132 },
133 {
134 title: "Status",
135 key: "status",
136 width: 120,
137 render: (row: VulnerabilityReport) => {
138 const statusMap = {
139 completed: { type: "success", icon: CheckIcon, text: "Completed" },
140 processing: { type: "warning", icon: LoadingIcon, text: "Processing" },
141 failed: { type: "error", icon: ErrorIcon, text: "Failed" }
142 }
143 const status = statusMap[row.status]
144 return h(
145 NTag,
146 { type: status.type as any, size: "small" },
147 {
148 default: () => status.text,
149 icon: () => h(Icon, { name: status.icon, size: 14 })
150 }
151 )
152 }
153 },
154 {
155 title: "Vulnerabilities",
156 key: "total_vulnerabilities",
157 width: 140,
158 render: (row: VulnerabilityReport) => {
159 return h(
160 NTooltip,
161 {},
162 {
163 trigger: () => row.total_vulnerabilities.toLocaleString(),
164 default: () =>
165 h("div", {}, [
166 h("div", {}, `Critical: ${row.critical_count}`),
167 h("div", {}, `High: ${row.high_count}`),
168 h("div", {}, `Medium: ${row.medium_count}`),
169 h("div", {}, `Low: ${row.low_count}`)
170 ])
171 }
172 )
173 }
174 },
175 {
176 title: "File Size",
177 key: "file_size",
178 width: 120,
179 render: (row: VulnerabilityReport) => formatBytes(row.file_size)
180 },
181 {
182 title: "Generated",
183 key: "generated_at",
184 width: 180,
185 render: (row: VulnerabilityReport) => `${formatDate(row.generated_at, dFormats.datetime)}`
186 },
187 {
188 title: "Actions",
189 key: "actions",
190 width: 140,
191 render: (row: VulnerabilityReport) => {
192 return h(
193 NSpace,
194 { size: "small" },
195 {
196 default: () => [
197 row.status === "completed"
198 ? h(
199 NButton,
200 {
201 size: "small",
202 type: "primary",
203 onClick: () => handleDownload(row)
204 },
205 {
206 icon: () => h(Icon, { name: DownloadIcon })
207 }
208 )
209 : null,
210 h(
211 NButton,
212 {
213 size: "small",
214 type: "error",
215 onClick: () => handleDeleteClick(row)
216 },
217 {
218 icon: () => h(Icon, { name: DeleteIcon })
219 }
220 )
221 ]
222 }
223 )
224 }
225 }
226 ]
227
228 async function loadReports() {
229 loading.value = true
230 try {
231 const response = await Api.vulnerabilities.listReports(filterCustomerCode.value || undefined)
232 if (response.data.success) {
233 reports.value = response.data.reports
234 } else {
235 message.error(response.data.message || "Failed to load reports")
236 }
237 } catch (error: any) {
238 message.error(error?.response?.data?.detail || "Failed to load reports")
239 } finally {
240 loading.value = false
241 }
242 }
243
244 async function loadCustomers() {
245 try {
246 // Try the customers API endpoint
247 const response = await Api.customers.getCustomers()
248
249 // Handle different possible response structures
250 let customerData = response.data
251
252 // If response.data is wrapped in a data property
253 // TODO-FE: review this (customerData.data) - it's not in the type definition
254 // @ts-expect-error - customerData.data is not in the type definition
255 if (customerData.data) {
256 // @ts-expect-error - customerData.data is not in the type definition
257 customerData = customerData.data
258 }
259
260 // If it's an array, map it
261 if (Array.isArray(customerData)) {
262 customers.value = customerData.map((c: any) => ({
263 label: `${c.customer_name || c.name || c.customer_code} (${c.customer_code})`,
264 value: c.customer_code
265 }))
266 } else if (customerData.customers && Array.isArray(customerData.customers)) {
267 // If customers are in a customers property
268 customers.value = customerData.customers.map((c: any) => ({
269 label: `${c.customer_name || c.name || c.customer_code} (${c.customer_code})`,
270 value: c.customer_code
271 }))
272 } else {
273 console.warn("Unexpected customer data structure:", customerData)
274 message.warning("Customer data format unexpected")
275 }
276 } catch (error: any) {
277 console.error("Failed to load customers:", error)
278 message.error("Failed to load customers list")
279 }
280 }
281
282 async function handleGenerateReport(request: VulnerabilityReportGenerateRequest) {
283 generating.value = true
284 try {
285 // Use background generation for better UX
286 const response = await Api.vulnerabilities.generateReportBackground(request)
287
288 if (response.data.success) {
289 message.success(response.data.message)
290 showGenerateModal.value = false
291
292 // Add the new processing report to the list
293 await loadReports()
294
295 // Start polling for status updates
296 startStatusPolling(response.data.report_id)
297 } else {
298 message.error("Failed to queue report generation")
299 }
300 } catch (error: any) {
301 message.error(error?.response?.data?.detail || "Failed to generate report")
302 } finally {
303 generating.value = false
304 }
305 }
306
307 function startStatusPolling(reportId: number) {
308 const pollInterval = setInterval(async () => {
309 try {
310 await loadReports()
311
312 const report = reports.value.find(r => r.id === reportId)
313 if (report && report.status !== "processing") {
314 clearInterval(pollInterval)
315
316 if (report.status === "completed") {
317 message.success(`Report "${report.report_name}" completed successfully`)
318 } else if (report.status === "failed") {
319 message.error(`Report "${report.report_name}" failed: ${report.error_message}`)
320 }
321 }
322 } catch {
323 clearInterval(pollInterval)
324 }
325 }, 3000) // Poll every 3 seconds
326
327 // Stop polling after 5 minutes
328 setTimeout(clearInterval, 300000, pollInterval)
329 }
330
331 async function handleDownload(report: VulnerabilityReport) {
332 try {
333 const response = await Api.vulnerabilities.downloadReport(report.id)
334
335 // Create download link
336 const url = window.URL.createObjectURL(new Blob([response.data]))
337 const link = document.createElement("a")
338 link.href = url
339 link.setAttribute("download", report.file_name)
340 document.body.appendChild(link)
341 link.click()
342 link.remove()
343 window.URL.revokeObjectURL(url)
344
345 message.success("Report downloaded successfully")
346 } catch (error: any) {
347 message.error(error?.response?.data?.detail || "Failed to download report")
348 }
349 }
350
351 function handleDeleteClick(report: VulnerabilityReport) {
352 reportToDelete.value = report
353 showDeleteModal.value = true
354 }
355
356 async function confirmDelete() {
357 if (!reportToDelete.value) return
358
359 try {
360 const response = await Api.vulnerabilities.deleteReport(reportToDelete.value.id)
361
362 if (response.data.success) {
363 message.success(response.data.message)
364 await loadReports()
365 } else {
366 message.error("Failed to delete report")
367 }
368 } catch (error: any) {
369 message.error(error?.response?.data?.detail || "Failed to delete report")
370 } finally {
371 showDeleteModal.value = false
372 reportToDelete.value = null
373 }
374 }
375
376 onBeforeMount(() => {
377 loadReports()
378 loadCustomers()
379 })
380 </script>
381
382 <style scoped>
383 .vulnerability-reports {
384 padding: 20px;
385 }
386 </style>