Update AuditLogView.vue
This commit is contained in:
@@ -1,63 +1,78 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="audit-log-view">
|
<div class="p-4 bg-background text-foreground"> <!-- Outer container with padding -->
|
||||||
<h1>{{ $t('auditLog.title') }}</h1>
|
<div class="max-w-7xl mx-auto"> <!-- Inner container for max-width (slightly wider for table) and centering -->
|
||||||
|
<h1 class="text-xl font-semibold text-foreground mb-4 pb-2 border-b border-border"> <!-- Title styling -->
|
||||||
|
{{ $t('auditLog.title') }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
<!-- TODO: Add filtering options (Action Type, Date Range) -->
|
<!-- TODO: Add filtering options (Action Type, Date Range) -->
|
||||||
|
|
||||||
<div v-if="store.isLoading" class="loading-indicator">
|
<div v-if="store.isLoading" class="p-4 text-center text-text-secondary italic"> <!-- Loading state -->
|
||||||
{{ $t('common.loading') }}
|
{{ $t('common.loading') }}
|
||||||
</div>
|
|
||||||
<div v-if="store.error" class="error-message">
|
|
||||||
{{ store.error }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!store.isLoading && !store.error">
|
|
||||||
<div v-if="logs.length === 0" class="alert alert-info">
|
|
||||||
{{ $t('auditLog.noLogs') }}
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else> <!-- Wrapper for v-else content -->
|
<div v-if="store.error" class="p-4 mb-4 border-l-4 border-error bg-error/10 text-error rounded"> <!-- Error state -->
|
||||||
<div class="table-container"> <!-- Add table container -->
|
{{ store.error }}
|
||||||
<table class="table table-striped table-hover">
|
</div>
|
||||||
<thead>
|
|
||||||
<tr>
|
<div v-if="!store.isLoading && !store.error">
|
||||||
<th>{{ $t('auditLog.table.timestamp') }}</th>
|
<div v-if="logs.length === 0" class="p-4 mb-4 border-l-4 border-blue-400 bg-blue-100 text-blue-700 rounded"> <!-- No logs state -->
|
||||||
<th>{{ $t('auditLog.table.actionType') }}</th>
|
{{ $t('auditLog.noLogs') }}
|
||||||
<th>{{ $t('auditLog.table.details') }}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="log in logs" :key="log.id">
|
|
||||||
<td>{{ formatTimestamp(log.timestamp) }}</td>
|
|
||||||
<td>{{ translateActionType(log.action_type) }}</td>
|
|
||||||
<td>
|
|
||||||
<pre v-if="log.details">{{ formatDetails(log.details) }}</pre>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div> <!-- End table container -->
|
|
||||||
<!-- Pagination Controls -->
|
|
||||||
<nav aria-label="Audit Log Pagination" v-if="totalPages > 1">
|
|
||||||
<ul class="pagination justify-content-center">
|
|
||||||
<li class="page-item" :class="{ disabled: currentPage === 1 }">
|
|
||||||
<a class="page-link" href="#" @click.prevent="changePage(currentPage - 1)">«</a>
|
|
||||||
</li>
|
|
||||||
<li v-for="page in paginationRange" :key="page" class="page-item" :class="{ active: page === currentPage, 'disabled': page === '...' }">
|
|
||||||
<a v-if="page !== '...'" class="page-link" href="#" @click.prevent="changePage(page as number)">{{ page }}</a>
|
|
||||||
<span v-else class="page-link">...</span>
|
|
||||||
</li>
|
|
||||||
<li class="page-item" :class="{ disabled: currentPage === totalPages }">
|
|
||||||
<a class="page-link" href="#" @click.prevent="changePage(currentPage + 1)">»</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<div class="text-center text-muted mt-2">
|
|
||||||
{{ $t('auditLog.paginationInfo', { currentPage, totalPages, totalLogs }) }}
|
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- This closes the v-else block starting implicitly at line 18 -->
|
<div v-else>
|
||||||
</div> <!-- This closes the v-if block starting at line 14 -->
|
<div class="border border-border rounded-lg overflow-hidden shadow-sm mt-4 bg-background"> <!-- Table container -->
|
||||||
</div> <!-- This closes the root div -->
|
<div class="overflow-x-auto"> <!-- Allow horizontal scroll -->
|
||||||
|
<table class="min-w-full divide-y divide-border text-sm"> <!-- Table styling -->
|
||||||
|
<thead class="bg-header">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left font-medium text-text-secondary tracking-wider whitespace-nowrap">{{ $t('auditLog.table.timestamp') }}</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left font-medium text-text-secondary tracking-wider whitespace-nowrap">{{ $t('auditLog.table.actionType') }}</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left font-medium text-text-secondary tracking-wider">{{ $t('auditLog.table.details') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-border">
|
||||||
|
<tr v-for="log in logs" :key="log.id" class="hover:bg-header/50"> <!-- Table rows with hover -->
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">{{ formatTimestamp(log.timestamp) }}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">{{ translateActionType(log.action_type) }}</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<pre v-if="log.details" class="whitespace-pre-wrap break-all bg-header/50 p-2 border border-border/50 rounded text-xs font-mono max-h-40 overflow-y-auto">{{ formatDetails(log.details) }}</pre> <!-- Details pre styling -->
|
||||||
|
<span v-else class="text-text-secondary">-</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Pagination Controls -->
|
||||||
|
<nav aria-label="Audit Log Pagination" v-if="totalPages > 1" class="mt-6 flex justify-center">
|
||||||
|
<ul class="inline-flex items-center -space-x-px">
|
||||||
|
<li>
|
||||||
|
<a href="#" @click.prevent="changePage(currentPage - 1)"
|
||||||
|
:class="['px-3 py-2 ml-0 leading-tight text-text-secondary bg-background border border-border rounded-l-lg hover:bg-header hover:text-foreground', { 'opacity-50 cursor-not-allowed pointer-events-none': currentPage === 1 }]">
|
||||||
|
«
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li v-for="page in paginationRange" :key="page">
|
||||||
|
<a v-if="page !== '...'" href="#" @click.prevent="changePage(page as number)"
|
||||||
|
:class="['px-3 py-2 leading-tight border border-border', page === currentPage ? 'text-button-text bg-button border-button hover:bg-button-hover' : 'text-text-secondary bg-background hover:bg-header hover:text-foreground']">
|
||||||
|
{{ page }}
|
||||||
|
</a>
|
||||||
|
<span v-else class="px-3 py-2 leading-tight text-text-secondary bg-background border border-border">...</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#" @click.prevent="changePage(currentPage + 1)"
|
||||||
|
:class="['px-3 py-2 leading-tight text-text-secondary bg-background border border-border rounded-r-lg hover:bg-header hover:text-foreground', { 'opacity-50 cursor-not-allowed pointer-events-none': currentPage === totalPages }]">
|
||||||
|
»
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div class="text-center text-text-secondary text-sm mt-3"> <!-- Pagination info -->
|
||||||
|
{{ $t('auditLog.paginationInfo', { currentPage, totalPages, totalLogs }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -145,172 +160,5 @@ const paginationRange = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.audit-log-view {
|
/* Remove all scoped styles as they are now handled by Tailwind utility classes */
|
||||||
padding: var(--base-padding, 20px);
|
|
||||||
color: var(--text-color);
|
|
||||||
background-color: var(--app-bg-color);
|
|
||||||
min-height: calc(100vh - 60px); /* Adjust based on actual header/footer */
|
|
||||||
max-width: 1400px; /* Limit max width for better readability on large screens */
|
|
||||||
margin: 0 auto; /* Center the view */
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-log-view h1 {
|
|
||||||
margin-bottom: calc(var(--base-margin, 1rem) * 1.5); /* Adjust space */
|
|
||||||
padding-bottom: var(--base-margin, 1rem);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
font-size: 1.8rem;
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-indicator, .error-message, .alert {
|
|
||||||
margin-top: var(--base-margin, 1rem);
|
|
||||||
padding: var(--base-padding, 1rem);
|
|
||||||
border-radius: 6px; /* Consistent border radius */
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-indicator {
|
|
||||||
color: var(--text-color-secondary);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
color: #842029;
|
|
||||||
background-color: #f8d7da;
|
|
||||||
border: 1px solid #f5c2c7;
|
|
||||||
border-left-width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table container with shadow and border */
|
|
||||||
.table-container {
|
|
||||||
border: 1px solid var(--border-color, #dee2e6);
|
|
||||||
border-radius: 8px; /* Match SettingsView */
|
|
||||||
overflow: hidden; /* Clip table corners */
|
|
||||||
margin-top: var(--base-margin, 1rem);
|
|
||||||
background-color: var(--content-bg-color, var(--app-bg-color)); /* Match SettingsView */
|
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.05); /* Match SettingsView */
|
|
||||||
overflow-x: auto; /* Allow horizontal scroll on small screens */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table base styles */
|
|
||||||
.table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
color: var(--text-color);
|
|
||||||
border: none; /* Container handles border */
|
|
||||||
font-size: 0.95rem; /* Slightly larger font */
|
|
||||||
}
|
|
||||||
|
|
||||||
.table th,
|
|
||||||
.table td {
|
|
||||||
padding: 0.9rem 1.1rem; /* Adjust padding */
|
|
||||||
vertical-align: middle;
|
|
||||||
border-top: 1px solid var(--border-color-light, var(--border-color)); /* Use lighter border */
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove top border for the first body row */
|
|
||||||
.table tbody tr:first-child td {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table header */
|
|
||||||
.table thead th {
|
|
||||||
vertical-align: bottom;
|
|
||||||
border-bottom: 2px solid var(--border-color, #dee2e6);
|
|
||||||
border-top: none;
|
|
||||||
background-color: var(--table-header-bg-color, var(--header-bg-color)); /* Use variable */
|
|
||||||
color: var(--table-header-text-color, var(--text-color)); /* Use variable */
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Striped rows */
|
|
||||||
.table-striped tbody tr:nth-of-type(odd) {
|
|
||||||
background-color: var(--table-stripe-bg-color, rgba(0,0,0,0.02)); /* More subtle stripe */
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
.table-striped tbody tr:nth-of-type(even) {
|
|
||||||
background-color: transparent; /* Use container background */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hover effect */
|
|
||||||
.table-hover tbody tr:hover {
|
|
||||||
background-color: var(--table-hover-bg-color, rgba(0,0,0,0.04)); /* Subtle hover */
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Details <pre> styling */
|
|
||||||
pre {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
background-color: var(--code-bg-color, var(--header-bg-color)); /* Use code background */
|
|
||||||
padding: 0.6rem 0.8rem; /* Adjust padding */
|
|
||||||
border: 1px solid var(--border-color-light, var(--border-color));
|
|
||||||
border-radius: 5px; /* Consistent radius */
|
|
||||||
font-size: 0.88em; /* Adjust font size */
|
|
||||||
color: var(--code-text-color, var(--text-color)); /* Use code text color */
|
|
||||||
max-height: 180px; /* Increase max height slightly */
|
|
||||||
overflow-y: auto;
|
|
||||||
margin: 0;
|
|
||||||
font-family: var(--font-family-monospace); /* Use monospace font */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pagination styling */
|
|
||||||
.pagination {
|
|
||||||
margin-top: calc(var(--base-margin, 1rem) * 2); /* Increase top margin */
|
|
||||||
justify-content: center; /* Ensure centered */
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-item .page-link {
|
|
||||||
color: var(--link-color, #007bff);
|
|
||||||
background-color: var(--content-bg-color, var(--app-bg-color)); /* Match container bg */
|
|
||||||
border: 1px solid var(--border-color, #dee2e6);
|
|
||||||
margin: 0 3px; /* Adjust margin */
|
|
||||||
border-radius: 5px; /* Match other elements */
|
|
||||||
padding: 0.4rem 0.8rem; /* Adjust padding */
|
|
||||||
transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-item.active .page-link {
|
|
||||||
z-index: 3;
|
|
||||||
color: var(--button-text-color, #fff);
|
|
||||||
background-color: var(--button-bg-color, #007bff);
|
|
||||||
border-color: var(--button-bg-color, #007bff);
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-item.disabled .page-link {
|
|
||||||
color: var(--text-color-secondary, #6c757d);
|
|
||||||
pointer-events: none;
|
|
||||||
background-color: var(--content-bg-color, var(--app-bg-color));
|
|
||||||
border-color: var(--border-color, #dee2e6);
|
|
||||||
opacity: 0.6; /* Adjust opacity */
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-link:hover { /* Combined hover for active/inactive */
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-item:not(.active) .page-link:hover {
|
|
||||||
color: var(--link-hover-color, #0056b3);
|
|
||||||
background-color: var(--header-bg-color, #e9ecef);
|
|
||||||
border-color: var(--border-color, #dee2e6);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Alert Info styling */
|
|
||||||
.alert-info {
|
|
||||||
color: var(--info-text-color, #0c5460); /* Specific info color */
|
|
||||||
background-color: var(--info-bg-color, #d1ecf1); /* Specific info background */
|
|
||||||
border: 1px solid var(--info-border-color, #bee5eb); /* Specific info border */
|
|
||||||
border-left-width: 4px; /* Add left accent border */
|
|
||||||
text-align: left; /* Align text left */
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-muted {
|
|
||||||
color: var(--text-color-secondary) !important;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user