User
add: language switch
8c88a89
Raw
History Blame Contribute Delete
10.5 kB
@page "/audit-logs"
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using TaskTrackingSystem.Shared.Models.AuditLog
@using TaskTrackingSystem.WebApp.Components.Partial
@rendermode @(new InteractiveServerRenderMode(prerender: false))
@attribute [Authorize]
@inject ApiClientService ApiClient
@inject IJSRuntime JS
@inject AuthenticationStateProvider AuthStateProvider
<PageTitle>@AppLocalization.PageTitle("auditLogs", "Audit Logs")</PageTitle>
<div class="space-y-6">
<!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.PageTitle("auditLogs", "Audit Logs")</h2>
<p class="text-slate-500 mt-1">@AppLocalization.PageDescription("auditLogs", "Review system activity and changes.")</p>
</div>
</div>
<!-- Search Panel -->
<div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm mb-4">
<div class="flex items-center gap-3">
<div class="relative flex-1 max-w-md">
<input @bind="searchInput" @bind:event="oninput" @onkeyup="HandleKeyUp" type="text" placeholder="@AppLocalization.Text("common.searchByUser", "Search by user, action, module, or details...")"
class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
</div>
<button @onclick="ApplySearch" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
<i data-lucide="search" class="h-4 w-4 mr-2"></i>
@AppLocalization.Text("action.search", "Search")
</button>
<button @onclick="ResetSearch" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm">
<i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i>
@AppLocalization.Text("action.reset", "Reset")
</button>
</div>
</div>
@if (isLoading)
{
<LoadingSpinner Message="@AppLocalization.Text("common.loadingAuditLogs", "Loading audit logs...")" />
}
else
{
<!-- Logs Table -->
<div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
<table class="w-full">
<thead>
<tr class="border-b border-slate-100 bg-slate-50/60">
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.date", "Timestamp")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.user", "User")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.module", "Module")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.actions", "Action")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.description", "Description")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">IP Address</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 font-sans">
@if (PagedLogs.Any())
{
@foreach (var entry in PagedLogs.Select((log, index) => (log, index)))
{
var log = entry.log;
<tr class="hover:bg-slate-50/50 transition-colors">
<td class="px-6 py-4 text-sm font-semibold text-slate-500 align-top">@((currentPage - 1) * pageSize + entry.index + 1)</td>
<td class="px-6 py-4 text-xs font-mono text-slate-500 whitespace-nowrap align-top">
@log.CreatedAt.ToString("dd-MM-yyyy HH:mm:ss")
</td>
<td class="px-6 py-4 align-top">
<div class="flex items-center gap-2">
<span class="flex h-6 w-6 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-[10px] font-bold shrink-0">
@(log.UserFullName.Length > 0 ? log.UserFullName[0].ToString().ToUpper() : "S")
</span>
<div>
<p class="text-xs font-semibold text-slate-800">@log.UserFullName</p>
<p class="text-[10px] text-slate-400">@@@log.Username</p>
</div>
</div>
</td>
<td class="px-6 py-4 align-top">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold bg-slate-100 text-slate-700 border border-slate-200">
@log.Module
</span>
</td>
<td class="px-6 py-4 align-top">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-[10px] font-bold @GetActionBadge(log.Action)">
@log.Action
</span>
</td>
<td class="px-6 py-4 text-xs text-slate-700 whitespace-normal break-words align-top">
@log.Description
</td>
<td class="px-6 py-4 text-xs font-mono text-slate-500 whitespace-nowrap align-top">
@(string.IsNullOrWhiteSpace(log.IpAddress) ? "N/A" : log.IpAddress)
</td>
</tr>
}
}
else
{
<EmptyState ColSpan="7" Icon="history" Title="@AppLocalization.Text("common.noResults", "No audit logs")" Subtitle="@AppLocalization.Text("common.noResults", "No security events or administrator operations were found.")" />
}
</tbody>
</table>
<!-- Pagination -->
<Pagination CurrentPage="currentPage"
TotalPages="TotalPages"
PageSize="pageSize"
TotalCount="TotalCount"
OnPageChanged="HandlePageChanged"
OnPageSizeChanged="HandlePageSizeChanged" />
</div>
}
</div>
@code {
private bool isLoading = true;
private List<AuditLogDto> auditLogs = new();
private string searchInput = "";
// Pagination state
private int currentPage = 1;
private int pageSize = 15;
private int totalCount = 0;
private int totalPages = 0;
private int TotalPages => totalPages;
private int TotalCount => totalCount;
private IEnumerable<AuditLogDto> PagedLogs => auditLogs;
protected override async Task OnInitializedAsync()
{
await LoadLogs();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await JS.InvokeVoidAsync("initIcons");
}
private async Task LoadLogs()
{
isLoading = true;
var client = ApiClient.CreateClient();
try
{
var url = BuildAuditLogsUrl();
var result = await client.GetFromJsonAsync<PagedResult<AuditLogDto>>(url, Serialization.CaseInsensitive);
auditLogs = result?.Items ?? new();
totalCount = result?.TotalCount ?? 0;
totalPages = result?.TotalPages ?? 0;
currentPage = result?.Page ?? currentPage;
pageSize = result?.PageSize ?? pageSize;
}
catch (Exception ex)
{
Console.WriteLine($"Load audit logs error: {ex.Message}");
}
finally
{
isLoading = false;
}
}
private void ApplySearch()
{
currentPage = 1;
_ = LoadLogs();
}
private void ResetSearch()
{
searchInput = "";
currentPage = 1;
_ = LoadLogs();
}
private void HandleKeyUp(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
ApplySearch();
}
}
private string BuildAuditLogsUrl()
{
var query = new List<string>
{
$"page={currentPage}",
$"limit={pageSize}"
};
if (!string.IsNullOrWhiteSpace(searchInput))
{
query.Add($"search={Uri.EscapeDataString(searchInput.Trim())}");
}
return $"AuditLog?{string.Join("&", query)}";
}
private async Task HandlePageChanged(int page)
{
currentPage = page;
await LoadLogs();
}
private async Task HandlePageSizeChanged(int size)
{
pageSize = size;
currentPage = 1;
await LoadLogs();
}
private string GetActionBadge(string action)
{
return action.ToLower() switch
{
"create" => "bg-emerald-50 text-emerald-700 border border-emerald-200",
"update" => "bg-amber-50 text-amber-700 border border-amber-200",
"delete" => "bg-rose-50 text-rose-700 border border-rose-200",
"assignaccess" => "bg-violet-50 text-violet-700 border border-violet-200",
_ => "bg-blue-50 text-blue-700 border border-blue-200"
};
}
}