Files
aliasvault/apps/server/AliasVault.Admin/Main/Pages/Logging/General.razor
2025-04-30 19:03:18 +02:00

274 lines
9.6 KiB
Plaintext

@page "/logging/general"
@using AliasVault.RazorComponents.Tables
@inherits MainBase
<LayoutPageTitle>System logs</LayoutPageTitle>
<PageHeader
BreadcrumbItems="@BreadcrumbItems"
Title="@(TotalRecords > 0 ? $"General logs ({TotalRecords:N0})" : "General logs")"
Description="This page shows an overview of recent system logs.">
<CustomActions>
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
</CustomActions>
</PageHeader>
@if (IsInitialized)
{
<div class="px-4">
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
<div class="mb-3 flex space-x-4">
<div class="flex w-full">
<div class="w-2/3 pr-2">
<div class="relative">
<SearchIcon />
<input type="text" @bind-value="SearchTerm" @bind-value:event="oninput" id="search" placeholder="Search logs..." class="w-full px-4 ps-10 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
</div>
</div>
<div class="w-1/3 pl-2">
<select @bind="SelectedServiceName" class="w-full px-4 py-2 border rounded text-sm text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
<option value="">All Services</option>
@foreach (var service in ServiceNames)
{
<option value="@service">@service</option>
}
</select>
</div>
</div>
</div>
</div>
}
@if (IsLoading)
{
<LoadingIndicator />
}
else
{
<div class="px-4">
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
@foreach (var log in LogList)
{
<SortableTableRow>
<SortableTableColumn IsPrimary="true">@log.Id</SortableTableColumn>
<SortableTableColumn>@log.TimeStamp.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
<SortableTableColumn>@log.Application</SortableTableColumn>
<SortableTableColumn>
@{
string bgColor = log.Level switch
{
"Information" => "bg-blue-500",
"Error" => "bg-red-500",
"Warning" => "bg-yellow-500",
"Debug" => "bg-green-500",
_ => "bg-gray-500"
};
}
<span class="px-2 py-1 rounded-full text-white @bgColor">
@log.Level
</span>
</SortableTableColumn>
<SortableTableColumn Title="@log.Exception">
@if (log.SourceContext.Length > 0)
{
<span>@log.SourceContext: </span>
}
@log.Message
</SortableTableColumn>
</SortableTableRow>
}
</SortableTable>
</div>
}
@code {
private readonly List<TableColumn> _tableColumns = [
new TableColumn { Title = "ID", PropertyName = "Id" },
new TableColumn { Title = "Time", PropertyName = "Timestamp" },
new TableColumn { Title = "Application", PropertyName = "Application" },
new TableColumn { Title = "Level", PropertyName = "Level" },
new TableColumn { Title = "Message", PropertyName = "Message" },
];
private List<Log> LogList { get; set; } = [];
private bool IsInitialized { get; set; } = false;
private bool IsLoading { get; set; } = true;
private int CurrentPage { get; set; } = 1;
private int PageSize { get; set; } = 50;
private int TotalRecords { get; set; }
private string _searchTerm = string.Empty;
private string _lastSearchTerm = string.Empty;
private string SearchTerm
{
get => _searchTerm;
set
{
if (_searchTerm != value)
{
_searchTerm = value;
_ = RefreshData();
}
}
}
private string _selectedServiceName = string.Empty;
private string SelectedServiceName
{
get => _selectedServiceName;
set
{
if (_selectedServiceName != value)
{
_selectedServiceName = value;
_ = RefreshData();
}
}
}
private List<string> ServiceNames { get; set; } = [];
private string SortColumn { get; set; } = "Id";
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
private async Task HandleSortChanged((string column, SortDirection direction) sort)
{
SortColumn = sort.column;
SortDirection = sort.direction;
await RefreshData();
}
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
ServiceNames = await dbContext.Logs.Select(l => l.Application).Distinct().ToListAsync();
await RefreshData();
}
}
private void HandlePageChanged(int newPage)
{
CurrentPage = newPage;
_ = RefreshData();
}
private async Task RefreshData()
{
IsLoading = true;
StateHasChanged();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
var query = dbContext.Logs.AsQueryable();
query = ApplySearchTermFilter(query);
query = ApplyServiceNameFilter(query);
// Apply sort.
switch (SortColumn)
{
case "Application":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Application)
: query.OrderByDescending(x => x.Application);
break;
case "Message":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Message)
: query.OrderByDescending(x => x.Message);
break;
case "Level":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Level)
: query.OrderByDescending(x => x.Level);
break;
case "Timestamp":
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.TimeStamp)
: query.OrderByDescending(x => x.TimeStamp);
break;
default:
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => x.Id)
: query.OrderByDescending(x => x.Id);
break;
}
TotalRecords = await query.CountAsync();
LogList = await query
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
IsLoading = false;
IsInitialized = true;
StateHasChanged();
}
/// <summary>
/// Applies a search term filter to the query.
/// </summary>
/// <param name="query">The query to apply the filter to.</param>
private IQueryable<Log> ApplySearchTermFilter(IQueryable<Log> query)
{
if (!string.IsNullOrEmpty(SearchTerm))
{
// Reset page number back to 1 if the search term has changed.
if (SearchTerm != _lastSearchTerm)
{
CurrentPage = 1;
}
_lastSearchTerm = SearchTerm;
var searchTerm = SearchTerm.Trim().ToLower();
query = query.Where(x => EF.Functions.Like(x.Application.ToLower(), "%" + searchTerm + "%") ||
EF.Functions.Like(x.Message.ToLower(), "%" + searchTerm + "%") ||
EF.Functions.Like(x.Level.ToLower(), "%" + searchTerm + "%") ||
EF.Functions.Like(x.SourceContext.ToLower(), "%" + searchTerm + "%"));
}
return query;
}
/// <summary>
/// Applies a service name filter to the query.
/// </summary>
/// <param name="query">The query to apply the filter to.</param>
private IQueryable<Log> ApplyServiceNameFilter(IQueryable<Log> query)
{
if (!string.IsNullOrEmpty(SelectedServiceName))
{
query = query.Where(x => x.Application == SelectedServiceName);
}
return query;
}
private async Task DeleteLogsWithConfirmation()
{
if (await ConfirmModalService.ShowConfirmation("Confirm Delete", "Are you sure you want to delete all logs? This action cannot be undone."))
{
await DeleteLogs();
}
}
private async Task DeleteLogs()
{
IsLoading = true;
StateHasChanged();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
dbContext.Logs.RemoveRange(dbContext.Logs);
await dbContext.SaveChangesAsync();
await RefreshData();
IsLoading = false;
StateHasChanged();
}
}