main
vue 220 lines 5.12 KB
Raw
1 <template>
2 <div class="patch-tuesday-stats">
3 <n-grid :x-gap="16" :y-gap="16" cols="1 s:2 m:4 l:5">
4 <!-- Total CVEs -->
5 <n-grid-item>
6 <n-card size="small" :bordered="false" class="stat-card">
7 <div class="stat-content">
8 <div class="stat-icon total">
9 <Icon :name="ShieldIcon" :size="24" />
10 </div>
11 <div class="stat-info">
12 <span class="stat-value">{{ summary?.unique_cves ?? "-" }}</span>
13 <span class="stat-label">Unique CVEs</span>
14 </div>
15 </div>
16 </n-card>
17 </n-grid-item>
18
19 <!-- P0 Emergency -->
20 <n-grid-item>
21 <n-card size="small" :bordered="false" class="stat-card priority-p0">
22 <div class="stat-content">
23 <div class="stat-icon p0">
24 <Icon :name="AlertIcon" :size="24" />
25 </div>
26 <div class="stat-info">
27 <span class="stat-value">{{ summary?.by_priority?.P0 ?? 0 }}</span>
28 <span class="stat-label">P0 Emergency</span>
29 </div>
30 </div>
31 </n-card>
32 </n-grid-item>
33
34 <!-- P1 High -->
35 <n-grid-item>
36 <n-card size="small" :bordered="false" class="stat-card priority-p1">
37 <div class="stat-content">
38 <div class="stat-icon p1">
39 <Icon :name="UrgentIcon" :size="24" />
40 </div>
41 <div class="stat-info">
42 <span class="stat-value">{{ summary?.by_priority?.P1 ?? 0 }}</span>
43 <span class="stat-label">P1 High</span>
44 </div>
45 </div>
46 </n-card>
47 </n-grid-item>
48
49 <!-- P2 Medium -->
50 <n-grid-item>
51 <n-card size="small" :bordered="false" class="stat-card priority-p2">
52 <div class="stat-content">
53 <div class="stat-icon p2">
54 <Icon :name="InfoIcon" :size="24" />
55 </div>
56 <div class="stat-info">
57 <span class="stat-value">{{ summary?.by_priority?.P2 ?? 0 }}</span>
58 <span class="stat-label">P2 Medium</span>
59 </div>
60 </div>
61 </n-card>
62 </n-grid-item>
63
64 <!-- P3 Low -->
65 <n-grid-item>
66 <n-card size="small" :bordered="false" class="stat-card priority-p3">
67 <div class="stat-content">
68 <div class="stat-icon p3">
69 <Icon :name="CheckIcon" :size="24" />
70 </div>
71 <div class="stat-info">
72 <span class="stat-value">{{ summary?.by_priority?.P3 ?? 0 }}</span>
73 <span class="stat-label">P3 Low</span>
74 </div>
75 </div>
76 </n-card>
77 </n-grid-item>
78 </n-grid>
79
80 <!-- Additional Info Bar -->
81 <div v-if="summary" class="info-bar mt-4">
82 <n-space :size="24">
83 <span class="info-item">
84 <Icon :name="CalendarIcon" class="mr-1" />
85 Patch Tuesday:
86 <strong>{{ formatDate(summary.patch_tuesday_date) }}</strong>
87 </span>
88 <span class="info-item">
89 <Icon :name="DatabaseIcon" class="mr-1" />
90 Total Records:
91 <strong>{{ summary.total_records }}</strong>
92 </span>
93 <span class="info-item">
94 <Icon :name="ClockIcon" class="mr-1" />
95 Generated:
96 <strong>{{ formatDateTime(summary.generated_utc) }}</strong>
97 </span>
98 </n-space>
99 </div>
100 </div>
101 </template>
102
103 <script setup lang="ts">
104 // TODO-FE: refactor
105 import type { PatchTuesdaySummary } from "@/types/patchTuesday.d"
106 import { NCard, NGrid, NGridItem, NSpace } from "naive-ui"
107 import Icon from "@/components/common/Icon.vue"
108
109 defineProps<{
110 summary: PatchTuesdaySummary | null
111 loading?: boolean
112 }>()
113 const AlertIcon = "carbon:warning"
114 const CalendarIcon = "carbon:calendar"
115 const CheckIcon = "carbon:checkmark-filled"
116 const ClockIcon = "carbon:time"
117 const DatabaseIcon = "carbon:data-base"
118 const InfoIcon = "carbon:information"
119 const ShieldIcon = "carbon:security"
120 const UrgentIcon = "carbon:warning-hex"
121
122 function formatDate(dateStr: string): string {
123 if (!dateStr) return "-"
124 return new Date(dateStr).toLocaleDateString("en-US", {
125 year: "numeric",
126 month: "long",
127 day: "numeric"
128 })
129 }
130
131 function formatDateTime(dateStr: string): string {
132 if (!dateStr) return "-"
133 return new Date(dateStr).toLocaleString("en-US", {
134 month: "short",
135 day: "numeric",
136 hour: "2-digit",
137 minute: "2-digit"
138 })
139 }
140 </script>
141
142 <style scoped lang="scss">
143 .patch-tuesday-stats {
144 .stat-card {
145 border-radius: 8px;
146 background: var(--bg-secondary-color);
147
148 .stat-content {
149 display: flex;
150 align-items: center;
151 gap: 12px;
152 }
153
154 .stat-icon {
155 width: 48px;
156 height: 48px;
157 border-radius: 8px;
158 display: flex;
159 align-items: center;
160 justify-content: center;
161
162 &.total {
163 background: rgba(99, 102, 241, 0.15);
164 color: #6366f1;
165 }
166
167 &.p0 {
168 background: rgba(239, 68, 68, 0.15);
169 color: #ef4444;
170 }
171
172 &.p1 {
173 background: rgba(249, 115, 22, 0.15);
174 color: #f97316;
175 }
176
177 &.p2 {
178 background: rgba(234, 179, 8, 0.15);
179 color: #eab308;
180 }
181
182 &.p3 {
183 background: rgba(34, 197, 94, 0.15);
184 color: #22c55e;
185 }
186 }
187
188 .stat-info {
189 display: flex;
190 flex-direction: column;
191
192 .stat-value {
193 font-size: 1.5rem;
194 font-weight: 700;
195 line-height: 1.2;
196 }
197
198 .stat-label {
199 font-size: 0.75rem;
200 opacity: 0.7;
201 text-transform: uppercase;
202 letter-spacing: 0.5px;
203 }
204 }
205 }
206
207 .info-bar {
208 padding: 12px 16px;
209 background: var(--bg-secondary-color);
210 border-radius: 8px;
211
212 .info-item {
213 display: inline-flex;
214 align-items: center;
215 font-size: 0.875rem;
216 opacity: 0.8;
217 }
218 }
219 }
220 </style>