Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.
This commit is contained in:
410
pages/notification/log-audit/logs.vue
Normal file
410
pages/notification/log-audit/logs.vue
Normal file
@@ -0,0 +1,410 @@
|
||||
<template>
|
||||
<div>
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<!-- Page Info Card -->
|
||||
<rs-card class="mb-6">
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<Icon class="mr-2 text-primary" name="ic:outline-list-alt"></Icon>
|
||||
<h1 class="text-xl font-bold text-primary">Audit Logs</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="text-gray-600">
|
||||
View notification logs and filter by date, channel, status, or content.
|
||||
</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
|
||||
<rs-card
|
||||
v-for="(item, index) in summaryStats"
|
||||
:key="index"
|
||||
>
|
||||
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
|
||||
<div
|
||||
class="p-5 flex justify-center items-center bg-primary/20 rounded-2xl"
|
||||
>
|
||||
<Icon class="text-primary text-3xl" :name="item.icon"></Icon>
|
||||
</div>
|
||||
<div class="flex-1 truncate">
|
||||
<span class="block font-bold text-2xl leading-tight text-primary">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-600">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Card -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<Icon class="mr-2 text-primary" name="ic:outline-history"></Icon>
|
||||
<h3 class="text-lg font-semibold text-primary">All Audit Logs</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<rs-button variant="primary-outline" size="sm" @click="refreshLogs">
|
||||
<Icon name="ic:outline-refresh" class="mr-1"/> Refresh
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<!-- Filters Section -->
|
||||
<div class="mb-6 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Actor"
|
||||
v-model="filters.actor"
|
||||
placeholder="Enter user or actor"
|
||||
outer-class="mb-0"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Channel"
|
||||
v-model="filters.channel"
|
||||
placeholder="Select channel"
|
||||
:options="availableChannels"
|
||||
outer-class="mb-0"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="select"
|
||||
label="Status"
|
||||
v-model="filters.status"
|
||||
placeholder="Select status"
|
||||
:options="availableStatuses"
|
||||
outer-class="mb-0"
|
||||
/>
|
||||
|
||||
<FormKit
|
||||
type="search"
|
||||
label="Search"
|
||||
v-model="filters.keyword"
|
||||
placeholder="Search in logs..."
|
||||
outer-class="mb-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<div class="text-sm text-gray-600" v-if="!loading">
|
||||
Showing {{ logs.length }} entries
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<rs-button @click="clearFilters" variant="secondary-outline" size="sm">
|
||||
<Icon name="ic:outline-clear" class="mr-1"/> Clear
|
||||
</rs-button>
|
||||
<rs-button @click="applyFilters(filters)" variant="primary">
|
||||
<Icon name="ic:outline-search" class="mr-1"/> Apply
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="flex justify-center py-8">
|
||||
<Loading />
|
||||
</div>
|
||||
|
||||
<!-- Log Table -->
|
||||
<rs-table
|
||||
v-else-if="logs && logs.length > 0"
|
||||
:data="logs"
|
||||
:field="logTableFields"
|
||||
:options="{
|
||||
variant: 'default',
|
||||
striped: true,
|
||||
borderless: false,
|
||||
hover: true
|
||||
}"
|
||||
:options-advanced="{
|
||||
sortable: true,
|
||||
responsive: true,
|
||||
outsideBorder: true
|
||||
}"
|
||||
advanced
|
||||
>
|
||||
<template #timestamp="{ value }">
|
||||
<div class="text-sm">
|
||||
<div class="font-medium">{{ formatDate(value.created_at, true) }}</div>
|
||||
<div class="text-gray-500 text-xs">{{ new Date(value.created_at).toLocaleTimeString() }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #status="{ value }">
|
||||
<rs-badge
|
||||
:variant="value.status === 'Sent' ? 'success' :
|
||||
value.status === 'Failed' ? 'danger' :
|
||||
value.status === 'Opened' ? 'info' :
|
||||
value.status === 'Queued' ? 'warning' : 'secondary'"
|
||||
size="sm"
|
||||
>
|
||||
{{ value.status }}
|
||||
</rs-badge>
|
||||
</template>
|
||||
|
||||
<template #actions="{ value }">
|
||||
<rs-button variant="primary-text" size="sm" @click="viewLogDetails(value)">
|
||||
<Icon name="ic:outline-visibility" class="mr-1"/> View
|
||||
</rs-button>
|
||||
</template>
|
||||
</rs-table>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else class="text-center py-8 text-gray-500">
|
||||
<Icon name="ic:outline-search-off" class="text-4xl mb-2 mx-auto"/>
|
||||
<p>No log entries found matching your filters.</p>
|
||||
<rs-button variant="primary-outline" @click="clearFilters" class="mt-4">
|
||||
Clear Filters
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="logs && logs.length > 0" class="flex justify-center mt-6">
|
||||
<div class="flex items-center gap-1">
|
||||
<rs-button
|
||||
variant="primary-text"
|
||||
size="sm"
|
||||
:disabled="pagination.page === 1"
|
||||
@click="changePage(pagination.page - 1)"
|
||||
>
|
||||
<Icon name="ic:outline-chevron-left"/>
|
||||
</rs-button>
|
||||
|
||||
<div class="flex gap-1">
|
||||
<rs-button
|
||||
v-for="p in paginationButtons"
|
||||
:key="p"
|
||||
variant="primary-text"
|
||||
size="sm"
|
||||
:class="pagination.page === p ? 'bg-primary/10' : ''"
|
||||
@click="changePage(p)"
|
||||
>
|
||||
{{ p }}
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<rs-button
|
||||
variant="primary-text"
|
||||
size="sm"
|
||||
:disabled="pagination.page === pagination.pages"
|
||||
@click="changePage(pagination.page + 1)"
|
||||
>
|
||||
<Icon name="ic:outline-chevron-right"/>
|
||||
</rs-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Log Details Modal -->
|
||||
<rs-modal
|
||||
v-model="isLogDetailModalOpen"
|
||||
title="Log Entry Details"
|
||||
size="lg"
|
||||
:overlay-close="true"
|
||||
>
|
||||
<template #body>
|
||||
<div v-if="selectedLog" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Log ID</label>
|
||||
<p class="font-mono text-sm">{{ selectedLog.id }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Timestamp</label>
|
||||
<p class="text-sm">{{ formatDate(selectedLog.created_at, true) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Action</label>
|
||||
<p class="text-sm">{{ selectedLog.action }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Actor</label>
|
||||
<p class="text-sm">{{ selectedLog.actor }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Channel</label>
|
||||
<p class="text-sm">{{ selectedLog.channel_type }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Status</label>
|
||||
<rs-badge
|
||||
:variant="selectedLog.status === 'Sent' ? 'success' :
|
||||
selectedLog.status === 'Failed' ? 'danger' :
|
||||
selectedLog.status === 'Opened' ? 'info' :
|
||||
selectedLog.status === 'Queued' ? 'warning' : 'secondary'"
|
||||
size="sm"
|
||||
>
|
||||
{{ selectedLog.status }}
|
||||
</rs-badge>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Source IP</label>
|
||||
<p class="text-sm font-mono">{{ selectedLog.source_ip }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Details</label>
|
||||
<p class="text-sm p-3 bg-gray-50 dark:bg-gray-800 rounded">{{ selectedLog.details }}</p>
|
||||
</div>
|
||||
|
||||
<template v-if="selectedLog.status === 'Failed'">
|
||||
<div class="border-t pt-4">
|
||||
<h4 class="text-lg font-medium text-red-600 dark:text-red-400 mb-3">Error Information</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Error Code</label>
|
||||
<p class="text-sm font-mono text-red-600 dark:text-red-400">{{ selectedLog.error_code }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-500 mb-1">Error Message</label>
|
||||
<p class="text-sm text-red-600 dark:text-red-400">{{ selectedLog.error_message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<rs-button @click="isLogDetailModalOpen = false" variant="primary-outline">
|
||||
Close
|
||||
</rs-button>
|
||||
</template>
|
||||
</rs-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Audit Logs",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/dashboard",
|
||||
},
|
||||
{
|
||||
name: "Notification",
|
||||
path: "/notification",
|
||||
},
|
||||
{
|
||||
name: "Logs & Audit Trail",
|
||||
path: "/notification/log-audit",
|
||||
},
|
||||
{
|
||||
name: "Audit Logs",
|
||||
path: "/notification/log-audit/logs",
|
||||
type: "current"
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
// Use the notification logs composable
|
||||
const {
|
||||
logs,
|
||||
loading,
|
||||
error,
|
||||
pagination,
|
||||
summaryStats,
|
||||
filters,
|
||||
fetchLogs,
|
||||
applyFilters,
|
||||
clearFilters,
|
||||
changePage,
|
||||
formatDate,
|
||||
availableActions,
|
||||
availableChannels,
|
||||
availableStatuses
|
||||
} = useNotificationLogs()
|
||||
|
||||
// Computed property for pagination buttons
|
||||
const paginationButtons = computed(() => {
|
||||
const current = pagination.value.page
|
||||
const total = pagination.value.pages
|
||||
const buttons = []
|
||||
|
||||
if (total <= 7) {
|
||||
// Less than 7 pages, show all
|
||||
for (let i = 1; i <= total; i++) {
|
||||
buttons.push(i)
|
||||
}
|
||||
} else {
|
||||
// Always show first page
|
||||
buttons.push(1)
|
||||
|
||||
if (current > 3) {
|
||||
buttons.push('...')
|
||||
}
|
||||
|
||||
// Pages around current
|
||||
const start = Math.max(2, current - 1)
|
||||
const end = Math.min(current + 1, total - 1)
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
buttons.push(i)
|
||||
}
|
||||
|
||||
if (current < total - 2) {
|
||||
buttons.push('...')
|
||||
}
|
||||
|
||||
// Always show last page
|
||||
buttons.push(total)
|
||||
}
|
||||
|
||||
return buttons
|
||||
})
|
||||
|
||||
// Fields for RsTable
|
||||
const logTableFields = ['timestamp', 'action', 'actor', 'channel_type', 'status', 'actions']
|
||||
|
||||
// Log detail modal
|
||||
const isLogDetailModalOpen = ref(false)
|
||||
const selectedLog = ref(null)
|
||||
|
||||
const viewLogDetails = (log) => {
|
||||
selectedLog.value = log
|
||||
isLogDetailModalOpen.value = true
|
||||
}
|
||||
|
||||
const refreshLogs = async () => {
|
||||
await fetchLogs()
|
||||
}
|
||||
|
||||
// Fetch logs on component mount
|
||||
onMounted(async () => {
|
||||
await fetchLogs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.formkit-outer) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.formkit-label) {
|
||||
font-weight: 500;
|
||||
color: rgb(107 114 128);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
:deep(.formkit-input) {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user