main
vue 225 lines 5.67 KB
Raw
1 <template>
2 <div class="healthcheck-list">
3 <div ref="header" class="header flex items-center justify-end gap-2">
4 <div class="info flex grow gap-2">
5 <n-popover overlap placement="bottom-start">
6 <template #trigger>
7 <div class="bg-default rounded-lg">
8 <n-button size="small" class="cursor-help!">
9 <template #icon>
10 <Icon :name="InfoIcon" />
11 </template>
12 </n-button>
13 </div>
14 </template>
15 <div class="flex flex-col gap-2">
16 <div class="box">
17 Total :
18 <code>{{ stats?.total_count || 0 }}</code>
19 </div>
20 <div class="box text-error-500">
21 Active :
22 <code>{{ stats?.active_alerts_count || 0 }}</code>
23 </div>
24 <div class="box text-warning-500">
25 Critical :
26 <code>{{ criticalTotal }}</code>
27 </div>
28 <div class="box text-success-500">
29 Cleared :
30 <code>{{ stats?.cleared_alerts_count || 0 }}</code>
31 </div>
32 </div>
33 </n-popover>
34 </div>
35 <div class="flex items-center gap-2">
36 <n-select
37 v-model:value="checkNameFilter"
38 :options="checkNameOptions"
39 size="small"
40 class="w-48!"
41 placeholder="Filter by check name"
42 clearable
43 filterable
44 @update:value="getData"
45 />
46 <n-select
47 v-model:value="statusFilter"
48 :options="statusOptions"
49 size="small"
50 class="w-32!"
51 @update:value="getData"
52 />
53 <n-checkbox v-model:checked="excludeOk" size="small" @update:checked="getData">Exclude OK</n-checkbox>
54 </div>
55 <n-pagination
56 v-model:page="currentPage"
57 v-model:page-size="pageSize"
58 :page-slot
59 :show-size-picker
60 :page-sizes
61 :item-count="total"
62 :simple="simpleMode"
63 />
64 </div>
65 <n-spin :show="loading">
66 <div class="my-3 flex min-h-52 flex-col gap-2">
67 <template v-if="healthcheckList.length">
68 <HealthcheckItem
69 v-for="alert of itemsPaginated"
70 :key="(alert.check_id || '') + alert.time"
71 :alert
72 class="item-appear item-appear-bottom item-appear-005"
73 />
74 </template>
75 <template v-else>
76 <n-empty v-if="!loading" description="No items found" class="h-48 justify-center" />
77 </template>
78 </div>
79 </n-spin>
80 <div class="footer flex justify-end">
81 <n-pagination
82 v-if="itemsPaginated.length > 3"
83 v-model:page="currentPage"
84 :page-size
85 :item-count="total"
86 :page-slot="6"
87 />
88 </div>
89 </div>
90 </template>
91
92 <script setup lang="ts">
93 // TODO-FE: refactor
94 import type { InfluxDBAlert, InfluxDBAlertResponse } from "@/types/healthchecks.d"
95 import { useResizeObserver } from "@vueuse/core"
96 import _orderBy from "lodash/orderBy"
97 import { NButton, NCheckbox, NEmpty, NPagination, NPopover, NSelect, NSpin, useMessage } from "naive-ui"
98 import { computed, onBeforeMount, ref } from "vue"
99 import Api from "@/api"
100 import Icon from "@/components/common/Icon.vue"
101 import { InfluxDBAlertSeverity } from "@/types/healthchecks.d"
102 import HealthcheckItem from "./HealthcheckItem.vue"
103
104 const message = useMessage()
105 const loading = ref(false)
106 const healthcheckList = ref<InfluxDBAlert[]>([])
107 const stats = ref<InfluxDBAlertResponse | null>(null)
108 const checkNames = ref<string[]>([])
109
110 const pageSize = ref(25)
111 const currentPage = ref(1)
112 const simpleMode = ref(false)
113 const showSizePicker = ref(true)
114 const pageSizes = [10, 25, 50, 100]
115 const header = ref()
116 const pageSlot = ref(8)
117
118 // Filters
119 const statusFilter = ref<"all" | "active" | "cleared">("all")
120 const excludeOk = ref(false)
121 const checkNameFilter = ref<string | null>(null)
122
123 const statusOptions = [
124 { label: "All", value: "all" },
125 { label: "Active", value: "active" },
126 { label: "Cleared", value: "cleared" }
127 ]
128
129 const checkNameOptions = computed(() => [
130 { label: "All Checks" },
131 ...checkNames.value.map(name => ({ label: name, value: name }))
132 ])
133
134 const itemsPaginated = computed(() => {
135 const from = (currentPage.value - 1) * pageSize.value
136 const to = currentPage.value * pageSize.value
137
138 const list = _orderBy(
139 healthcheckList.value,
140 [
141 // Sort by severity priority
142 item => {
143 const severityOrder = {
144 [InfluxDBAlertSeverity.Critical]: 0,
145 [InfluxDBAlertSeverity.Warning]: 1,
146 [InfluxDBAlertSeverity.Info]: 2,
147 [InfluxDBAlertSeverity.Ok]: 3
148 }
149 return severityOrder[item.severity as InfluxDBAlertSeverity]
150 },
151 "time"
152 ],
153 ["asc", "desc"]
154 )
155
156 return list.slice(from, to)
157 })
158
159 const InfoIcon = "carbon:information"
160
161 const total = computed<number>(() => {
162 return healthcheckList.value.length || 0
163 })
164
165 const criticalTotal = computed<number>(() => {
166 return healthcheckList.value.filter(o => o.severity === InfluxDBAlertSeverity.Critical).length || 0
167 })
168
169 function getCheckNames() {
170 Api.healthchecks
171 .getCheckNames()
172 .then(res => {
173 if (res.data.success) {
174 checkNames.value = res.data.check_names || []
175 }
176 })
177 .catch(() => {
178 checkNames.value = []
179 })
180 }
181
182 function getData() {
183 loading.value = true
184
185 Api.healthchecks
186 .getHealthchecks({
187 days: 7,
188 status: statusFilter.value,
189 exclude_ok: excludeOk.value,
190 check_name: checkNameFilter.value || undefined
191 })
192 .then(res => {
193 if (res.data.success) {
194 healthcheckList.value = res.data.alerts || []
195 stats.value = res.data
196 } else {
197 message.warning(res.data?.message || "An error occurred. Please try again later.")
198 }
199 })
200 .catch(err => {
201 healthcheckList.value = []
202 stats.value = null
203
204 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
205 })
206 .finally(() => {
207 loading.value = false
208 })
209 }
210
211 useResizeObserver(header, entries => {
212 const entry = entries[0]
213 if (!entry) return
214
215 const { width } = entry.contentRect
216
217 pageSlot.value = width < 650 ? 5 : 8
218 simpleMode.value = width < 450
219 })
220
221 onBeforeMount(() => {
222 getCheckNames()
223 getData()
224 })
225 </script>