main
vue 378 lines 10.2 KB
Raw
1 <template>
2 <n-tabs v-model:value="activeTab" type="segment" animated>
3 <n-tab-pane name="overview" tab="Overview">
4 <div class="flex flex-col gap-6 py-4">
5 <GitHubAuditConfigSummary :config />
6
7 <dl class="border-border bg-secondary flex flex-col gap-2 rounded-md border p-4">
8 <div class="flex items-baseline justify-between gap-3">
9 <dt class="text-secondary shrink-0 text-[10px] tracking-wider uppercase">Enabled</dt>
10 <dd class="font-mono text-xs" :class="config.enabled ? 'text-success' : 'text-warning'">
11 {{ config.enabled ? "Active" : "Disabled" }}
12 </dd>
13 </div>
14 <div class="flex items-baseline justify-between gap-3">
15 <dt class="text-secondary shrink-0 text-[10px] tracking-wider uppercase">Scheduled</dt>
16 <dd
17 class="font-mono text-xs"
18 :class="config.auto_audit_enabled ? 'text-success' : 'text-tertiary'"
19 >
20 {{ config.auto_audit_enabled ? "Enabled" : "Disabled" }}
21 </dd>
22 </div>
23 <div v-if="config.auto_audit_enabled" class="flex items-baseline justify-between gap-3">
24 <dt class="text-secondary shrink-0 text-[10px] tracking-wider uppercase">Cron</dt>
25 <dd class="text-default min-w-0 truncate text-right font-mono text-xs tabular-nums">
26 {{ config.audit_schedule_cron || "" }}
27 </dd>
28 </div>
29 </dl>
30
31 <div class="flex flex-col gap-2">
32 <p class="text-secondary text-[10px] font-medium tracking-widest uppercase">Audit scope</p>
33 <GitHubAuditScopeFlags :config size="large" class="bg-secondary" />
34 </div>
35
36 <div class="border-border flex flex-wrap justify-between gap-3 border-t pt-6">
37 <div class="flex flex-wrap gap-3">
38 <n-button @click="handleEdit">
39 <template #icon>
40 <Icon :name="EditIcon" />
41 </template>
42 Edit Configuration
43 </n-button>
44 <n-button quaternary :loading="running" @click="runAudit">
45 <template #icon>
46 <Icon :name="PlayIcon" />
47 </template>
48 Run Audit Now
49 </n-button>
50 </div>
51 <n-popconfirm @positive-click="deleteConfig">
52 <template #trigger>
53 <n-button type="error" ghost>
54 <template #icon>
55 <Icon :name="DeleteIcon" />
56 </template>
57 Delete
58 </n-button>
59 </template>
60 Are you sure you want to delete this configuration?
61 </n-popconfirm>
62 </div>
63 </div>
64 </n-tab-pane>
65
66 <n-tab-pane name="reports" tab="Reports">
67 <n-spin :show="loadingReports">
68 <div v-if="reports.length === 0 && !loadingReports" class="py-8 text-center">
69 <n-empty description="No reports yet">
70 <template #extra>
71 <n-button type="primary" @click="runAudit">Run your first audit</n-button>
72 </template>
73 </n-empty>
74 </div>
75
76 <div v-else class="flex flex-col gap-3">
77 <GitHubAuditReportCard
78 v-for="report in reports"
79 :key="report.id"
80 :report
81 :loading="loadingReportId === report.id"
82 @click="openReportDetail"
83 />
84
85 <n-pagination
86 v-if="totalReports > pageSize"
87 v-model:page="currentPage"
88 :page-size
89 :item-count="totalReports"
90 @update:page="loadReports"
91 />
92 </div>
93 </n-spin>
94 </n-tab-pane>
95
96 <n-tab-pane name="exclusions" tab="Exclusions">
97 <div class="mb-4">
98 <n-button type="primary" size="small" @click="showExclusionForm = true">
99 <template #icon>
100 <Icon :name="AddIcon" />
101 </template>
102 Add Exclusion
103 </n-button>
104 </div>
105
106 <n-spin :show="loadingExclusions">
107 <div v-if="exclusions.length === 0 && !loadingExclusions" class="py-8 text-center">
108 <n-empty description="No exclusions configured" />
109 </div>
110
111 <div v-else class="scrollbar-styled overflow-x-auto">
112 <n-table :single-line="false" class="min-w-150">
113 <thead>
114 <tr>
115 <th>Check</th>
116 <th>Resource</th>
117 <th>Reason</th>
118 <th>Expires</th>
119 <th class="text-right!">Actions</th>
120 </tr>
121 </thead>
122 <tbody>
123 <tr v-for="exclusion in exclusions" :key="exclusion.id">
124 <td>{{ exclusion.check_id }}</td>
125 <td>{{ exclusion.resource_name || "All" }}</td>
126 <td>{{ exclusion.reason }}</td>
127 <td>
128 {{
129 exclusion.expires_at
130 ? formatDate(exclusion.expires_at, dFormats.datetime)
131 : "Never"
132 }}
133 </td>
134 <td>
135 <div class="flex items-center justify-end gap-2">
136 <n-button
137 size="small"
138 ghost
139 type="error"
140 @click="deleteExclusion(exclusion.id)"
141 >
142 <template #icon>
143 <Icon :name="DeleteIcon" />
144 </template>
145 Delete
146 </n-button>
147 </div>
148 </td>
149 </tr>
150 </tbody>
151 </n-table>
152 </div>
153 </n-spin>
154 </n-tab-pane>
155 </n-tabs>
156
157 <n-modal v-model:show="showExclusionForm" preset="dialog" title="Add Exclusion" style="width: 500px">
158 <GitHubAuditExclusionForm
159 v-if="showExclusionForm"
160 :config-id="config.id"
161 @saved="onExclusionSaved"
162 @cancel="showExclusionForm = false"
163 />
164 </n-modal>
165
166 <n-drawer v-model:show="showReportDetail" :width="900" placement="right" class="max-w-[98vw]">
167 <n-drawer-content v-if="selectedReport" closable :native-scrollbar="false">
168 <template #header>
169 <div class="flex items-center gap-3">
170 <span>{{ selectedReport.report_name }}</span>
171 <n-tag :type="reportStatusType" size="small">{{ selectedReport.status }}</n-tag>
172 </div>
173 </template>
174
175 <GitHubAuditReportDetail
176 :report="selectedReport"
177 @close="showReportDetail = false"
178 @deleted="onReportDeleted"
179 />
180 </n-drawer-content>
181 </n-drawer>
182 </template>
183
184 <script setup lang="ts">
185 import type {
186 GitHubAuditCheckExclusion,
187 GitHubAuditConfig,
188 GitHubAuditReport,
189 GitHubAuditReportSummary
190 } from "@/types/githubAudit.d"
191 import {
192 NButton,
193 NDrawer,
194 NDrawerContent,
195 NEmpty,
196 NModal,
197 NPagination,
198 NPopconfirm,
199 NSpin,
200 NTable,
201 NTabPane,
202 NTabs,
203 NTag,
204 useMessage
205 } from "naive-ui"
206 import { computed, ref, watch } from "vue"
207 import Api from "@/api"
208 import Icon from "@/components/common/Icon.vue"
209 import { useSettingsStore } from "@/stores/settings"
210 import { formatDate } from "@/utils/format"
211 import GitHubAuditConfigSummary from "./GitHubAuditConfigSummary.vue"
212 import GitHubAuditExclusionForm from "./GitHubAuditExclusionForm.vue"
213 import GitHubAuditReportCard from "./GitHubAuditReportCard.vue"
214 import GitHubAuditReportDetail from "./GitHubAuditReportDetail.vue"
215 import GitHubAuditScopeFlags from "./GitHubAuditScopeFlags.vue"
216
217 const props = defineProps<{
218 config: GitHubAuditConfig
219 }>()
220
221 const emit = defineEmits<{
222 (e: "updated"): void
223 (e: "edit", config: GitHubAuditConfig): void
224 (e: "close"): void
225 }>()
226
227 const PlayIcon = "carbon:play"
228 const EditIcon = "carbon:edit"
229 const DeleteIcon = "ion:trash-outline"
230 const AddIcon = "ion:add"
231
232 const message = useMessage()
233 const dFormats = useSettingsStore().dateFormat
234
235 const activeTab = ref("overview")
236 const running = ref(false)
237
238 // Reports
239 const loadingReports = ref(false)
240 const reports = ref<GitHubAuditReportSummary[]>([])
241 const totalReports = ref(0)
242 const currentPage = ref(1)
243 const pageSize = 10
244 const showReportDetail = ref(false)
245 const selectedReport = ref<GitHubAuditReport | null>(null)
246 const loadingReportId = ref<number | null>(null)
247
248 // Exclusions
249 const loadingExclusions = ref(false)
250 const exclusions = ref<GitHubAuditCheckExclusion[]>([])
251 const showExclusionForm = ref(false)
252
253 const reportStatusType = computed(() => {
254 switch (selectedReport.value?.status) {
255 case "completed":
256 return "success"
257 case "running":
258 return "info"
259 case "failed":
260 return "error"
261 default:
262 return "default"
263 }
264 })
265
266 function resetAndLoad() {
267 activeTab.value = "overview"
268 reports.value = []
269 exclusions.value = []
270 currentPage.value = 1
271 loadReports()
272 loadExclusions()
273 }
274
275 watch(() => props.config.id, resetAndLoad, { immediate: true })
276
277 async function loadReports() {
278 if (loadingReports.value) {
279 return
280 }
281
282 loadingReports.value = true
283 try {
284 const response = await Api.githubAudit.getReports({
285 configId: props.config.id,
286 limit: pageSize,
287 offset: (currentPage.value - 1) * pageSize
288 })
289 reports.value = response.data.reports || []
290 totalReports.value = response.data.total_count || 0
291 } catch (error: any) {
292 console.error("Failed to load reports:", error)
293 message.error("Failed to load reports")
294 } finally {
295 loadingReports.value = false
296 }
297 }
298
299 async function loadExclusions() {
300 if (loadingExclusions.value) return
301
302 loadingExclusions.value = true
303 try {
304 const response = await Api.githubAudit.getExclusions(props.config.id)
305 exclusions.value = response.data.exclusions || []
306 } catch (error: any) {
307 console.error("Failed to load exclusions:", error)
308 message.error("Failed to load exclusions")
309 } finally {
310 loadingExclusions.value = false
311 }
312 }
313
314 async function runAudit() {
315 running.value = true
316 try {
317 await Api.githubAudit.runAuditFromConfig(props.config.id)
318 message.success("Audit completed successfully")
319 // Reload reports after audit completes
320 await loadReports()
321 emit("updated")
322 } catch (error: any) {
323 message.error(error.response?.data?.detail || "Failed to run audit")
324 } finally {
325 running.value = false
326 }
327 }
328
329 async function deleteConfig() {
330 try {
331 await Api.githubAudit.deleteConfig(props.config.id)
332 message.success("Configuration deleted")
333 emit("close")
334 emit("updated")
335 } catch (error: any) {
336 message.error(error.response?.data?.detail || "Failed to delete configuration")
337 }
338 }
339
340 async function deleteExclusion(exclusionId: number) {
341 try {
342 await Api.githubAudit.deleteExclusion(exclusionId)
343 message.success("Exclusion deleted")
344 loadExclusions()
345 } catch {
346 message.error("Failed to delete exclusion")
347 }
348 }
349
350 function handleEdit() {
351 emit("edit", props.config)
352 }
353
354 async function openReportDetail(report: GitHubAuditReportSummary) {
355 loadingReportId.value = report.id
356 try {
357 const response = await Api.githubAudit.getReport(report.id)
358 if (response.data.report) {
359 selectedReport.value = response.data.report
360 showReportDetail.value = true
361 }
362 } catch {
363 message.error("Failed to load report details")
364 } finally {
365 loadingReportId.value = null
366 }
367 }
368
369 function onReportDeleted() {
370 showReportDetail.value = false
371 loadReports()
372 }
373
374 function onExclusionSaved() {
375 showExclusionForm.value = false
376 loadExclusions()
377 }
378 </script>