mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-15 21:29:07 -04:00
Add cancellation token to search fields in admin (#1110)
This commit is contained in:
committed by
Leendert de Borst
parent
6eb8266d05
commit
27fc298b5e
@@ -15,7 +15,7 @@
|
||||
</svg>
|
||||
Email Storage Stats
|
||||
</a>
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
<RefreshButton OnClick="() => RefreshData(CancellationToken.None)" ButtonText="Refresh" />
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
|
||||
@@ -88,6 +88,7 @@ else
|
||||
private int PageSize { get; set; } = 50;
|
||||
private int TotalRecords { get; set; }
|
||||
private string _searchTerm = string.Empty;
|
||||
private CancellationTokenSource? _searchCancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// The last search term.
|
||||
@@ -102,7 +103,9 @@ else
|
||||
if (_searchTerm != value)
|
||||
{
|
||||
_searchTerm = value;
|
||||
_ = RefreshData();
|
||||
_searchCancellationTokenSource?.Cancel();
|
||||
_searchCancellationTokenSource = new CancellationTokenSource();
|
||||
_ = RefreshData(_searchCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +116,7 @@ else
|
||||
{
|
||||
SortColumn = sort.column;
|
||||
SortDirection = sort.direction;
|
||||
await RefreshData();
|
||||
await RefreshData(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -134,53 +137,65 @@ else
|
||||
_searchTerm = SearchTermFromQuery;
|
||||
}
|
||||
|
||||
await RefreshData();
|
||||
await RefreshData(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePageChanged(int newPage)
|
||||
{
|
||||
CurrentPage = newPage;
|
||||
_ = RefreshData();
|
||||
_ = RefreshData(CancellationToken.None);
|
||||
}
|
||||
|
||||
private async Task RefreshData()
|
||||
private async Task RefreshData(CancellationToken cancellationToken = default)
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
|
||||
IQueryable<Email> query = dbContext.Emails;
|
||||
|
||||
query = ApplySearchFilter(query);
|
||||
query = ApplySort(query);
|
||||
|
||||
TotalRecords = await query.CountAsync();
|
||||
var emailList = await query
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
// Get all usernames for the emails in the current list
|
||||
var encryptionKeyIds = emailList.Select(x => x.UserEncryptionKeyId).Distinct().ToList();
|
||||
var encryptionKeyUsernames = await dbContext.UserEncryptionKeys
|
||||
.Where(x => encryptionKeyIds.Contains(x.Id))
|
||||
.Join(dbContext.AliasVaultUsers, x => x.UserId, y => y.Id, (x, y) => new { EncryptionKeyId = x.Id, UserId = y.Id, y.UserName })
|
||||
.ToListAsync();
|
||||
|
||||
// Create new list of viewmodels
|
||||
EmailViewModelList = new List<EmailViewModel>();
|
||||
|
||||
foreach (var email in emailList)
|
||||
try
|
||||
{
|
||||
var encryptionKey = encryptionKeyUsernames.FirstOrDefault(x => x.EncryptionKeyId == email.UserEncryptionKeyId);
|
||||
EmailViewModelList.Add(new EmailViewModel { Email = email, UserId = encryptionKey?.UserId ?? string.Empty, UserName = encryptionKey?.UserName ?? string.Empty });
|
||||
}
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
IsLoading = false;
|
||||
IsInitialized = true;
|
||||
StateHasChanged();
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
IQueryable<Email> query = dbContext.Emails;
|
||||
|
||||
query = ApplySearchFilter(query);
|
||||
query = ApplySort(query);
|
||||
|
||||
TotalRecords = await query.CountAsync(cancellationToken);
|
||||
var emailList = await query
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all usernames for the emails in the current list
|
||||
var encryptionKeyIds = emailList.Select(x => x.UserEncryptionKeyId).Distinct().ToList();
|
||||
var encryptionKeyUsernames = await dbContext.UserEncryptionKeys
|
||||
.Where(x => encryptionKeyIds.Contains(x.Id))
|
||||
.Join(dbContext.AliasVaultUsers, x => x.UserId, y => y.Id, (x, y) => new { EncryptionKeyId = x.Id, UserId = y.Id, y.UserName })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// Create new list of viewmodels
|
||||
EmailViewModelList = new List<EmailViewModel>();
|
||||
|
||||
foreach (var email in emailList)
|
||||
{
|
||||
var encryptionKey = encryptionKeyUsernames.FirstOrDefault(x => x.EncryptionKeyId == email.UserEncryptionKeyId);
|
||||
EmailViewModelList.Add(new EmailViewModel { Email = email, UserId = encryptionKey?.UserId ?? string.Empty, UserName = encryptionKey?.UserName ?? string.Empty });
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
IsInitialized = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected when cancellation is requested, do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -254,4 +269,14 @@ else
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_searchCancellationTokenSource?.Cancel();
|
||||
_searchCancellationTokenSource?.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
Description="This page shows an overview of recent auth attempts.">
|
||||
<CustomActions>
|
||||
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
<RefreshButton OnClick="() => RefreshData(CancellationToken.None)" ButtonText="Refresh" />
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
|
||||
@@ -107,6 +107,8 @@ else
|
||||
|
||||
private string _searchTerm = string.Empty;
|
||||
private string _lastSearchTerm = string.Empty;
|
||||
private CancellationTokenSource? _searchCancellationTokenSource;
|
||||
|
||||
private string SearchTerm
|
||||
{
|
||||
get => _searchTerm;
|
||||
@@ -115,7 +117,9 @@ else
|
||||
if (_searchTerm != value)
|
||||
{
|
||||
_searchTerm = value;
|
||||
_ = RefreshData();
|
||||
_searchCancellationTokenSource?.Cancel();
|
||||
_searchCancellationTokenSource = new CancellationTokenSource();
|
||||
_ = RefreshData(_searchCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +133,9 @@ else
|
||||
if (_selectedEventType != value)
|
||||
{
|
||||
_selectedEventType = value;
|
||||
_ = RefreshData();
|
||||
_searchCancellationTokenSource?.Cancel();
|
||||
_searchCancellationTokenSource = new CancellationTokenSource();
|
||||
_ = RefreshData(_searchCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,7 +145,7 @@ else
|
||||
private void ToggleUniqueUsernames()
|
||||
{
|
||||
ShowUniqueUsernames = !ShowUniqueUsernames;
|
||||
_ = RefreshData();
|
||||
_ = RefreshData(CancellationToken.None);
|
||||
}
|
||||
|
||||
private string SortColumn { get; set; } = "Id";
|
||||
@@ -149,7 +155,7 @@ else
|
||||
{
|
||||
SortColumn = sort.column;
|
||||
SortDirection = sort.direction;
|
||||
await RefreshData();
|
||||
await RefreshData(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -161,7 +167,7 @@ else
|
||||
|
||||
Navigation.LocationChanged += OnLocationChanged;
|
||||
ParseQueryAndRefresh();
|
||||
await RefreshData();
|
||||
await RefreshData(CancellationToken.None);
|
||||
}
|
||||
|
||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
@@ -186,7 +192,7 @@ else
|
||||
if (_searchTerm != _lastSearchTerm)
|
||||
{
|
||||
_lastSearchTerm = _searchTerm;
|
||||
_ = RefreshData(); // Fire and forget
|
||||
_ = RefreshData(CancellationToken.None); // Fire and forget
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,96 +200,115 @@ else
|
||||
public void Dispose()
|
||||
{
|
||||
Navigation.LocationChanged -= OnLocationChanged;
|
||||
_searchCancellationTokenSource?.Cancel();
|
||||
_searchCancellationTokenSource?.Dispose();
|
||||
}
|
||||
|
||||
private void HandlePageChanged(int newPage)
|
||||
{
|
||||
CurrentPage = newPage;
|
||||
_ = RefreshData();
|
||||
_ = RefreshData(CancellationToken.None);
|
||||
}
|
||||
|
||||
private async Task RefreshData()
|
||||
private async Task RefreshData(CancellationToken cancellationToken = default)
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
var query = dbContext.AuthLogs.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(SearchTerm))
|
||||
try
|
||||
{
|
||||
// Reset page number back to 1 if the search term has changed.
|
||||
if (SearchTerm != _lastSearchTerm)
|
||||
{
|
||||
CurrentPage = 1;
|
||||
}
|
||||
_lastSearchTerm = SearchTerm;
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
// If the search term starts with "client:", we search for the client header.
|
||||
if (SearchTerm.StartsWith("client:", StringComparison.OrdinalIgnoreCase))
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
var query = dbContext.AuthLogs.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(SearchTerm))
|
||||
{
|
||||
var clientSearchTerm = SearchTerm.Substring(7).ToLower();
|
||||
query = query.Where(x => EF.Functions.Like((x.Client ?? string.Empty).ToLower(), "%" + clientSearchTerm + "%"));
|
||||
// Reset page number back to 1 if the search term has changed.
|
||||
if (SearchTerm != _lastSearchTerm)
|
||||
{
|
||||
CurrentPage = 1;
|
||||
}
|
||||
_lastSearchTerm = SearchTerm;
|
||||
|
||||
// If the search term starts with "client:", we search for the client header.
|
||||
if (SearchTerm.StartsWith("client:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var clientSearchTerm = SearchTerm.Substring(7).ToLower();
|
||||
query = query.Where(x => EF.Functions.Like((x.Client ?? string.Empty).ToLower(), "%" + clientSearchTerm + "%"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var searchTerm = SearchTerm.Trim().ToLower();
|
||||
query = query.Where(x => EF.Functions.Like((x.Username ?? string.Empty).ToLower(), "%" + searchTerm + "%") ||
|
||||
EF.Functions.Like((x.IpAddress ?? string.Empty).ToLower(), "%" + searchTerm + "%"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(SelectedEventType))
|
||||
{
|
||||
var success = Enum.TryParse<AuthEventType>(SelectedEventType, out var eventType);
|
||||
if (success)
|
||||
{
|
||||
query = query.Where(x => x.EventType == eventType);
|
||||
}
|
||||
}
|
||||
|
||||
query = ApplySort(query);
|
||||
|
||||
// Handle unique usernames filtering after getting all results (since GroupBy with OrderBy in Select is complex for EF)
|
||||
if (ShowUniqueUsernames)
|
||||
{
|
||||
// Get all matching records first
|
||||
var allLogs = await query.ToListAsync(cancellationToken);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by username and take the latest entry for each username
|
||||
var uniqueLogs = allLogs
|
||||
.GroupBy(x => x.Username)
|
||||
.Select(g => g.OrderByDescending(x => x.Timestamp).First())
|
||||
.ToList();
|
||||
|
||||
// Apply pagination to the unique results
|
||||
TotalRecords = uniqueLogs.Count;
|
||||
LogList = uniqueLogs
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
var searchTerm = SearchTerm.Trim().ToLower();
|
||||
query = query.Where(x => EF.Functions.Like((x.Username ?? string.Empty).ToLower(), "%" + searchTerm + "%") ||
|
||||
EF.Functions.Like((x.IpAddress ?? string.Empty).ToLower(), "%" + searchTerm + "%"));
|
||||
TotalRecords = await query.CountAsync(cancellationToken);
|
||||
LogList = await query
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(SelectedEventType))
|
||||
// Create user lookup dictionary for the current page
|
||||
var usernames = LogList.Select(x => x.Username).Distinct().Where(x => !string.IsNullOrEmpty(x)).ToList();
|
||||
var users = await dbContext.AliasVaultUsers
|
||||
.Where(u => u.UserName != null && usernames.Contains(u.UserName))
|
||||
.Select(u => new { u.UserName, u.Id })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
UserLookup = users.Where(u => u.UserName != null).ToDictionary(u => u.UserName!, u => u.Id);
|
||||
|
||||
IsLoading = false;
|
||||
IsInitialized = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
var success = Enum.TryParse<AuthEventType>(SelectedEventType, out var eventType);
|
||||
if (success)
|
||||
{
|
||||
query = query.Where(x => x.EventType == eventType);
|
||||
}
|
||||
// Expected when cancellation is requested, do nothing
|
||||
}
|
||||
|
||||
query = ApplySort(query);
|
||||
|
||||
// Handle unique usernames filtering after getting all results (since GroupBy with OrderBy in Select is complex for EF)
|
||||
if (ShowUniqueUsernames)
|
||||
{
|
||||
// Get all matching records first
|
||||
var allLogs = await query.ToListAsync();
|
||||
|
||||
// Group by username and take the latest entry for each username
|
||||
var uniqueLogs = allLogs
|
||||
.GroupBy(x => x.Username)
|
||||
.Select(g => g.OrderByDescending(x => x.Timestamp).First())
|
||||
.ToList();
|
||||
|
||||
// Apply pagination to the unique results
|
||||
TotalRecords = uniqueLogs.Count;
|
||||
LogList = uniqueLogs
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
TotalRecords = await query.CountAsync();
|
||||
LogList = await query
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
// Create user lookup dictionary for the current page
|
||||
var usernames = LogList.Select(x => x.Username).Distinct().Where(x => !string.IsNullOrEmpty(x)).ToList();
|
||||
var users = await dbContext.AliasVaultUsers
|
||||
.Where(u => u.UserName != null && usernames.Contains(u.UserName))
|
||||
.Select(u => new { u.UserName, u.Id })
|
||||
.ToListAsync();
|
||||
|
||||
UserLookup = users.Where(u => u.UserName != null).ToDictionary(u => u.UserName!, u => u.Id);
|
||||
|
||||
IsLoading = false;
|
||||
IsInitialized = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -356,7 +381,7 @@ else
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
dbContext.AuthLogs.RemoveRange(dbContext.AuthLogs);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await RefreshData();
|
||||
await RefreshData(CancellationToken.None);
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
Description="This page shows an overview of recent system logs.">
|
||||
<CustomActions>
|
||||
<DeleteButton OnClick="DeleteLogsWithConfirmation" ButtonText="Delete all logs" />
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
<RefreshButton OnClick="() => RefreshData(CancellationToken.None)" ButtonText="Refresh" />
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
|
||||
@@ -102,6 +102,8 @@ else
|
||||
|
||||
private string _searchTerm = string.Empty;
|
||||
private string _lastSearchTerm = string.Empty;
|
||||
private CancellationTokenSource? _searchCancellationTokenSource;
|
||||
|
||||
private string SearchTerm
|
||||
{
|
||||
get => _searchTerm;
|
||||
@@ -110,7 +112,9 @@ else
|
||||
if (_searchTerm != value)
|
||||
{
|
||||
_searchTerm = value;
|
||||
_ = RefreshData();
|
||||
_searchCancellationTokenSource?.Cancel();
|
||||
_searchCancellationTokenSource = new CancellationTokenSource();
|
||||
_ = RefreshData(_searchCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,7 +128,9 @@ else
|
||||
if (_selectedServiceName != value)
|
||||
{
|
||||
_selectedServiceName = value;
|
||||
_ = RefreshData();
|
||||
_searchCancellationTokenSource?.Cancel();
|
||||
_searchCancellationTokenSource = new CancellationTokenSource();
|
||||
_ = RefreshData(_searchCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +144,7 @@ else
|
||||
{
|
||||
SortColumn = sort.column;
|
||||
SortDirection = sort.direction;
|
||||
await RefreshData();
|
||||
await RefreshData(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -155,66 +161,78 @@ else
|
||||
{
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
ServiceNames = await dbContext.Logs.Select(l => l.Application).Distinct().ToListAsync();
|
||||
await RefreshData();
|
||||
await RefreshData(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePageChanged(int newPage)
|
||||
{
|
||||
CurrentPage = newPage;
|
||||
_ = RefreshData();
|
||||
_ = RefreshData(CancellationToken.None);
|
||||
}
|
||||
|
||||
private async Task RefreshData()
|
||||
private async Task RefreshData(CancellationToken cancellationToken = default)
|
||||
{
|
||||
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)
|
||||
try
|
||||
{
|
||||
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;
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
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(cancellationToken);
|
||||
LogList = await query
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
IsInitialized = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected when cancellation is requested, do nothing
|
||||
}
|
||||
|
||||
TotalRecords = await query.CountAsync();
|
||||
LogList = await query
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
IsLoading = false;
|
||||
IsInitialized = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -272,9 +290,19 @@ else
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
dbContext.Logs.RemoveRange(dbContext.Logs);
|
||||
await dbContext.SaveChangesAsync();
|
||||
await RefreshData();
|
||||
await RefreshData(CancellationToken.None);
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_searchCancellationTokenSource?.Cancel();
|
||||
_searchCancellationTokenSource?.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
Title="@(TotalRecords > 0 ? $"Users ({TotalRecords:N0})" : "Users")"
|
||||
Description="This page shows an overview of all registered users and the associated vaults.">
|
||||
<CustomActions>
|
||||
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
|
||||
<RefreshButton OnClick="() => RefreshData(CancellationToken.None)" ButtonText="Refresh" />
|
||||
</CustomActions>
|
||||
</PageHeader>
|
||||
|
||||
@@ -86,6 +86,7 @@ else
|
||||
private int TotalRecords { get; set; }
|
||||
|
||||
private string _searchTerm = string.Empty;
|
||||
private CancellationTokenSource? _searchCancellationTokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// The last search term.
|
||||
@@ -100,7 +101,9 @@ else
|
||||
if (_searchTerm != value)
|
||||
{
|
||||
_searchTerm = value;
|
||||
_ = RefreshData();
|
||||
_searchCancellationTokenSource?.Cancel();
|
||||
_searchCancellationTokenSource = new CancellationTokenSource();
|
||||
_ = RefreshData(_searchCancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,7 +115,7 @@ else
|
||||
{
|
||||
SortColumn = sort.column;
|
||||
SortDirection = sort.direction;
|
||||
await RefreshData();
|
||||
await RefreshData(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -127,72 +130,84 @@ else
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await RefreshData();
|
||||
await RefreshData(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePageChanged(int newPage)
|
||||
{
|
||||
CurrentPage = newPage;
|
||||
_ = RefreshData();
|
||||
_ = RefreshData(CancellationToken.None);
|
||||
}
|
||||
|
||||
private async Task RefreshData()
|
||||
private async Task RefreshData(CancellationToken cancellationToken = default)
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
|
||||
IQueryable<AliasVaultUser> query = dbContext.AliasVaultUsers;
|
||||
|
||||
query = ApplySearchFilter(query);
|
||||
query = ApplySort(query, dbContext);
|
||||
|
||||
TotalRecords = await query.CountAsync();
|
||||
var users = await query
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.Select(u => new
|
||||
{
|
||||
u.Id,
|
||||
u.UserName,
|
||||
u.CreatedAt,
|
||||
u.TwoFactorEnabled,
|
||||
u.Blocked,
|
||||
Vaults = u.Vaults.Select(v => new
|
||||
{
|
||||
v.FileSize,
|
||||
v.CreatedAt,
|
||||
v.RevisionNumber,
|
||||
CredentialCount = v.CredentialsCount,
|
||||
}),
|
||||
EmailClaims = u.EmailClaims.Select(ec => new
|
||||
{
|
||||
ec.CreatedAt,
|
||||
ec.Address
|
||||
}),
|
||||
ReceivedEmails = u.EmailClaims.SelectMany(ec => dbContext.Emails.Where(e => e.To == ec.Address)).Count(),
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
UserList = users.Select(user => new UserViewModel
|
||||
try
|
||||
{
|
||||
Id = user.Id,
|
||||
UserName = user.UserName?.ToLower() ?? "N/A",
|
||||
TwoFactorEnabled = user.TwoFactorEnabled,
|
||||
Blocked = user.Blocked,
|
||||
CreatedAt = user.CreatedAt,
|
||||
VaultCount = user.Vaults.Count(),
|
||||
CredentialCount = user.Vaults.OrderByDescending(x => x.RevisionNumber).First().CredentialCount,
|
||||
EmailClaimCount = user.EmailClaims.Count(),
|
||||
ReceivedEmailCount = user.ReceivedEmails,
|
||||
VaultStorageInKb = user.Vaults.Sum(x => x.FileSize),
|
||||
LastVaultUpdate = user.Vaults.Any() ? user.Vaults.Max(x => x.CreatedAt) : user.CreatedAt,
|
||||
}).ToList();
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
IsLoading = false;
|
||||
IsInitialized = true;
|
||||
StateHasChanged();
|
||||
await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
IQueryable<AliasVaultUser> query = dbContext.AliasVaultUsers;
|
||||
|
||||
query = ApplySearchFilter(query);
|
||||
query = ApplySort(query, dbContext);
|
||||
|
||||
TotalRecords = await query.CountAsync(cancellationToken);
|
||||
var users = await query
|
||||
.Skip((CurrentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
.Select(u => new
|
||||
{
|
||||
u.Id,
|
||||
u.UserName,
|
||||
u.CreatedAt,
|
||||
u.TwoFactorEnabled,
|
||||
u.Blocked,
|
||||
Vaults = u.Vaults.Select(v => new
|
||||
{
|
||||
v.FileSize,
|
||||
v.CreatedAt,
|
||||
v.RevisionNumber,
|
||||
CredentialCount = v.CredentialsCount,
|
||||
}),
|
||||
EmailClaims = u.EmailClaims.Select(ec => new
|
||||
{
|
||||
ec.CreatedAt,
|
||||
ec.Address
|
||||
}),
|
||||
ReceivedEmails = u.EmailClaims.SelectMany(ec => dbContext.Emails.Where(e => e.To == ec.Address)).Count(),
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UserList = users.Select(user => new UserViewModel
|
||||
{
|
||||
Id = user.Id,
|
||||
UserName = user.UserName?.ToLower() ?? "N/A",
|
||||
TwoFactorEnabled = user.TwoFactorEnabled,
|
||||
Blocked = user.Blocked,
|
||||
CreatedAt = user.CreatedAt,
|
||||
VaultCount = user.Vaults.Count(),
|
||||
CredentialCount = user.Vaults.OrderByDescending(x => x.RevisionNumber).First().CredentialCount,
|
||||
EmailClaimCount = user.EmailClaims.Count(),
|
||||
ReceivedEmailCount = user.ReceivedEmails,
|
||||
VaultStorageInKb = user.Vaults.Sum(x => x.FileSize),
|
||||
LastVaultUpdate = user.Vaults.Any() ? user.Vaults.Max(x => x.CreatedAt) : user.CreatedAt,
|
||||
}).ToList();
|
||||
|
||||
IsLoading = false;
|
||||
IsInitialized = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected when cancellation is requested, do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -279,4 +294,14 @@ else
|
||||
return query;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_searchCancellationTokenSource?.Cancel();
|
||||
_searchCancellationTokenSource?.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user