feat: add resources and view components
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
<x-app-layout>
|
||||
<div class="container-fluid" id="main-content">
|
||||
<div class="row gx-3 gx-lg-4">
|
||||
<div class="col-12">
|
||||
<div class="card adminuiux-card">
|
||||
<div class="card-body p-0">
|
||||
|
||||
{{-- Action Log Header --}}
|
||||
<div class="p-4 pb-0">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h5 class="mb-0 fw-bold">{{ __('Action Log') }}</h5>
|
||||
<small class="text-muted">
|
||||
{{ __('Monitor all recorded user actions within the system, including authentication and data modifications.') }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<button type="button" class="btn btn-outline-dark btn-sm rounded-pill px-3" id="export-csv-btn">
|
||||
<i class="bi bi-file-earmark-excel me-1"></i> {{ __('Export CSV') }}
|
||||
</button>
|
||||
@can('manage action history')
|
||||
<button type="button" class="btn btn-outline-danger btn-sm rounded-pill px-3" id="clear-activity-logs-btn">
|
||||
<i class="bi bi-trash me-1"></i> {{ __('Clear All Logs') }}
|
||||
</button>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mb-4">
|
||||
<button type="button" class="btn btn-sm btn-dark rounded-pill px-3 filter-event" data-value="">
|
||||
<i class="bi bi-list-ul me-1"></i> {{ __('All Logs') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-dark border border-dark rounded-pill px-3 filter-event" data-value="auth">
|
||||
<i class="bi bi-shield-lock me-1"></i> {{ __('Authentication') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-dark border border-dark rounded-pill px-3 filter-event" data-value="data">
|
||||
<i class="bi bi-database-gear me-1"></i> {{ __('Data Modification') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-dark border border-dark rounded-pill px-3 filter-event" data-value="system">
|
||||
<i class="bi bi-cpu me-1"></i> {{ __('System') }}
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" id="filter-event-val" name="event" class="filter-extra" value="">
|
||||
</div>
|
||||
|
||||
{{-- Action Log Table --}}
|
||||
<div class="p-4">
|
||||
<div class="table-responsive">
|
||||
<table id="datatables" class="table table-hover table-bordered w-100 nowrap mb-0"
|
||||
data-server-side="true" data-ajax-url="{{ route('action-logs') }}"
|
||||
data-order='@json([[4, "desc"]])'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-wrap">{{ __('User') }}</th>
|
||||
<th class="text-wrap">{{ __('Action') }}</th>
|
||||
<th class="text-wrap">{{ __('Preview') }}</th>
|
||||
<th class="text-wrap">{{ __('Module') }}</th>
|
||||
<th class="text-wrap">{{ __('Executed At') }}</th>
|
||||
<th class="text-wrap" data-hide="audit">{{ __('IP Address') }}</th>
|
||||
<th class="text-wrap" data-hide="audit">{{ __('Device / Agent') }}</th>
|
||||
<th class="text-wrap" data-hide="audit">{{ __('Properties') }}</th>
|
||||
<th class="text-end text-wrap" data-orderable="false"
|
||||
data-searchable="false">{{ __('Details') }}</th>
|
||||
</tr>
|
||||
|
||||
<tr class="filter-row">
|
||||
<th><input class="form-control form-control-sm" placeholder="{{ __('Search User') }}"></th>
|
||||
<th><input class="form-control form-control-sm" placeholder="{{ __('Action') }}"></th>
|
||||
<th><input class="form-control form-control-sm" placeholder="{{ __('Keyword') }}"></th>
|
||||
<th><input class="form-control form-control-sm" placeholder="{{ __('Module') }}"></th>
|
||||
<th><input type="date" class="form-control form-control-sm"></th>
|
||||
<th><input class="form-control form-control-sm" placeholder="{{ __('IP') }}"></th>
|
||||
<th><input class="form-control form-control-sm" placeholder="{{ __('Agent') }}"></th>
|
||||
<th><input class="form-control form-control-sm" placeholder="{{ __('Props') }}"></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ADVANCED AUDIT MODAL --}}
|
||||
<div class="modal fade" id="detailLogModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow-lg" style="border-radius: 1.5rem;">
|
||||
<div class="modal-header border-0 p-4 pb-0">
|
||||
<div>
|
||||
<h5 class="modal-title fw-black tracking-tight" id="modal-event-label">Action Log Detail</h5>
|
||||
<div id="modal-module-info" class="text-theme-1 small fw-bold text-uppercase"></div>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
|
||||
{{-- Initiator & Time --}}
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 bg-light rounded-4 h-100 shadow-sm border border-white">
|
||||
<label class="extra-small text-uppercase fw-black text-secondary mb-2 d-block opacity-50">{{ __('Initiator') }}</label>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="avatar avatar-40 rounded-circle bg-white shadow-sm d-flex align-items-center justify-content-center text-theme-1">
|
||||
<i class="bi bi-person-fill"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div id="modal-causer-name" class="fw-bold text-dark"></div>
|
||||
<div id="modal-causer-email" class="text-secondary extra-small"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="p-3 bg-light rounded-4 h-100 shadow-sm border border-white">
|
||||
<label class="extra-small text-uppercase fw-black text-secondary mb-2 d-block opacity-50">{{ __('Timestamp & Origin') }}</label>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="avatar avatar-40 rounded-circle bg-white shadow-sm d-flex align-items-center justify-content-center text-info">
|
||||
<i class="bi bi-clock-history"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div id="modal-time-info" class="fw-bold text-dark small"></div>
|
||||
<div id="modal-network-info" class="text-secondary extra-small"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Data Changes List (Professional Diff Style) --}}
|
||||
<div class="mb-4">
|
||||
<label class="extra-small text-uppercase fw-black text-secondary mb-3 d-block opacity-50 ms-1">{{ __('Data Modifications') }}</label>
|
||||
<div id="modal-changes-list" class="overflow-hidden border border-light" style="border-radius: 1rem;">
|
||||
<!-- Injected here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Technical Data --}}
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 px-1">
|
||||
<label class="extra-small text-uppercase fw-black text-secondary mb-0 opacity-50">{{ __('Raw Metadata') }}</label>
|
||||
<button class="btn btn-sm btn-link extra-small text-decoration-none fw-bold p-0" onclick="copyRawJson()">{{ __('COPY JSON') }}</button>
|
||||
</div>
|
||||
<pre id="modal-raw-json" class="bg-dark text-warning p-4 rounded-4 small mb-0 shadow-sm scroll-custom" style="white-space: pre-wrap; font-size: 11px; max-height: 250px; overflow-y: auto; font-family: 'Fira Code', 'JetBrains Mono', monospace; line-height: 1.5;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-0">
|
||||
<button type="button" class="btn btn-dark w-100 py-3 rounded-pill fw-bold shadow-sm" data-bs-dismiss="modal">{{ __('Close Audit View') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const modal = new bootstrap.Modal('#detailLogModal');
|
||||
|
||||
document.addEventListener("click", e => {
|
||||
// Event category filter handler
|
||||
const filterBtn = e.target.closest(".filter-event");
|
||||
if (filterBtn) {
|
||||
document.querySelectorAll(".filter-event").forEach(b => {
|
||||
b.classList.remove("btn-dark");
|
||||
b.classList.add("btn-outline-dark", "border", "border-dark");
|
||||
});
|
||||
|
||||
filterBtn.classList.add("btn-dark");
|
||||
filterBtn.classList.remove("btn-outline-dark", "border", "border-dark");
|
||||
|
||||
document.getElementById("filter-event-val").value = filterBtn.dataset.value;
|
||||
window.reloadDataTable?.();
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = e.target.closest(".btn-detail-log");
|
||||
if (!btn) return;
|
||||
|
||||
const data = JSON.parse(btn.dataset.activity);
|
||||
|
||||
// Populating data
|
||||
document.getElementById('modal-event-label').textContent = data.event.label;
|
||||
document.getElementById('modal-causer-name').textContent = data.causer.name;
|
||||
document.getElementById('modal-causer-email').textContent = data.causer.email;
|
||||
document.getElementById('modal-module-info').textContent = data.subject.module + " — " + data.subject.type;
|
||||
document.getElementById('modal-time-info').textContent = data.meta.time;
|
||||
document.getElementById('modal-network-info').textContent = "IP: " + data.meta.ip;
|
||||
|
||||
// Build Changes
|
||||
const container = document.getElementById('modal-changes-list');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (data.changes.length > 0) {
|
||||
data.changes.forEach(change => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'bg-white border-bottom p-3 transition-all hover-bg-light';
|
||||
|
||||
let diffContent = '';
|
||||
if (change.old === 'NULL' && change.new !== 'NULL') {
|
||||
// Creation / Initialization
|
||||
diffContent = `
|
||||
<div class="diff-box bg-success bg-opacity-10 border-success border-opacity-25 p-2 rounded small w-100">
|
||||
<span class="text-success fw-bold me-2">+</span> ${change.new}
|
||||
</div>`;
|
||||
} else if (change.old !== null) {
|
||||
// Update
|
||||
diffContent = `
|
||||
<div class="d-flex flex-column gap-2 w-100">
|
||||
<div class="diff-box bg-danger bg-opacity-10 border-danger border-opacity-25 p-2 rounded small">
|
||||
<span class="text-danger fw-bold me-2">-</span> ${change.old}
|
||||
</div>
|
||||
<div class="diff-box bg-success bg-opacity-10 border-success border-opacity-25 p-2 rounded small">
|
||||
<span class="text-success fw-bold me-2">+</span> ${change.new}
|
||||
</div>
|
||||
</div>`;
|
||||
} else {
|
||||
// General
|
||||
diffContent = `<div class="p-2 small text-dark fw-medium">${change.new}</div>`;
|
||||
}
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="row align-items-start g-3">
|
||||
<div class="col-md-3">
|
||||
<span class="extra-small text-uppercase fw-black text-secondary d-block mt-1">${change.field}</span>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
${diffContent}
|
||||
</div>
|
||||
</div>`;
|
||||
container.appendChild(row);
|
||||
});
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="p-5 text-center bg-light bg-opacity-50">
|
||||
<i class="bi bi-info-circle text-muted fs-2 mb-2 d-block"></i>
|
||||
<div class="text-muted small fw-bold">No data modifications recorded for this event.</div>
|
||||
<div class="extra-small text-secondary opacity-75">This usually happens for authentication or read events.</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
document.getElementById('modal-raw-json').textContent = JSON.stringify(data.raw, null, 2);
|
||||
modal.show();
|
||||
});
|
||||
|
||||
// Export CSV Handler
|
||||
const exportBtn = document.getElementById('export-csv-btn');
|
||||
if (exportBtn) {
|
||||
exportBtn.addEventListener('click', () => {
|
||||
const event = document.getElementById('filter-event-val').value;
|
||||
const search = document.querySelector('.dataTables_filter input')?.value || '';
|
||||
|
||||
let url = `{{ route('action-logs.export') }}?event=${event}&search=${encodeURIComponent(search)}`;
|
||||
window.location.href = url;
|
||||
toastr?.info('{{ __("Preparing your export, please wait...") }}');
|
||||
});
|
||||
}
|
||||
|
||||
// Clear Activity Logs Handler
|
||||
const clearBtn = document.getElementById('clear-activity-logs-btn');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
StandardSwal.fire({
|
||||
title: 'Clear Global Action Logs?',
|
||||
text: 'You are about to permanently erase all recorded system audit trails. This action cannot be undone.',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
customClass: {
|
||||
confirmButton: 'btn-pill-danger',
|
||||
cancelButton: 'btn-pill-cancel'
|
||||
},
|
||||
confirmButtonText: 'Yes, Clear All',
|
||||
cancelButtonText: 'Cancel'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
fetch('{{ route("action-logs.clear") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.reloadDataTable?.();
|
||||
StandardSwal.fire({
|
||||
icon: 'success',
|
||||
title: 'Logs Cleared!',
|
||||
text: data.message || 'System audit trails have been successfully removed.',
|
||||
timer: 2000,
|
||||
showConfirmButton: false,
|
||||
timerProgressBar: true
|
||||
});
|
||||
} else {
|
||||
StandardSwal.fire({ icon: 'error', title: 'Action Failed!', text: data.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function copyRawJson() {
|
||||
const content = document.getElementById('modal-raw-json').textContent;
|
||||
navigator.clipboard.writeText(content);
|
||||
toastr?.info('{{ __("Audit JSON Copied to Clipboard") }}');
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.fw-black { font-weight: 900; }
|
||||
.tracking-tight { letter-spacing: -1px; }
|
||||
.extra-small { font-size: 0.65rem; }
|
||||
.hover-bg-light:hover { background-color: #f8fafc !important; }
|
||||
.diff-box { border: 1px solid transparent; font-family: 'Fira Code', 'JetBrains Mono', monospace; word-break: break-all; }
|
||||
.scroll-custom::-webkit-scrollbar { width: 6px; }
|
||||
.scroll-custom::-webkit-scrollbar-thumb { background: #334155; border-radius: 10px; }
|
||||
.bg-dark { background-color: #0f172a !important; }
|
||||
</style>
|
||||
@endpush
|
||||
</x-app-layout>
|
||||
Reference in New Issue
Block a user