main
vue 245 lines 6.69 KB
Raw
1 <template>
2 <div class="@container flex flex-col gap-6">
3 <!-- Summary Section -->
4 <n-card title="Summary" size="small">
5 <div class="grid gap-4 @sm:grid-cols-2 @xl:grid-cols-4">
6 <n-statistic label="Score">
7 <template #default>
8 <span :class="scoreClass">{{ report.score.toFixed(1) }}%</span>
9 </template>
10 <template #suffix>
11 <GitHubAuditGradeBadge :grade="report.grade" class="relative -top-0.5" />
12 </template>
13 </n-statistic>
14
15 <n-statistic label="Repos Audited" :value="report.total_repos_audited" />
16
17 <n-statistic label="Checks Passed">
18 <template #default>{{ report.passed_checks }} / {{ report.total_checks }}</template>
19 </n-statistic>
20
21 <n-statistic label="Duration">
22 <template #default>{{ report.audit_duration_seconds?.toFixed(1) || "N/A" }}s</template>
23 </n-statistic>
24 </div>
25 </n-card>
26
27 <!-- Findings Summary -->
28 <n-card title="Findings by Severity" size="small">
29 <div class="border-border divide-border grid grid-cols-4 divide-x rounded-md border">
30 <div
31 v-for="stat in severityStats"
32 :key="stat.key"
33 class="flex flex-col items-center gap-1 px-3 py-4"
34 :class="stat.bgClass"
35 >
36 <p class="text-secondary text-[10px] tracking-wider uppercase">{{ stat.label }}</p>
37 <p class="font-mono text-2xl leading-none font-bold tabular-nums" :class="stat.numberClass">
38 {{ report[stat.key] }}
39 </p>
40 </div>
41 </div>
42 </n-card>
43
44 <!-- Top Findings -->
45 <n-card v-if="report.top_findings && report.top_findings.length > 0" title="Top Findings" size="small">
46 <div class="scrollbar-styled overflow-x-auto">
47 <n-table :single-line="false" size="small" class="min-w-150">
48 <thead>
49 <tr>
50 <th>Severity</th>
51 <th>Check</th>
52 <th>Resource</th>
53 <th>Description</th>
54 </tr>
55 </thead>
56 <tbody>
57 <tr v-for="(finding, index) in report.top_findings" :key="index">
58 <td>
59 <n-tag :type="getSeverityType(finding.severity)" size="small">
60 {{ finding.severity }}
61 </n-tag>
62 </td>
63 <td>{{ finding.check_name }}</td>
64 <td>{{ finding.resource_name || "N/A" }}</td>
65 <td>{{ finding.description }}</td>
66 </tr>
67 </tbody>
68 </n-table>
69 </div>
70 </n-card>
71
72 <!-- Organization Results -->
73 <n-card v-if="report.full_report?.organization_results" title="Organization Settings" size="small">
74 <div class="scrollbar-styled overflow-x-auto">
75 <n-table :single-line="false" size="small" class="min-w-150">
76 <thead>
77 <tr>
78 <th>Status</th>
79 <th>Check</th>
80 <th>Description</th>
81 </tr>
82 </thead>
83 <tbody>
84 <tr v-for="check in report.full_report.organization_results.checks" :key="check.check_id">
85 <td>
86 <n-tag :type="getStatusType(check.status)" size="small">
87 {{ check.status }}
88 </n-tag>
89 </td>
90 <td>{{ check.check_name }}</td>
91 <td>{{ check.description }}</td>
92 </tr>
93 </tbody>
94 </n-table>
95 </div>
96 </n-card>
97
98 <!-- Repository Results -->
99 <n-card v-if="report.full_report?.repository_results?.length" title="Repository Results" size="small">
100 <n-collapse>
101 <n-collapse-item
102 v-for="repo in report.full_report.repository_results"
103 :key="repo.repo_name"
104 :title="repo.repo_name"
105 >
106 <template #header-extra>
107 <n-space>
108 <n-tag type="success" size="small">{{ repo.passed_count }} passed</n-tag>
109 <n-tag v-if="repo.failed_count > 0" type="error" size="small">
110 {{ repo.failed_count }} failed
111 </n-tag>
112 </n-space>
113 </template>
114
115 <div class="scrollbar-styled overflow-x-auto">
116 <n-table :single-line="false" size="small" class="min-w-150">
117 <thead>
118 <tr>
119 <th>Status</th>
120 <th>Check</th>
121 <th>Description</th>
122 </tr>
123 </thead>
124 <tbody>
125 <tr v-for="check in repo.checks" :key="check.check_id">
126 <td>
127 <n-tag :type="getStatusType(check.status)" size="small">
128 {{ check.status }}
129 </n-tag>
130 </td>
131 <td>{{ check.check_name }}</td>
132 <td>{{ check.description }}</td>
133 </tr>
134 </tbody>
135 </n-table>
136 </div>
137 </n-collapse-item>
138 </n-collapse>
139 </n-card>
140
141 <!-- Error Message -->
142 <n-alert v-if="report.error_message" title="Error" type="error">
143 {{ report.error_message }}
144 </n-alert>
145
146 <div class="border-border flex justify-between gap-3 border-t pt-4">
147 <n-popconfirm @positive-click="deleteReport">
148 <template #trigger>
149 <n-button type="error" ghost>Delete Report</n-button>
150 </template>
151 Are you sure you want to delete this report?
152 </n-popconfirm>
153 <n-button @click="emit('close')">Close</n-button>
154 </div>
155 </div>
156 </template>
157
158 <script setup lang="ts">
159 import type { GitHubAuditReport } from "@/types/githubAudit.d"
160 import {
161 NAlert,
162 NButton,
163 NCard,
164 NCollapse,
165 NCollapseItem,
166 NPopconfirm,
167 NSpace,
168 NStatistic,
169 NTable,
170 NTag,
171 useMessage
172 } from "naive-ui"
173 import { computed } from "vue"
174 import Api from "@/api"
175 import { AuditStatus, SeverityLevel } from "@/types/githubAudit.d"
176 import GitHubAuditGradeBadge from "./GitHubAuditGradeBadge.vue"
177
178 const props = defineProps<{
179 report: GitHubAuditReport
180 }>()
181
182 const emit = defineEmits<{
183 (e: "close"): void
184 (e: "deleted"): void
185 }>()
186
187 const message = useMessage()
188
189 const severityStats = [
190 { key: "critical_findings" as const, label: "Critical", numberClass: "text-error", bgClass: "bg-error/5" },
191 { key: "high_findings" as const, label: "High", numberClass: "text-warning", bgClass: "bg-warning/5" },
192 { key: "medium_findings" as const, label: "Medium", numberClass: "text-info", bgClass: "bg-info/5" },
193 { key: "low_findings" as const, label: "Low", numberClass: "text-success", bgClass: "bg-success/5" }
194 ]
195
196 const scoreClass = computed(() => {
197 const score = props.report.score ?? 0
198 if (score >= 80) return "text-success"
199 if (score >= 60) return "text-warning"
200 return "text-error"
201 })
202
203 function getStatusType(status: AuditStatus | string) {
204 switch (status) {
205 case AuditStatus.PASS:
206 case "pass":
207 return "success"
208 case AuditStatus.FAIL:
209 case "fail":
210 return "error"
211 case AuditStatus.WARNING:
212 case "warning":
213 return "warning"
214 default:
215 return "default"
216 }
217 }
218
219 function getSeverityType(severity: SeverityLevel | string) {
220 switch (severity) {
221 case SeverityLevel.CRITICAL:
222 case "critical":
223 return "error"
224 case SeverityLevel.HIGH:
225 case "high":
226 return "warning"
227 case SeverityLevel.MEDIUM:
228 case "medium":
229 return "info"
230 default:
231 return "default"
232 }
233 }
234
235 async function deleteReport() {
236 try {
237 await Api.githubAudit.deleteReport(props.report.id)
238 message.success("Report deleted")
239 emit("deleted")
240 emit("close")
241 } catch {
242 message.error("Failed to delete report")
243 }
244 }
245 </script>