41
41
</Listbox >
42
42
43
43
<PopoverGroup class =" flex items-baseline space-x-8" >
44
- <Popover as =" div" class =" relative inline-block text-left" >
44
+ <Popover v-slot = " { close } " as =" div" class =" relative inline-block text-left" >
45
45
<PopoverButton class =" inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-xs text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-primary" >
46
46
<span >{{ t('auditLog.filter') }}</span >
47
47
<ChevronDownIcon class =" -mr-1 ml-2 h-5 w-5" aria-hidden =" true" />
48
48
</PopoverButton >
49
49
50
50
<transition enter-active-class =" transition ease-out duration-100" enter-from-class =" transform opacity-0 scale-95" enter-to-class =" transform opacity-100 scale-100" leave-active-class =" transition ease-in duration-75" leave-from-class =" transform opacity-100 scale-100" leave-to-class =" transform opacity-0 scale-95" >
51
- <PopoverPanel class =" absolute right-0 z-10 mt-2 origin-top-right rounded-md bg-white p-4 shadow-2xl ring-1 ring-black/5 focus:outline-hidden w-80 " >
51
+ <PopoverPanel class =" absolute right-0 z-10 mt-2 origin-top-right rounded-md bg-white p-4 shadow-2xl ring-1 ring-black/5 focus:outline-hidden w-96 " >
52
52
<form class =" space-y-4" >
53
53
<div class =" sm:grid sm:grid-cols-2 sm:items-center sm:gap-2" >
54
54
<label for =" filter-start-date" class =" block text-sm font-medium text-gray-700" >
62
62
</label >
63
63
<input id =" filter-end-date" v-model =" endDateFilter" type =" text" class =" shadow-xs focus:ring-primary focus:border-primary block w-full sm:text-sm border-gray-300 rounded-md" :class =" { 'border-red-300 text-red-900 focus:ring-red-500 focus:border-red-500': !endDateFilterIsValid }" placeholder =" yyyy-MM-dd" />
64
64
</div >
65
+ <div class =" sm:grid sm:grid-cols-2 sm:items-center sm:gap-2" >
66
+ <label class =" block text-sm font-medium text-gray-700 flex items-center" >
67
+ {{ t('auditLog.type') }}
68
+ <button
69
+ type =" button"
70
+ class =" ml-2 p-1 flex items-center justify-center focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-30 disabled:cursor-not-allowed"
71
+ :disabled =" selectedEventTypes.length === 0"
72
+ :title =" selectedEventTypes.length > 0 ? t('auditLog.filter.clerEventFilter') : ''"
73
+ @click =" selectedEventTypes = []"
74
+ >
75
+ <TrashIcon class =" h-4 w-4 text-gray-500 hover:text-gray-700 disabled:text-gray-300" aria-hidden =" true" />
76
+ </button >
77
+ </label >
78
+ </div >
79
+ <Listbox v-model =" selectedEventTypes" as =" div" multiple >
80
+ <div class =" relative w-88" >
81
+ <ListboxButton class =" relative w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left shadow-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary text-sm" >
82
+ <div class =" flex flex-wrap gap-2" >
83
+ <template v-if =" selectedEventTypes .length > 0 " >
84
+ <button
85
+ v-for =" type in selectedEventTypes"
86
+ :key =" type"
87
+ class =" inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20"
88
+ :aria-label =" t('auditLog.filter.removeEventType', { type: eventTypeOptions[type] })"
89
+ @click.stop =" removeEventType(type)"
90
+ >
91
+ <span class =" mr-1" >{{ eventTypeOptions[type] }}</span >
92
+ <span class =" text-green-800 font-bold" aria-hidden =" true" >× ; </span >
93
+ </button >
94
+ </template >
95
+ <template v-else >
96
+ <span class =" text-gray-500" >{{ t('auditLog.filter.selectEventFilter') }}</span >
97
+ </template >
98
+ </div >
99
+ <span class =" pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2" >
100
+ <ChevronUpDownIcon class =" h-5 w-5 text-gray-400" aria-hidden =" true" />
101
+ </span >
102
+ </ListboxButton >
103
+ <transition leave-active-class =" transition ease-in duration-100" leave-from-class =" opacity-100" leave-to-class =" opacity-0" >
104
+ <ListboxOptions class =" absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 focus:outline-none text-sm" >
105
+ <ListboxOption
106
+ v-for =" (label, key) in eventTypeOptions"
107
+ :key =" key"
108
+ v-slot =" { selected }"
109
+ class =" relative cursor-default select-none py-2 pl-3 pr-9 ui-not-active:text-gray-900 ui-active:text-white ui-active:bg-primary"
110
+ :value =" key"
111
+ >
112
+ <span :class =" [selected ? 'font-semibold' : 'font-normal', 'block truncate']" >{{ label }}</span >
113
+ <span v-if =" selected" :class =" ['absolute inset-y-0 right-0 flex items-center pr-4', selected ? 'text-primary' : 'text-gray-400']" >
114
+ <CheckIcon class =" h-5 w-5" aria-hidden =" true" />
115
+ </span >
116
+ </ListboxOption >
117
+ </ListboxOptions >
118
+ </transition >
119
+ </div >
120
+ </Listbox >
65
121
<div class =" flex flex-col sm:flex-row gap-2 pt-4 border-t border-gray-200" >
66
122
<button type =" button" class =" w-full border border-gray-300 rounded-md bg-white px-3 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:opacity-50 disabled:hover:bg-white disabled:cursor-not-allowed" :disabled =" filterIsReset" @click =" resetFilter()" >
67
123
{{ t('common.reset') }}
68
124
</button >
69
- <button type =" button" class =" w-full rounded-md bg-primary px-3 py-2 text-sm font-medium text-white shadow-sm hover:bg-primary-d1 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:opacity-50 disabled:hover:bg-primary disabled:cursor-not-allowed" :disabled =" !filterIsValid" @click =" applyFilter()" >
125
+ <button type =" button" class =" w-full rounded-md bg-primary px-3 py-2 text-sm font-medium text-white shadow-sm hover:bg-primary-d1 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:opacity-50 disabled:hover:bg-primary disabled:cursor-not-allowed" :disabled =" !filterIsValid" @click =" async () => { close(); await applyFilter(); } " >
70
126
{{ t('common.apply') }}
71
127
</button >
72
128
</div >
166
222
<script setup lang="ts">
167
223
import { Listbox , ListboxButton , ListboxOption , ListboxOptions , Popover , PopoverButton , PopoverGroup , PopoverPanel } from ' @headlessui/vue' ;
168
224
import { ChevronDownIcon } from ' @heroicons/vue/20/solid' ;
169
- import { CheckIcon , ChevronUpDownIcon , WrenchIcon } from ' @heroicons/vue/24/solid' ;
225
+ import { CheckIcon , ChevronUpDownIcon , TrashIcon , WrenchIcon } from ' @heroicons/vue/24/solid' ;
170
226
import { computed , onMounted , ref , watch } from ' vue' ;
171
227
import { useI18n } from ' vue-i18n' ;
172
228
import auditlog , { AuditEventDto } from ' ../common/auditlog' ;
@@ -205,7 +261,11 @@ const startDateFilter = ref(startDate.value.toISOString().split('T')[0]);
205
261
const endDate = ref (endOfDate (new Date ()));
206
262
const endDateFilter = ref (endDate .value .toISOString ().split (' T' )[0 ]);
207
263
208
- const filterIsReset = computed (() => startDateFilter .value == startDate .value .toISOString ().split (' T' )[0 ] && endDateFilter .value == endDate .value .toISOString ().split (' T' )[0 ]);
264
+ const filterIsReset = computed (() =>
265
+ startDateFilter .value == startDate .value .toISOString ().split (' T' )[0 ] &&
266
+ endDateFilter .value == endDate .value .toISOString ().split (' T' )[0 ] &&
267
+ selectedEventTypes .value .length == 0
268
+ );
209
269
const startDateFilterIsValid = computed (() => validateDateFilterValue (startDateFilter .value ) != null );
210
270
const endDateFilterIsValid = computed (() => {
211
271
const endDate = validateDateFilterValue (endDateFilter .value );
@@ -235,6 +295,28 @@ const orderOptions = {
235
295
};
236
296
watch (selectedOrder , refreshData );
237
297
298
+ const eventTypeOptions = Object .fromEntries (
299
+ Object .entries ({
300
+ DEVICE_REGISTER: t (' auditLog.details.device.register' ),
301
+ DEVICE_REMOVE: t (' auditLog.details.device.remove' ),
302
+ SETTING_WOT_UPDATE: t (' auditLog.details.setting.wot.update' ),
303
+ SIGN_WOT_ID: t (' auditLog.details.wot.signedIdentity' ),
304
+ USER_ACCOUNT_RESET: t (' auditLog.details.user.account.reset' ),
305
+ USER_KEYS_CHANGE: t (' auditLog.details.user.keys.change' ),
306
+ USER_SETUP_CODE_CHANGE: t (' auditLog.details.user.setupCode.change' ),
307
+ VAULT_ACCESS_GRANT: t (' auditLog.details.vaultAccess.grant' ),
308
+ VAULT_CREATE: t (' auditLog.details.vault.create' ),
309
+ VAULT_KEY_RETRIEVE: t (' auditLog.details.vaultKey.retrieve' ),
310
+ VAULT_MEMBER_ADD: t (' auditLog.details.vaultMember.add' ),
311
+ VAULT_MEMBER_REMOVE: t (' auditLog.details.vaultMember.remove' ),
312
+ VAULT_MEMBER_UPDATE: t (' auditLog.details.vaultMember.update' ),
313
+ VAULT_OWNERSHIP_CLAIM: t (' auditLog.details.vaultOwnership.claim' ),
314
+ VAULT_UPDATE: t (' auditLog.details.vault.update' )
315
+ }).sort (([,valueA ], [,valueB ]) => valueA .localeCompare (valueB ))
316
+ );
317
+ const allEventTypes = Object .keys (eventTypeOptions );
318
+ const selectedEventTypes = ref <string []>([]);
319
+
238
320
const currentPage = ref (0 );
239
321
const pageSize = ref (20 );
240
322
const paginationBegin = computed (() => auditEvents .value ? currentPage .value * pageSize .value + Math .min (1 , auditEvents .value .length ) : 0 );
@@ -244,11 +326,15 @@ let lastIdOfPreviousPage = [Number.MAX_SAFE_INTEGER];
244
326
245
327
onMounted (fetchData );
246
328
329
+ watch (selectedEventTypes , (newSelection , oldSelection ) => {
330
+ selectedEventTypes .value .sort ((a , b ) => eventTypeOptions [a ].localeCompare (eventTypeOptions [b ]));
331
+ });
332
+
247
333
async function fetchData(page : number = 0 ) {
248
334
onFetchError .value = null ;
249
335
try {
250
336
// Fetch one more event than the page size to determine if there is a next page
251
- const events = await auditlog .service .getAllEvents (startDate .value , endDate .value , lastIdOfPreviousPage [page ], selectedOrder .value , pageSize .value + 1 );
337
+ const events = await auditlog .service .getAllEvents (startDate .value , endDate .value , selectedEventTypes . value , lastIdOfPreviousPage [page ], selectedOrder .value , pageSize .value + 1 );
252
338
// If the lastIdOfPreviousPage for the first page has not been set yet, set it to an id "before"/"after" the first event
253
339
if (page == 0 && lastIdOfPreviousPage [0 ] == 0 && events .length > 0 ) {
254
340
lastIdOfPreviousPage [0 ] = events [0 ].id + orderOptions [selectedOrder .value ].sign ;
@@ -291,9 +377,14 @@ async function applyFilter() {
291
377
}
292
378
}
293
379
380
+ function removeEventType(type : string ): void {
381
+ selectedEventTypes .value = selectedEventTypes .value .filter (t => t !== type );
382
+ }
383
+
294
384
function resetFilter() {
295
385
startDateFilter .value = startDate .value .toISOString ().split (' T' )[0 ];
296
386
endDateFilter .value = endDate .value .toISOString ().split (' T' )[0 ];
387
+ selectedEventTypes .value = [];
297
388
}
298
389
299
390
function beginOfDate(date : Date ): Date {
0 commit comments