generated from corrad-software/corrad-af-2024
712 lines
25 KiB
Vue
712 lines
25 KiB
Vue
<script setup>
|
|
import { ref, computed, onMounted, watch } from 'vue';
|
|
|
|
const props = defineProps({
|
|
documentId: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
userId: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
showFilters: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
pageSize: {
|
|
type: Number,
|
|
default: 25
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['log-selected', 'export-logs']);
|
|
|
|
// Local state
|
|
const auditLogs = ref([]);
|
|
const isLoading = ref(false);
|
|
const currentPage = ref(1);
|
|
const totalLogs = ref(0);
|
|
const selectedLog = ref(null);
|
|
const showDetails = ref(false);
|
|
|
|
// Filters
|
|
const filters = ref({
|
|
dateRange: {
|
|
start: null,
|
|
end: null
|
|
},
|
|
actions: [],
|
|
users: [],
|
|
ipAddresses: [],
|
|
searchQuery: '',
|
|
severity: []
|
|
});
|
|
|
|
// Filter options
|
|
const actionTypes = [
|
|
{ value: 'view', label: 'View Document', icon: 'mdi:eye', color: 'blue' },
|
|
{ value: 'download', label: 'Download', icon: 'mdi:download', color: 'green' },
|
|
{ value: 'upload', label: 'Upload', icon: 'mdi:upload', color: 'purple' },
|
|
{ value: 'edit', label: 'Edit/Modify', icon: 'mdi:pencil', color: 'orange' },
|
|
{ value: 'delete', label: 'Delete', icon: 'mdi:delete', color: 'red' },
|
|
{ value: 'share', label: 'Share', icon: 'mdi:share', color: 'teal' },
|
|
{ value: 'permission_change', label: 'Permission Change', icon: 'mdi:shield-account', color: 'indigo' },
|
|
{ value: 'version_create', label: 'Create Version', icon: 'mdi:source-branch', color: 'emerald' },
|
|
{ value: 'version_restore', label: 'Restore Version', icon: 'mdi:restore', color: 'amber' },
|
|
{ value: 'login', label: 'User Login', icon: 'mdi:login', color: 'cyan' },
|
|
{ value: 'logout', label: 'User Logout', icon: 'mdi:logout', color: 'gray' },
|
|
{ value: 'access_granted', label: 'Access Granted', icon: 'mdi:check-circle', color: 'green' },
|
|
{ value: 'access_denied', label: 'Access Denied', icon: 'mdi:close-circle', color: 'red' }
|
|
];
|
|
|
|
const severityLevels = [
|
|
{ value: 'info', label: 'Information', color: 'blue' },
|
|
{ value: 'warning', label: 'Warning', color: 'yellow' },
|
|
{ value: 'error', label: 'Error', color: 'red' },
|
|
{ value: 'critical', label: 'Critical', color: 'purple' }
|
|
];
|
|
|
|
// Mock audit log data - replace with API call
|
|
const mockAuditLogs = [
|
|
{
|
|
id: 'audit_1',
|
|
timestamp: '2023-12-20T14:30:00Z',
|
|
action: 'view',
|
|
severity: 'info',
|
|
userId: 'user1',
|
|
userName: 'Aiman Fakhrullah',
|
|
userEmail: 'aiman@example.com',
|
|
documentId: 'doc_123',
|
|
documentName: 'Annual_Budget_2024.xlsx',
|
|
ipAddress: '192.168.1.100',
|
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
details: {
|
|
action: 'Document viewed',
|
|
duration: '2 minutes 30 seconds',
|
|
location: '/private-cabinet/finance',
|
|
method: 'web_interface'
|
|
},
|
|
changes: null,
|
|
success: true
|
|
},
|
|
{
|
|
id: 'audit_2',
|
|
timestamp: '2023-12-20T14:25:00Z',
|
|
action: 'download',
|
|
severity: 'info',
|
|
userId: 'user1',
|
|
userName: 'Aiman Fakhrullah',
|
|
userEmail: 'aiman@example.com',
|
|
documentId: 'doc_456',
|
|
documentName: 'Project_Plan_2024.pdf',
|
|
ipAddress: '192.168.1.100',
|
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
details: {
|
|
action: 'Document downloaded',
|
|
fileSize: '2.5 MB',
|
|
downloadMethod: 'direct_download',
|
|
location: '/public-cabinet/projects'
|
|
},
|
|
changes: null,
|
|
success: true
|
|
},
|
|
{
|
|
id: 'audit_3',
|
|
timestamp: '2023-12-20T14:20:00Z',
|
|
action: 'access_denied',
|
|
severity: 'warning',
|
|
userId: 'user2',
|
|
userName: 'Sarah Ahmed',
|
|
userEmail: 'sarah@example.com',
|
|
documentId: 'doc_789',
|
|
documentName: 'Confidential_Report.docx',
|
|
ipAddress: '192.168.1.105',
|
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
|
details: {
|
|
action: 'Access denied to confidential document',
|
|
reason: 'Insufficient permissions',
|
|
attemptedAction: 'view',
|
|
location: '/private-cabinet/confidential'
|
|
},
|
|
changes: null,
|
|
success: false
|
|
},
|
|
{
|
|
id: 'audit_4',
|
|
timestamp: '2023-12-20T14:15:00Z',
|
|
action: 'upload',
|
|
severity: 'info',
|
|
userId: 'user3',
|
|
userName: 'Ahmad Hassan',
|
|
userEmail: 'ahmad@example.com',
|
|
documentId: 'doc_new_001',
|
|
documentName: 'Meeting_Minutes_Dec2023.docx',
|
|
ipAddress: '192.168.1.110',
|
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
details: {
|
|
action: 'New document uploaded',
|
|
fileSize: '1.2 MB',
|
|
fileType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
location: '/public-cabinet/meetings',
|
|
metadata: {
|
|
title: 'Meeting Minutes December 2023',
|
|
department: 'HR',
|
|
tags: ['meeting', 'minutes', 'hr']
|
|
}
|
|
},
|
|
changes: {
|
|
type: 'create',
|
|
description: 'Document created with initial metadata'
|
|
},
|
|
success: true
|
|
}
|
|
];
|
|
|
|
// Computed properties
|
|
const filteredLogs = computed(() => {
|
|
let logs = [...auditLogs.value];
|
|
|
|
// Apply filters
|
|
if (filters.value.searchQuery) {
|
|
const query = filters.value.searchQuery.toLowerCase();
|
|
logs = logs.filter(log =>
|
|
log.userName.toLowerCase().includes(query) ||
|
|
log.documentName.toLowerCase().includes(query) ||
|
|
log.details.action.toLowerCase().includes(query) ||
|
|
log.ipAddress.includes(query)
|
|
);
|
|
}
|
|
|
|
if (filters.value.actions.length > 0) {
|
|
logs = logs.filter(log => filters.value.actions.includes(log.action));
|
|
}
|
|
|
|
if (filters.value.users.length > 0) {
|
|
logs = logs.filter(log => filters.value.users.includes(log.userId));
|
|
}
|
|
|
|
if (filters.value.severity.length > 0) {
|
|
logs = logs.filter(log => filters.value.severity.includes(log.severity));
|
|
}
|
|
|
|
if (filters.value.dateRange.start && filters.value.dateRange.end) {
|
|
const start = new Date(filters.value.dateRange.start);
|
|
const end = new Date(filters.value.dateRange.end);
|
|
logs = logs.filter(log => {
|
|
const logDate = new Date(log.timestamp);
|
|
return logDate >= start && logDate <= end;
|
|
});
|
|
}
|
|
|
|
return logs;
|
|
});
|
|
|
|
const paginatedLogs = computed(() => {
|
|
const start = (currentPage.value - 1) * props.pageSize;
|
|
const end = start + props.pageSize;
|
|
return filteredLogs.value.slice(start, end);
|
|
});
|
|
|
|
const totalPages = computed(() => {
|
|
return Math.ceil(filteredLogs.value.length / props.pageSize);
|
|
});
|
|
|
|
// Methods
|
|
const loadAuditLogs = async () => {
|
|
isLoading.value = true;
|
|
|
|
try {
|
|
// Simulate API call
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
// In real implementation, this would be an API call
|
|
auditLogs.value = mockAuditLogs;
|
|
totalLogs.value = mockAuditLogs.length;
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load audit logs:', error);
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const getActionConfig = (action) => {
|
|
return actionTypes.find(type => type.value === action) || {
|
|
value: action,
|
|
label: action,
|
|
icon: 'mdi:information',
|
|
color: 'gray'
|
|
};
|
|
};
|
|
|
|
const getSeverityConfig = (severity) => {
|
|
return severityLevels.find(level => level.value === severity) || {
|
|
value: severity,
|
|
label: severity,
|
|
color: 'gray'
|
|
};
|
|
};
|
|
|
|
const formatTimestamp = (timestamp) => {
|
|
return new Date(timestamp).toLocaleString();
|
|
};
|
|
|
|
const getTimeAgo = (timestamp) => {
|
|
const now = new Date();
|
|
const date = new Date(timestamp);
|
|
const diffInSeconds = Math.floor((now - date) / 1000);
|
|
|
|
if (diffInSeconds < 60) return 'Just now';
|
|
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
|
|
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
|
|
return `${Math.floor(diffInSeconds / 86400)} days ago`;
|
|
};
|
|
|
|
const selectLog = (log) => {
|
|
selectedLog.value = log;
|
|
showDetails.value = true;
|
|
emit('log-selected', log);
|
|
};
|
|
|
|
const clearFilters = () => {
|
|
filters.value = {
|
|
dateRange: { start: null, end: null },
|
|
actions: [],
|
|
users: [],
|
|
ipAddresses: [],
|
|
searchQuery: '',
|
|
severity: []
|
|
};
|
|
currentPage.value = 1;
|
|
};
|
|
|
|
const exportLogs = () => {
|
|
emit('export-logs', filteredLogs.value);
|
|
};
|
|
|
|
const changePage = (page) => {
|
|
currentPage.value = page;
|
|
};
|
|
|
|
// Watch for prop changes
|
|
watch(() => props.documentId, () => {
|
|
if (props.documentId) {
|
|
// Filter logs for specific document
|
|
loadAuditLogs();
|
|
}
|
|
});
|
|
|
|
watch(() => props.userId, () => {
|
|
if (props.userId) {
|
|
// Filter logs for specific user
|
|
filters.value.users = [props.userId];
|
|
}
|
|
});
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
loadAuditLogs();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="audit-log-viewer">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
|
|
Audit Log
|
|
</h2>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
|
Showing {{ filteredLogs.length }} of {{ totalLogs }} audit entries
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex space-x-2">
|
|
<button
|
|
@click="exportLogs"
|
|
class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 text-sm"
|
|
>
|
|
<Icon name="mdi:download" class="w-4 h-4 mr-2 inline" />
|
|
Export
|
|
</button>
|
|
|
|
<button
|
|
@click="loadAuditLogs"
|
|
:disabled="isLoading"
|
|
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm disabled:opacity-50"
|
|
>
|
|
<Icon name="mdi:refresh" class="w-4 h-4 mr-2 inline" />
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div v-if="showFilters" class="filters mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
|
<!-- Search -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Search
|
|
</label>
|
|
<input
|
|
v-model="filters.searchQuery"
|
|
type="text"
|
|
placeholder="Search users, documents, actions..."
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Date Range -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Date From
|
|
</label>
|
|
<input
|
|
v-model="filters.dateRange.start"
|
|
type="datetime-local"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Date To
|
|
</label>
|
|
<input
|
|
v-model="filters.dateRange.end"
|
|
type="datetime-local"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-sm"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Actions Filter -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Actions
|
|
</label>
|
|
<select
|
|
v-model="filters.actions"
|
|
multiple
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-sm"
|
|
>
|
|
<option v-for="action in actionTypes" :key="action.value" :value="action.value">
|
|
{{ action.label }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-between items-center">
|
|
<div class="flex flex-wrap gap-2">
|
|
<!-- Severity filter chips -->
|
|
<button
|
|
v-for="severity in severityLevels"
|
|
:key="severity.value"
|
|
@click="
|
|
filters.severity.includes(severity.value)
|
|
? filters.severity = filters.severity.filter(s => s !== severity.value)
|
|
: filters.severity.push(severity.value)
|
|
"
|
|
class="px-3 py-1 rounded-full text-xs font-medium transition-colors"
|
|
:class="
|
|
filters.severity.includes(severity.value)
|
|
? `bg-${severity.color}-100 text-${severity.color}-800 dark:bg-${severity.color}-900/20 dark:text-${severity.color}-200`
|
|
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200 hover:bg-gray-200'
|
|
"
|
|
>
|
|
{{ severity.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
@click="clearFilters"
|
|
class="px-3 py-1 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"
|
|
>
|
|
Clear all filters
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading state -->
|
|
<div v-if="isLoading" class="text-center py-8">
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
<p class="text-gray-600 dark:text-gray-400">Loading audit logs...</p>
|
|
</div>
|
|
|
|
<!-- Audit log table -->
|
|
<div v-else class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 overflow-hidden">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full divide-y divide-gray-200 dark:divide-gray-600">
|
|
<thead class="bg-gray-50 dark:bg-gray-700">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
Timestamp
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
Action
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
User
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
Document
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
IP Address
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
Status
|
|
</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-600">
|
|
<tr
|
|
v-for="log in paginatedLogs"
|
|
:key="log.id"
|
|
class="hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
|
|
@click="selectLog(log)"
|
|
>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
<div>{{ formatTimestamp(log.timestamp) }}</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">{{ getTimeAgo(log.timestamp) }}</div>
|
|
</td>
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="flex items-center">
|
|
<Icon
|
|
:name="getActionConfig(log.action).icon"
|
|
class="w-4 h-4 mr-2"
|
|
:class="`text-${getActionConfig(log.action).color}-500`"
|
|
/>
|
|
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
|
{{ getActionConfig(log.action).label }}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ log.userName }}</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">{{ log.userEmail }}</div>
|
|
</td>
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<div class="text-sm text-gray-900 dark:text-gray-100 truncate max-w-[200px]">
|
|
{{ log.documentName || 'N/A' }}
|
|
</div>
|
|
</td>
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
{{ log.ipAddress }}
|
|
</td>
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span
|
|
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
|
:class="
|
|
log.success
|
|
? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-200'
|
|
: 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-200'
|
|
"
|
|
>
|
|
{{ log.success ? 'Success' : 'Failed' }}
|
|
</span>
|
|
|
|
<span
|
|
class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
|
:class="`bg-${getSeverityConfig(log.severity).color}-100 text-${getSeverityConfig(log.severity).color}-800 dark:bg-${getSeverityConfig(log.severity).color}-900/20 dark:text-${getSeverityConfig(log.severity).color}-200`"
|
|
>
|
|
{{ getSeverityConfig(log.severity).label }}
|
|
</span>
|
|
</td>
|
|
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
|
<button
|
|
@click.stop="selectLog(log)"
|
|
class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"
|
|
>
|
|
View Details
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div v-if="paginatedLogs.length === 0" class="text-center py-8">
|
|
<Icon name="mdi:file-document-outline" class="w-16 h-16 mx-auto text-gray-400 mb-4" />
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
|
|
No audit logs found
|
|
</h3>
|
|
<p class="text-gray-500 dark:text-gray-400">
|
|
Try adjusting your filters or search criteria.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div v-if="totalPages > 1" class="flex items-center justify-between mt-6">
|
|
<div class="text-sm text-gray-700 dark:text-gray-300">
|
|
Showing {{ (currentPage - 1) * pageSize + 1 }} to {{ Math.min(currentPage * pageSize, filteredLogs.length) }} of {{ filteredLogs.length }} results
|
|
</div>
|
|
|
|
<div class="flex space-x-1">
|
|
<button
|
|
@click="changePage(currentPage - 1)"
|
|
:disabled="currentPage === 1"
|
|
class="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm disabled:opacity-50 hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
>
|
|
Previous
|
|
</button>
|
|
|
|
<button
|
|
v-for="page in Math.min(5, totalPages)"
|
|
:key="page"
|
|
@click="changePage(page)"
|
|
class="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm"
|
|
:class="page === currentPage ? 'bg-blue-600 text-white border-blue-600' : 'hover:bg-gray-50 dark:hover:bg-gray-700'"
|
|
>
|
|
{{ page }}
|
|
</button>
|
|
|
|
<button
|
|
@click="changePage(currentPage + 1)"
|
|
:disabled="currentPage === totalPages"
|
|
class="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm disabled:opacity-50 hover:bg-gray-50 dark:hover:bg-gray-700"
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audit log detail modal -->
|
|
<rs-modal :visible="showDetails" @close="showDetails = false" size="4xl">
|
|
<template #header>
|
|
<h3 class="text-lg font-semibold">Audit Log Details</h3>
|
|
</template>
|
|
|
|
<template #body>
|
|
<div v-if="selectedLog" class="space-y-6">
|
|
<!-- Overview -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<h4 class="text-md font-medium text-gray-900 dark:text-gray-100 mb-3">Basic Information</h4>
|
|
<dl class="space-y-2 text-sm">
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500 dark:text-gray-400">Timestamp:</dt>
|
|
<dd class="text-gray-900 dark:text-gray-100">{{ formatTimestamp(selectedLog.timestamp) }}</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500 dark:text-gray-400">Action:</dt>
|
|
<dd class="text-gray-900 dark:text-gray-100">{{ getActionConfig(selectedLog.action).label }}</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500 dark:text-gray-400">Severity:</dt>
|
|
<dd>
|
|
<span
|
|
class="px-2 py-1 rounded-full text-xs font-medium"
|
|
:class="`bg-${getSeverityConfig(selectedLog.severity).color}-100 text-${getSeverityConfig(selectedLog.severity).color}-800`"
|
|
>
|
|
{{ getSeverityConfig(selectedLog.severity).label }}
|
|
</span>
|
|
</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500 dark:text-gray-400">Status:</dt>
|
|
<dd>
|
|
<span
|
|
class="px-2 py-1 rounded-full text-xs font-medium"
|
|
:class="
|
|
selectedLog.success
|
|
? 'bg-green-100 text-green-800'
|
|
: 'bg-red-100 text-red-800'
|
|
"
|
|
>
|
|
{{ selectedLog.success ? 'Success' : 'Failed' }}
|
|
</span>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 class="text-md font-medium text-gray-900 dark:text-gray-100 mb-3">User Information</h4>
|
|
<dl class="space-y-2 text-sm">
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500 dark:text-gray-400">User:</dt>
|
|
<dd class="text-gray-900 dark:text-gray-100">{{ selectedLog.userName }}</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500 dark:text-gray-400">Email:</dt>
|
|
<dd class="text-gray-900 dark:text-gray-100">{{ selectedLog.userEmail }}</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500 dark:text-gray-400">IP Address:</dt>
|
|
<dd class="text-gray-900 dark:text-gray-100 font-mono">{{ selectedLog.ipAddress }}</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500 dark:text-gray-400">User Agent:</dt>
|
|
<dd class="text-gray-900 dark:text-gray-100 text-xs break-all">{{ selectedLog.userAgent }}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Document Information -->
|
|
<div v-if="selectedLog.documentId">
|
|
<h4 class="text-md font-medium text-gray-900 dark:text-gray-100 mb-3">Document Information</h4>
|
|
<dl class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500 dark:text-gray-400">Document:</dt>
|
|
<dd class="text-gray-900 dark:text-gray-100">{{ selectedLog.documentName }}</dd>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<dt class="text-gray-500 dark:text-gray-400">Document ID:</dt>
|
|
<dd class="text-gray-900 dark:text-gray-100 font-mono">{{ selectedLog.documentId }}</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<!-- Action Details -->
|
|
<div>
|
|
<h4 class="text-md font-medium text-gray-900 dark:text-gray-100 mb-3">Action Details</h4>
|
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
|
<pre class="text-sm text-gray-900 dark:text-gray-100 whitespace-pre-wrap">{{ JSON.stringify(selectedLog.details, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Changes -->
|
|
<div v-if="selectedLog.changes">
|
|
<h4 class="text-md font-medium text-gray-900 dark:text-gray-100 mb-3">Changes Made</h4>
|
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
|
<pre class="text-sm text-gray-900 dark:text-gray-100 whitespace-pre-wrap">{{ JSON.stringify(selectedLog.changes, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #footer>
|
|
<div class="flex justify-end">
|
|
<button
|
|
@click="showDetails = false"
|
|
class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</rs-modal>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.audit-log-viewer table {
|
|
min-width: 800px;
|
|
}
|
|
</style> |