main
vue 263 lines 6.75 KB
Raw
1 <template>
2 <div class="flex flex-col gap-4">
3 <div>
4 <Filters v-model:value="filters" class="w-auto!" />
5 </div>
6
7 <div class="flex flex-col gap-2">
8 <div ref="headerRef" class="flex items-center justify-between">
9 <Chip size="small" :value="loading ? 'Loading...' : pagination.total" label="items" />
10
11 <div class="flex items-center gap-2 whitespace-nowrap">
12 <n-pagination
13 v-model:page="pagination.page"
14 v-model:page-size="pagination.pageSize"
15 :page-slot
16 :show-size-picker
17 :page-sizes
18 :item-count="pagination.total"
19 :simple="simpleMode"
20 size="small"
21 />
22 </div>
23 </div>
24
25 <div class="grow overflow-hidden">
26 <n-data-table
27 bordered
28 :loading
29 size="small"
30 :data
31 :columns
32 :scroll-x="1400"
33 class="[&_.n-data-table-th\_\_title]:whitespace-nowrap"
34 >
35 <template #empty>
36 <n-empty description="No alerts found">
37 <template #extra>try changing the filters</template>
38 </n-empty>
39 </template>
40 </n-data-table>
41 </div>
42
43 <div class="flex justify-end">
44 <n-pagination
45 v-if="pagination.total > pagination.pageSize"
46 v-model:page="pagination.page"
47 :page-size="pagination.pageSize"
48 :item-count="pagination.total"
49 :page-slot="6"
50 size="small"
51 :simple="simpleMode"
52 />
53 </div>
54 </div>
55 </div>
56 </template>
57
58 <script setup lang="tsx">
59 import type { AxiosResponse } from "axios"
60 import type { DataTableColumns } from "naive-ui"
61 import type { AlertStatusUpdateSuccessPayload } from "@/components/alerts/AlertStatusSelect.vue"
62 import type { FiltersModel } from "@/components/alerts/Filters.vue"
63 import type { Alert, AlertsListResponse, AlertStatus } from "@/types/alerts"
64 import type { ApiError, CommonResponse, Pagination } from "@/types/common"
65 import { useDebounceFn, useElementSize } from "@vueuse/core"
66 import axios from "axios"
67 import { NDataTable, NEmpty, NPagination, NTag, useMessage } from "naive-ui"
68 import { computed, ref, useTemplateRef, watch } from "vue"
69 import Api from "@/api"
70 import AlertDetailsButton from "@/components/alerts/AlertDetailsButton.vue"
71 import AlertStatusSelect from "@/components/alerts/AlertStatusSelect.vue"
72 import Filters from "@/components/alerts/Filters.vue"
73 import Chip from "@/components/common/Chip.vue"
74 import Icon from "@/components/common/Icon.vue"
75 import { useCustomerFilterStore } from "@/stores/customerFilter"
76 import { useSettingsStore } from "@/stores/settings"
77 import { getApiErrorMessage, getStatusColor } from "@/utils"
78 import { formatDate } from "@/utils/format"
79
80 const message = useMessage()
81 const data = ref<Alert[]>([])
82 const loading = ref(false)
83 const dFormats = useSettingsStore().dateFormat
84 const customerFilterStore = useCustomerFilterStore()
85
86 const { width: headerWidthRef } = useElementSize(useTemplateRef("headerRef"))
87 const pageSizes = [10, 25, 50, 100]
88 const pageSlot = computed(() => (headerWidthRef.value < 800 ? 5 : 8))
89 const simpleMode = computed(() => headerWidthRef.value < 600)
90 const showSizePicker = ref(true)
91
92 const pagination = ref({
93 page: 1,
94 pageSize: pageSizes[1],
95 total: 0
96 })
97
98 // Filters
99 const filters = ref<FiltersModel>({
100 key: null,
101 value: null
102 })
103
104 const columns = computed<DataTableColumns<Alert>>(() => [
105 {
106 title: "Alert",
107 key: "alert",
108 fixed: simpleMode.value ? undefined : "left",
109 width: 380,
110 render: row => <div>{row.alert_name}</div>
111 },
112 {
113 title: "Source",
114 key: "source",
115 width: 180,
116 render: row => <div>{row.source}</div>
117 },
118 {
119 title: "Assets",
120 key: "assets",
121 ellipsis: true,
122 render: row => row.assets.map(asset => asset.asset_name).join(", ")
123 },
124 {
125 title: "Created",
126 key: "alert_creation_time",
127 width: 200,
128 render: row => <span class="font-mono text-sm">{formatDate(row.alert_creation_time, dFormats.datetime)}</span>
129 },
130 {
131 title: "Status",
132 key: "status",
133 width: 200,
134 render: row => {
135 return (
136 <div class="flex items-center gap-2">
137 <NTag
138 type={getStatusColor(row.status)}
139 round
140 class="p-1! [&_.n-tag\_\_icon]:m-0!"
141 v-slots={{
142 icon: () => <Icon name="carbon:circle-solid" />
143 }}
144 />
145 <AlertStatusSelect alertId={row.id} status={row.status} onSuccess={handleStatusUpdateSuccess} />
146 </div>
147 )
148 }
149 },
150 {
151 title: "Actions",
152 key: "actions",
153 width: 194,
154 render: row => {
155 return <AlertDetailsButton alertId={row.id} onStatusUpdated={handleStatusUpdateSuccess} />
156 }
157 }
158 ])
159
160 let abortController = new AbortController()
161
162 const loadAlerts = useDebounceFn(async () => {
163 loading.value = true
164
165 abortController?.abort()
166 abortController = new AbortController()
167
168 try {
169 let response: AxiosResponse<CommonResponse<AlertsListResponse>>
170
171 const paginationPayload: Pagination = {
172 page: pagination.value.page,
173 pageSize: pagination.value.pageSize,
174 order: "desc"
175 }
176
177 if (filters.value.key && filters.value.value) {
178 switch (filters.value.key) {
179 case "statuses":
180 response = await Api.alerts.getAlertsByStatus(
181 filters.value.value as AlertStatus,
182 paginationPayload,
183 abortController.signal,
184 customerFilterStore.queryCustomerCodes
185 )
186 break
187 case "sources":
188 response = await Api.alerts.getAlertsBySource(
189 filters.value.value,
190 paginationPayload,
191 abortController.signal,
192 customerFilterStore.queryCustomerCodes
193 )
194 break
195 case "assets":
196 response = await Api.alerts.getAlertsByAsset(
197 filters.value.value,
198 paginationPayload,
199 abortController.signal,
200 customerFilterStore.queryCustomerCodes
201 )
202 break
203 case "tags":
204 response = await Api.alerts.getAlertsByTag(
205 filters.value.value,
206 paginationPayload,
207 abortController.signal,
208 customerFilterStore.queryCustomerCodes
209 )
210 break
211 default:
212 response = await Api.alerts.getAlerts(
213 paginationPayload,
214 abortController.signal,
215 customerFilterStore.queryCustomerCodes
216 )
217 break
218 }
219 } else {
220 response = await Api.alerts.getAlerts(
221 paginationPayload,
222 abortController.signal,
223 customerFilterStore.queryCustomerCodes
224 )
225 }
226
227 data.value = response.data.alerts
228 pagination.value.total = response.data.total
229 loading.value = false
230 } catch (err) {
231 if (!axios.isCancel(err)) {
232 message.error(getApiErrorMessage(err as ApiError))
233 loading.value = false
234 }
235 }
236 }, 400)
237
238 function resetPage() {
239 pagination.value.page = 1
240 loadAlerts()
241 }
242
243 function handleStatusUpdateSuccess(payload: AlertStatusUpdateSuccessPayload) {
244 const alert = data.value.find(a => a.id === payload.alertId)
245 if (alert) {
246 alert.status = payload.status
247 }
248 }
249
250 watch(
251 [() => pagination.value.pageSize, () => filters.value.value, () => customerFilterStore.selectedCustomerCodes],
252 resetPage,
253 {
254 deep: true,
255 immediate: true
256 }
257 )
258
259 watch(() => pagination.value.page, loadAlerts, {
260 deep: true,
261 immediate: true
262 })
263 </script>