main
vue 227 lines 5.84 KB
Raw
1 <template>
2 <div class="patch-tuesday-list">
3 <!-- Header -->
4 <div class="header mb-4 flex items-center justify-between gap-4">
5 <div class="flex items-center gap-3">
6 <Icon :name="CalendarIcon" :size="26" class="text-primary-color" />
7 <h1 class="text-2xl font-bold">Microsoft Patch Tuesday</h1>
8 </div>
9 <div class="flex items-center gap-2">
10 <n-button :loading :disabled="loading" type="primary" secondary @click="fetchData">
11 <template #icon>
12 <Icon :name="RefreshIcon" />
13 </template>
14 Refresh
15 </n-button>
16 </div>
17 </div>
18
19 <!-- Stats Cards -->
20 <PatchTuesdayStats :summary :loading class="mb-4" />
21
22 <!-- Filters -->
23 <PatchTuesdayFilters
24 v-model:filters="filters"
25 :cycles="availableCycles"
26 :families="availableFamilies"
27 :loading
28 class="mb-4"
29 @update:filters="handleFiltersChange"
30 />
31
32 <!-- Items List -->
33 <n-spin :show="loading">
34 <div v-if="filteredItems.length > 0" class="items-grid">
35 <PatchTuesdayCard
36 v-for="item in paginatedItems"
37 :key="`${item.cve}-${item.affected.product}`"
38 :item
39 @click="openItemDetail(item)"
40 />
41 </div>
42
43 <n-empty
44 v-else-if="!loading"
45 description="No vulnerabilities found for the selected filters"
46 class="py-12"
47 />
48 </n-spin>
49
50 <!-- Pagination -->
51 <div v-if="filteredItems.length > pageSize" class="mt-4 flex justify-center">
52 <n-pagination v-model:page="currentPage" :page-count="totalPages" :page-size show-quick-jumper />
53 </div>
54
55 <!-- Detail Drawer -->
56 <n-drawer v-model:show="showDetail" :width="600" placement="right">
57 <n-drawer-content :title="selectedItem?.cve || 'Vulnerability Details'" closable>
58 <PatchTuesdayDetail v-if="selectedItem" :item="selectedItem" />
59 </n-drawer-content>
60 </n-drawer>
61 </div>
62 </template>
63
64 <script setup lang="ts">
65 // TODO-FE: refactor
66 import type { PatchTuesdayFilters as FiltersType } from "./types"
67 import type { PatchTuesdayItem, PatchTuesdaySummary } from "@/types/patchTuesday.d"
68 import { NButton, NDrawer, NDrawerContent, NEmpty, NPagination, NSpin, useMessage } from "naive-ui"
69 import { computed, onBeforeMount, ref, watch } from "vue"
70 import patchTuesdayApi from "@/api/endpoints/patchTuesday"
71 import Icon from "@/components/common/Icon.vue"
72 import PatchTuesdayCard from "./PatchTuesdayCard.vue"
73 import PatchTuesdayDetail from "./PatchTuesdayDetail.vue"
74 import PatchTuesdayFilters from "./PatchTuesdayFilters.vue"
75 import PatchTuesdayStats from "./PatchTuesdayStats.vue"
76
77 const CalendarIcon = "carbon:calendar"
78 const RefreshIcon = "carbon:refresh"
79
80 const message = useMessage()
81
82 // State
83 const loading = ref(false)
84 const items = ref<PatchTuesdayItem[]>([])
85 const summary = ref<PatchTuesdaySummary | null>(null)
86 const availableCycles = ref<string[]>([])
87 const currentPage = ref(1)
88 const pageSize = 24
89 const showDetail = ref(false)
90 const selectedItem = ref<PatchTuesdayItem | null>(null)
91
92 const filters = ref<FiltersType>({
93 cycle: "",
94 priority: null,
95 family: null,
96 severity: null,
97 searchQuery: "",
98 kevOnly: false
99 })
100
101 // Computed
102 const availableFamilies = computed(() => {
103 if (!summary.value?.by_family) return []
104 return Object.keys(summary.value.by_family).sort()
105 })
106
107 const filteredItems = computed(() => {
108 let result = [...items.value]
109
110 // Filter by priority
111 if (filters.value.priority) {
112 result = result.filter(item => item.prioritization.priority === filters.value.priority)
113 }
114
115 // Filter by family
116 if (filters.value.family) {
117 result = result.filter(item => item.affected.family === filters.value.family)
118 }
119
120 // Filter by severity
121 if (filters.value.severity) {
122 result = result.filter(item => item.severity?.toLowerCase() === filters.value.severity?.toLowerCase())
123 }
124
125 // Filter KEV only
126 if (filters.value.kevOnly) {
127 result = result.filter(item => item.kev.in_kev)
128 }
129
130 // Search filter
131 if (filters.value.searchQuery) {
132 const query = filters.value.searchQuery.toLowerCase()
133 result = result.filter(
134 item =>
135 item.cve.toLowerCase().includes(query) ||
136 item.title?.toLowerCase().includes(query) ||
137 item.affected.product.toLowerCase().includes(query)
138 )
139 }
140
141 return result
142 })
143
144 const totalPages = computed(() => Math.ceil(filteredItems.value.length / pageSize))
145
146 const paginatedItems = computed(() => {
147 const start = (currentPage.value - 1) * pageSize
148 return filteredItems.value.slice(start, start + pageSize)
149 })
150
151 // Methods
152 async function fetchCycles() {
153 try {
154 const response = await patchTuesdayApi.getCycles()
155 if (response.data.success) {
156 availableCycles.value = response.data.cycles
157 if (!filters.value.cycle && response.data.current_cycle) {
158 filters.value.cycle = response.data.current_cycle
159 }
160 }
161 } catch (error) {
162 console.error("Failed to fetch cycles:", error)
163 }
164 }
165
166 async function fetchData() {
167 if (!filters.value.cycle) return
168
169 loading.value = true
170 try {
171 const response = await patchTuesdayApi.getPatchTuesday({
172 cycle: filters.value.cycle,
173 include_epss: true,
174 include_kev: true
175 })
176
177 if (response.data.success) {
178 items.value = response.data.items
179 summary.value = response.data.summary
180 } else {
181 message.error(response.data.message || "Failed to fetch Patch Tuesday data")
182 }
183 } catch (error: unknown) {
184 const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"
185 message.error(`Error fetching data: ${errorMessage}`)
186 } finally {
187 loading.value = false
188 }
189 }
190
191 function handleFiltersChange() {
192 currentPage.value = 1
193 if (filters.value.cycle) {
194 fetchData()
195 }
196 }
197
198 function openItemDetail(item: PatchTuesdayItem) {
199 selectedItem.value = item
200 showDetail.value = true
201 }
202
203 // Watch for cycle changes
204 watch(
205 () => filters.value.cycle,
206 newCycle => {
207 if (newCycle) {
208 fetchData()
209 }
210 }
211 )
212
213 // Lifecycle
214 onBeforeMount(async () => {
215 await fetchCycles()
216 })
217 </script>
218
219 <style scoped lang="scss">
220 .patch-tuesday-list {
221 .items-grid {
222 display: grid;
223 grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
224 gap: 16px;
225 }
226 }
227 </style>