Skip to content

Commit

Permalink
add audit log event filter
Browse files Browse the repository at this point in the history
  • Loading branch information
mindmonk committed Feb 5, 2025
1 parent c18af8c commit f89f1db
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 11 deletions.
113 changes: 102 additions & 11 deletions frontend/src/components/AuditLog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@
</h2>

<div class="flex gap-3">
<button class="w-full bg-primary py-2 px-4 border border-transparent rounded-md shadow-xs text-sm font-medium text-white hover:bg-primary-d1 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-primary" @click="refreshData()">
<button class="w-full bg-primary py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-primary-d1 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary" @click="refreshData()">
{{ t('common.refresh') }}
</button>

<Listbox v-model="selectedOrder" as="div">
<div class="relative w-36">
<ListboxButton class="relative w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left shadow-xs focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary text-sm">
<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">
<span class="block truncate">{{ orderOptions[selectedOrder].label }}</span>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</ListboxButton>
<transition leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100" leave-to-class="opacity-0">
<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-hidden text-sm">
<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 ring-opacity-5 focus:outline-none text-sm">
<ListboxOption v-for="(option, key) in orderOptions" :key="key" v-slot="{ active, selected }" 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" :value="key">
<span :class="[selected ? 'font-semibold' : 'font-normal', 'block truncate']">{{ option.label }}</span>
<span v-if="selected" :class="[active ? 'text-white' : 'text-primary', 'absolute inset-y-0 right-0 flex items-center pr-4']">
Expand All @@ -42,31 +42,77 @@

<PopoverGroup class="flex items-baseline space-x-8">
<Popover as="div" class="relative inline-block text-left">
<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">
<PopoverButton class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary">
<span>{{ t('auditLog.filter') }}</span>
<ChevronDownIcon class="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
</PopoverButton>

<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">
<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">
<PopoverPanel class="absolute right-0 z-10 mt-2 origin-top-right rounded-md bg-white p-4 shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none w-96">
<form class="space-y-4">
<div class="sm:grid sm:grid-cols-2 sm:items-center sm:gap-2">
<label for="filter-start-date" class="block text-sm font-medium text-gray-700">
{{ t('auditLog.filter.startDate') }}
</label>
<input id="filter-start-date" v-model="startDateFilter" 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': !startDateFilterIsValid }" placeholder="yyyy-MM-dd" />
<input id="filter-start-date" v-model="startDateFilter" type="text" class="shadow-sm 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': !startDateFilterIsValid }" placeholder="yyyy-MM-dd" />
</div>
<div class="sm:grid sm:grid-cols-2 sm:items-center sm:gap-2">
<label for="filter-end-date" class="block text-sm font-medium text-gray-700">
{{ t('auditLog.filter.endDate') }}
</label>
<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" />
<input id="filter-end-date" v-model="endDateFilter" type="text" class="shadow-sm 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" />
</div>
<div class="sm:grid sm:grid-cols-2 sm:items-center sm:gap-2">
<label class="block text-sm font-medium text-gray-700">
{{ t('auditLog.type') }}
</label>
</div>
<Listbox v-model="selectedEventTypes" as="div" multiple>
<div class="relative w-88">
<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">
<div class="flex flex-wrap gap-2">
<template v-if="selectedEventTypes.length === 0">
<span class="text-gray-500">{{ t('auditLog.all') }}</span>
</template>
<template v-else>
<button
v-for="type in selectedEventTypes"
:key="type"
@click.stop="type !== 'ALL' && removeEventType(type)"
:disabled="type === 'ALL'"
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">
<span class="mr-1">{{ eventTypeOptions[type] }}</span>
<span v-if="type !== 'ALL'" class="text-green-800 font-bold">&times;</span>
</button>
</template>
</div>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</ListboxButton>

<transition leave-active-class="transition ease-in duration-100" leave-from-class="opacity-100" leave-to-class="opacity-0">
<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 ring-opacity-5 focus:outline-none text-sm">
<ListboxOption
v-for="(label, key) in eventTypeOptions"
:key="key"
v-slot="{ active, selected }"
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"
:value="key">
<span :class="[selected ? 'font-semibold' : 'font-normal', 'block truncate']">{{ label }}</span>
<span v-if="selected" :class="[selected ? 'text-primary' : active ? 'text-white' : 'text-primary', 'absolute inset-y-0 right-0 flex items-center pr-4']">
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
<div class="flex flex-col sm:flex-row gap-2 pt-4 border-t border-gray-200">
<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()">
<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 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()">
{{ t('common.reset') }}
</button>
<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()">
<button type="button" class="w-full rounded-md bg-primary px-3 py-2 text-sm font-medium text-white shadow 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()">
{{ t('common.apply') }}
</button>
</div>
Expand All @@ -81,7 +127,7 @@
<div class="mt-5 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<div class="overflow-hidden shadow-sm ring-1 ring-black/5 sm:rounded-lg">
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-300" aria-describedby="title">
<thead class="bg-gray-50">
<tr>
Expand Down Expand Up @@ -152,7 +198,7 @@
<h3 class="mt-2 text-sm font-medium text-gray-900">{{ t('auditLog.paymentRequired.message') }}</h3>
<p class="mt-1 text-sm text-gray-500">{{ t('auditLog.paymentRequired.description') }}</p>
<router-link v-slot="{ navigate }" to="/app/admin/settings" custom>
<button type="button" class="inline-flex items-center px-4 py-2 border border-transparent shadow-xs text-sm font-medium rounded-md text-white bg-primary hover:bg-primary-d1 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-primary mt-6" @click="navigate()">
<button type="button" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary hover:bg-primary-d1 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary mt-6" @click="navigate()">
<WrenchIcon class="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
{{ t('auditLog.paymentRequired.openAdminSection') }}
</button>
Expand Down Expand Up @@ -276,12 +322,57 @@ async function refreshData() {
auditlog.entityCache.invalidateAll();
await fetchData();
}
const selectedEventTypes = ref<string[]>(['ALL']);
const eventTypeOptions = Object.fromEntries(
Object.entries({
ALL: t('auditLog.details.all'),
DEVICE_REGISTER: t('auditLog.details.device.register'),
DEVICE_REMOVE: t('auditLog.details.device.remove'),
SETTING_WOT_UPDATE: t('auditLog.details.setting.wot.update'),
SIGN_WOT_ID: t('auditLog.details.wot.signedIdentity'),
VAULT_ACCESS_GRANT: t('auditLog.details.vaultAccess.grant'),
VAULT_CREATE: t('auditLog.details.vault.create'),
VAULT_KEY_RETRIEVE: t('auditLog.details.vaultKey.retrieve'),
VAULT_MEMBER_ADD: t('auditLog.details.vaultMember.add'),
VAULT_MEMBER_REMOVE: t('auditLog.details.vaultMember.remove'),
VAULT_MEMBER_UPDATE: t('auditLog.details.vaultMember.update'),
VAULT_OWNERSHIP_CLAIM: t('auditLog.details.vaultOwnership.claim'),
VAULT_UPDATE: t('auditLog.details.vault.update')
}).sort((a, b) => {
if (a[0] === "ALL") return -1;
if (b[0] === "ALL") return 1;
return a[1].localeCompare(b[1]);
})
);
watch(selectedEventTypes, (newSelection, oldSelection) => {
if (!oldSelection.includes('ALL') && newSelection.includes('ALL')) {
selectedEventTypes.value = ['ALL'];
} else if (newSelection.includes('ALL') && newSelection.length > 1) {
selectedEventTypes.value = newSelection.filter(type => type !== 'ALL');
} else if (selectedEventTypes.value.length === 0) {
selectedEventTypes.value = ['ALL'];
}
selectedEventTypes.value.sort((a, b) => eventTypeOptions[a].localeCompare(eventTypeOptions[b]));
});
async function applyFilter() {
if (filterIsValid.value) {
startDate.value = beginOfDate(new Date(startDateFilter.value));
endDate.value = endOfDate(new Date(endDateFilter.value));
await fetchData();
if (!selectedEventTypes.value.includes('ALL')) {
auditEvents.value = auditEvents.value.filter(event => selectedEventTypes.value.includes(event.type));
}
}
}
function removeEventType(type: string): void {
if (type !== 'ALL') {
selectedEventTypes.value = selectedEventTypes.value.filter(t => t !== type);
}
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"auditLog.timestamp": "Zeitstempel",
"auditLog.type": "Ereignis",
"auditLog.details": "Details",
"auditLog.details.all": "Alle",
"auditLog.details.device.register": "Gerät registrieren",
"auditLog.details.device.remove": "Gerät entfernen",
"auditLog.details.setting.wot.update": "WoT-Einstellung aktualisieren",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"auditLog.timestamp": "Timestamp",
"auditLog.type": "Event",
"auditLog.details": "Details",
"auditLog.details.all": "All",
"auditLog.details.device.register": "Register Device",
"auditLog.details.device.remove": "Remove Device",
"auditLog.details.setting.wot.update": "Update Wot Settings",
Expand Down

0 comments on commit f89f1db

Please sign in to comment.