| 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> |