main
vue 332 lines 8.07 KB
Raw
1 <template>
2 <div class="sca-reports">
3 <div class="header mb-6 flex items-center justify-between">
4 <div>
5 <h2 class="mb-2 text-2xl font-bold">SCA Reports</h2>
6 <p class="text-secondary">Generate, manage, and download Security Configuration Assessment 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 :columns :data="reports" :loading :pagination :row-key="(row: SCAReport) => row.id" />
40 </n-card>
41
42 <!-- Generate Report Modal -->
43 <n-modal
44 v-model:show="showGenerateModal"
45 preset="card"
46 title="Generate SCA Report"
47 style="width: 600px; max-width: 90vw"
48 closable
49 >
50 <GenerateReportForm
51 :customers
52 :loading="generating"
53 @generate="handleGenerateReport"
54 @cancel="showGenerateModal = false"
55 />
56 </n-modal>
57
58 <!-- Delete Confirmation Modal -->
59 <n-modal
60 v-model:show="showDeleteModal"
61 preset="dialog"
62 title="Delete Report"
63 positive-text="Delete"
64 negative-text="Cancel"
65 @positive-click="confirmDelete"
66 @negative-click="showDeleteModal = false"
67 >
68 <p>Are you sure you want to delete the report "{{ reportToDelete?.report_name }}"?</p>
69 <p class="text-secondary mt-2">This action cannot be undone.</p>
70 </n-modal>
71 </div>
72 </template>
73
74 <script setup lang="ts">
75 // TODO-FE: refactor
76 import type { DataTableColumns } from "naive-ui"
77 import type { Customer } from "@/types/customers"
78 import type { SCAReport, SCAReportGenerateRequest } from "@/types/sca.d"
79 import { NButton, NCard, NDataTable, NModal, NSelect, NSpace, NTag, NTooltip, useMessage } from "naive-ui"
80 import { computed, h, onBeforeMount, ref } from "vue"
81 import Api from "@/api"
82 import Icon from "@/components/common/Icon.vue"
83 import { useSettingsStore } from "@/stores/settings"
84 import { formatBytes, formatDate } from "@/utils/format"
85 import GenerateReportForm from "./GenerateReportForm.vue"
86
87 const AddIcon = "carbon:document-add"
88 const RefreshIcon = "carbon:renew"
89 const DownloadIcon = "carbon:download"
90 const DeleteIcon = "carbon:trash-can"
91 const CheckIcon = "carbon:checkmark-filled"
92 const ErrorIcon = "carbon:warning-filled"
93 const LoadingIcon = "eos-icons:loading"
94
95 const message = useMessage()
96 const dFormats = useSettingsStore().dateFormat
97
98 const loading = ref(false)
99 const generating = ref(false)
100 const reports = ref<SCAReport[]>([])
101 const showGenerateModal = ref(false)
102 const showDeleteModal = ref(false)
103 const reportToDelete = ref<SCAReport | null>(null)
104 const filterCustomerCode = ref<string | null>(null)
105 const customers = ref<Array<{ label: string; value: string }>>([])
106
107 const customerOptions = computed(() => [...customers.value])
108
109 const pagination = {
110 pageSize: 20,
111 showSizePicker: true,
112 pageSizes: [10, 20, 50, 100]
113 }
114
115 const columns: DataTableColumns<SCAReport> = [
116 {
117 title: "Report Name",
118 key: "report_name",
119 ellipsis: {
120 tooltip: true
121 }
122 },
123 {
124 title: "Customer",
125 key: "customer_code",
126 width: 120
127 },
128 {
129 title: "Status",
130 key: "status",
131 width: 120,
132 render: (row: SCAReport) => {
133 const statusMap = {
134 completed: { type: "success", icon: CheckIcon, text: "Completed" },
135 processing: { type: "warning", icon: LoadingIcon, text: "Processing" },
136 failed: { type: "error", icon: ErrorIcon, text: "Failed" }
137 }
138 const status = statusMap[row.status]
139 return h(
140 NTag,
141 { type: status.type as any, size: "small" },
142 {
143 default: () => status.text,
144 icon: () => h(Icon, { name: status.icon, size: 14 })
145 }
146 )
147 }
148 },
149 {
150 title: "Policies",
151 key: "total_policies",
152 width: 100,
153 render: (row: SCAReport) => row.total_policies.toLocaleString()
154 },
155 {
156 title: "Checks",
157 key: "total_checks",
158 width: 140,
159 render: (row: SCAReport) => {
160 return h(
161 NTooltip,
162 {},
163 {
164 trigger: () => row.total_checks.toLocaleString(),
165 default: () =>
166 h("div", {}, [
167 h("div", {}, `Passed: ${row.passed_count}`),
168 h("div", {}, `Failed: ${row.failed_count}`),
169 h("div", {}, `Invalid: ${row.invalid_count}`)
170 ])
171 }
172 )
173 }
174 },
175 {
176 title: "File Size",
177 key: "file_size",
178 width: 120,
179 render: (row: SCAReport) => formatBytes(row.file_size)
180 },
181 {
182 title: "Generated",
183 key: "generated_at",
184 width: 180,
185 render: (row: SCAReport) => `${formatDate(row.generated_at, dFormats.datetime)}`
186 },
187 {
188 title: "Actions",
189 key: "actions",
190 width: 140,
191 render: (row: SCAReport) => {
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.sca.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 const response = await Api.customers.getCustomers()
247 const customerData = response.data.customers || []
248
249 customers.value = customerData.map((c: Customer) => ({
250 label: `${c.customer_name} (${c.customer_code})`,
251 value: c.customer_code
252 }))
253 } catch (error: any) {
254 console.error("Failed to load customers:", error)
255 message.error("Failed to load customers list")
256 }
257 }
258
259 async function handleGenerateReport(request: SCAReportGenerateRequest) {
260 generating.value = true
261 try {
262 const response = await Api.sca.generateReport(request)
263
264 if (response.data.success) {
265 message.success(response.data.message)
266 showGenerateModal.value = false
267 await loadReports()
268 } else {
269 message.error(response.data.error || "Failed to generate report")
270 }
271 } catch (error: any) {
272 message.error(error?.response?.data?.detail || "Failed to generate report")
273 } finally {
274 generating.value = false
275 }
276 }
277
278 async function handleDownload(report: SCAReport) {
279 try {
280 const response = await Api.sca.downloadReport(report.id)
281
282 const url = window.URL.createObjectURL(new Blob([response.data]))
283 const link = document.createElement("a")
284 link.href = url
285 link.setAttribute("download", report.file_name)
286 document.body.appendChild(link)
287 link.click()
288 link.remove()
289 window.URL.revokeObjectURL(url)
290
291 message.success("Report downloaded successfully")
292 } catch (error: any) {
293 message.error(error?.response?.data?.detail || "Failed to download report")
294 }
295 }
296
297 function handleDeleteClick(report: SCAReport) {
298 reportToDelete.value = report
299 showDeleteModal.value = true
300 }
301
302 async function confirmDelete() {
303 if (!reportToDelete.value) return
304
305 try {
306 const response = await Api.sca.deleteReport(reportToDelete.value.id)
307
308 if (response.data.success) {
309 message.success(response.data.message)
310 await loadReports()
311 } else {
312 message.error("Failed to delete report")
313 }
314 } catch (error: any) {
315 message.error(error?.response?.data?.detail || "Failed to delete report")
316 } finally {
317 showDeleteModal.value = false
318 reportToDelete.value = null
319 }
320 }
321
322 onBeforeMount(() => {
323 loadReports()
324 loadCustomers()
325 })
326 </script>
327
328 <style scoped>
329 .sca-reports {
330 padding: 20px;
331 }
332 </style>