main
vue 282 lines 7.38 KB
Raw
1 <template>
2 <div class="logs-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>{{ total }}</code>
19 </div>
20 <div class="box">
21 Event Info :
22 <code>{{ eventInfoTotal }}</code>
23 </div>
24 <div class="box text-error">
25 Event Error :
26 <code>{{ eventErrorTotal }}</code>
27 </div>
28 </div>
29 </n-popover>
30
31 <n-button size="small" type="error" ghost :loading="loadingPurge" @click="showPurgeConfirm = true">
32 <div class="flex items-center gap-2">
33 <Icon :name="TrashIcon" :size="16" />
34 <span class="xs:block hidden">Purge</span>
35 </div>
36 </n-button>
37 </div>
38 <n-pagination
39 v-model:page="currentPage"
40 v-model:page-size="pageSize"
41 :page-slot
42 :show-size-picker
43 :page-sizes
44 :item-count="total"
45 :simple="simpleMode"
46 />
47 <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="px-0!">
48 <template #trigger>
49 <div class="bg-default rounded-lg">
50 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
51 <n-button size="small" @click="showFilters = true">
52 <template #icon>
53 <Icon :name="FilterIcon" />
54 </template>
55 </n-button>
56 </n-badge>
57 </div>
58 </template>
59 <LogsFilters
60 v-model:type="filterType"
61 v-model:value="filterValue"
62 v-model:filtered="filtered"
63 :users="usersList"
64 :loading-users
65 @submit="getData()"
66 @close="showFilters = false"
67 />
68 </n-popover>
69 </div>
70 <n-spin :show="loading">
71 <div class="my-3 flex min-h-52 flex-col gap-2">
72 <template v-if="logsList.length">
73 <LogItem
74 v-for="log of itemsPaginated"
75 :key="log.id"
76 :log
77 :users="usersList"
78 class="item-appear item-appear-bottom item-appear-005"
79 />
80 </template>
81 <template v-else>
82 <n-empty v-if="!loading" description="No Logs found" class="h-48 justify-center" />
83 </template>
84 </div>
85 </n-spin>
86 <div class="footer flex justify-end">
87 <n-pagination
88 v-if="itemsPaginated.length > 3"
89 v-model:page="currentPage"
90 :page-size
91 :item-count="total"
92 :page-slot="6"
93 />
94 </div>
95
96 <n-modal v-model:show="showPurgeConfirm" preset="dialog" type="warning" title="Purge Logs">
97 <div class="mt-5 flex flex-col gap-2">
98 <div>Are you sure you want to purge Logs ?</div>
99 <n-select
100 v-model:value="purgeSelected"
101 :options="purgeOptions"
102 :disabled="loadingPurge"
103 placeholder="Select time range"
104 />
105 </div>
106 <template #action>
107 <div class="flex gap-3">
108 <n-button size="small" ghost @click="showPurgeConfirm = false">Cancel</n-button>
109 <n-button size="small" type="warning" :loading="loadingPurge" @click="purge()">
110 Yes I'm sure
111 </n-button>
112 </div>
113 </template>
114 </n-modal>
115 </div>
116 </template>
117
118 <script setup lang="ts">
119 // TODO-FE: refactor
120 import type { Log, LogsQuery, LogsQueryTimeRange, LogsQueryTypes, LogsQueryValues } from "@/types/logs.d"
121 import type { User } from "@/types/user.d"
122 import { useResizeObserver } from "@vueuse/core"
123 import _orderBy from "lodash/orderBy"
124 import { NBadge, NButton, NEmpty, NModal, NPagination, NPopover, NSelect, NSpin, useMessage } from "naive-ui"
125 import { nanoid } from "nanoid"
126 import { computed, onBeforeMount, ref, toRefs } from "vue"
127 import Api from "@/api"
128 import Icon from "@/components/common/Icon.vue"
129 import { LogEventType } from "@/types/logs.d"
130 import LogItem from "./LogItem.vue"
131 import LogsFilters from "./LogsFilters.vue"
132
133 interface LogExt extends Log {
134 id?: string
135 }
136
137 const props = defineProps<{ userId?: string }>()
138 const { userId } = toRefs(props)
139
140 const message = useMessage()
141 const loadingUsers = ref(false)
142 const loading = ref(false)
143 const loadingPurge = ref(false)
144 const showPurgeConfirm = ref(false)
145 const usersList = ref<User[]>([])
146 const logsList = ref<LogExt[]>([])
147 const showFilters = ref(false)
148
149 const pageSize = ref(25)
150 const currentPage = ref(1)
151 const simpleMode = ref(false)
152 const showSizePicker = ref(true)
153 const pageSizes = [10, 25, 50, 100]
154 const header = ref()
155 const pageSlot = ref(8)
156
157 const itemsPaginated = computed(() => {
158 const from = (currentPage.value - 1) * pageSize.value
159 const to = currentPage.value * pageSize.value
160
161 const list = _orderBy(logsList.value, ["timestamp"], ["desc"])
162
163 return list.slice(from, to)
164 })
165
166 const FilterIcon = "carbon:filter-edit"
167 const TrashIcon = "carbon:trash-can"
168 const InfoIcon = "carbon:information"
169
170 const total = computed<number>(() => {
171 return logsList.value.length || 0
172 })
173
174 const eventInfoTotal = computed<number>(() => {
175 return logsList.value.filter(o => o.event_type === LogEventType.INFO).length || 0
176 })
177 const eventErrorTotal = computed<number>(() => {
178 return logsList.value.filter(o => o.event_type === LogEventType.ERROR).length || 0
179 })
180
181 const filterType = ref<LogsQueryTypes | null>(null)
182 const filterValue = ref<LogsQueryValues | null>(null)
183
184 const filtered = ref(false)
185
186 const purgeSelected = ref<LogsQueryTimeRange | "">("")
187 const purgeOptions: { label: string; value: LogsQueryTimeRange | string }[] = [
188 { label: "All Logs", value: "" },
189 { label: "1 Hour", value: "1h" },
190 { label: "6 Hours", value: "6h" },
191 { label: "12 Hours", value: "12h" },
192 { label: "1 Day", value: "1d" },
193 { label: "3 Days", value: "3d" },
194 { label: "1 Week", value: "1w" }
195 ]
196
197 function purge() {
198 loadingPurge.value = true
199
200 Api.logs
201 .purge(purgeSelected.value || undefined)
202 .then(res => {
203 if (res.data.success) {
204 getData()
205 message.success(res.data?.message || "Logs purged successfully")
206 } else {
207 message.warning(res.data?.message || "An error occurred. Please try again later.")
208 }
209 })
210 .catch(err => {
211 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
212 })
213 .finally(() => {
214 loadingPurge.value = false
215 })
216 }
217
218 function getData() {
219 showFilters.value = false
220 loading.value = true
221
222 const query =
223 filterType.value && filterValue.value ? ({ [filterType.value]: filterValue.value } as LogsQuery) : undefined
224
225 Api.logs
226 .getLogs(query)
227 .then(res => {
228 if (res.data.success) {
229 logsList.value = (res.data.logs || []).map((o: LogExt) => {
230 o.id = nanoid()
231 return o
232 })
233 } else {
234 message.warning(res.data?.message || "An error occurred. Please try again later.")
235 }
236 })
237 .catch(err => {
238 logsList.value = []
239
240 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
241 })
242 .finally(() => {
243 loading.value = false
244 })
245 }
246
247 function getUsers() {
248 loadingUsers.value = true
249
250 Api.users
251 .getUsers()
252 .then(res => {
253 if (res.data.success) {
254 usersList.value = res.data?.users || []
255 }
256 })
257 .finally(() => {
258 loadingUsers.value = false
259 })
260 }
261
262 useResizeObserver(header, entries => {
263 const entry = entries[0]
264 if (!entry) return
265
266 const { width } = entry.contentRect
267
268 pageSlot.value = width < 700 ? 5 : 8
269 simpleMode.value = width < 550
270 })
271
272 onBeforeMount(() => {
273 if (userId.value) {
274 filterType.value = "userId"
275 filterValue.value = userId.value
276 filtered.value = true
277 }
278
279 getUsers()
280 getData()
281 })
282 </script>