From 3728d6d6bc3d9e6f30f2516f77af23ef4f88fd33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:25:23 +0100 Subject: [PATCH] [GEN-1863] Users pagination Add paginated data loading for ApplicationUser repository with server-side pagination support. Implement GetPaginatedAsync method that includes filtering options for banned users and returns both user list and total count for efficient data grid rendering. stack-info: PR: https://github.com/Elenpay/NodeGuard/pull/478, branch: Jossec101/stack/11 --- .../Repositories/ApplicationUserRepository.cs | 29 ++++++++ .../Interfaces/IApplicationUserRepository.cs | 5 ++ src/Pages/Users.razor | 73 ++++++++++++++++--- 3 files changed, 95 insertions(+), 12 deletions(-) diff --git a/src/Data/Repositories/ApplicationUserRepository.cs b/src/Data/Repositories/ApplicationUserRepository.cs index 61292073..77215eb4 100644 --- a/src/Data/Repositories/ApplicationUserRepository.cs +++ b/src/Data/Repositories/ApplicationUserRepository.cs @@ -357,6 +357,35 @@ public async Task> GetAll(bool includeBanned = false) return result; } + public async Task<(List users, int totalCount)> GetPaginatedAsync( + int pageNumber, + int pageSize, + bool includeBanned = false) + { + await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync(); + + var query = applicationDbContext.ApplicationUsers + .Include(user => user.Keys) + .Include(user => user.Nodes) + .AsQueryable(); + + if (!includeBanned) + { + query = query.Where(x => x.LockoutEnd <= DateTimeOffset.UtcNow || x.LockoutEnd == null); + } + + query = query.OrderBy(x => x.UserName); + + var totalCount = await query.CountAsync(); + + var users = await query + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + return (users, totalCount); + } + public async Task<(bool, string?)> AddAsync(ApplicationUser type, string? password = null) { await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync(); diff --git a/src/Data/Repositories/Interfaces/IApplicationUserRepository.cs b/src/Data/Repositories/Interfaces/IApplicationUserRepository.cs index 7d75fb06..37a35ac7 100644 --- a/src/Data/Repositories/Interfaces/IApplicationUserRepository.cs +++ b/src/Data/Repositories/Interfaces/IApplicationUserRepository.cs @@ -29,6 +29,11 @@ public interface IApplicationUserRepository Task> GetAll(bool includeBanned = false); + Task<(List users, int totalCount)> GetPaginatedAsync( + int pageNumber, + int pageSize, + bool includeBanned = false); + Task<(bool, string?)> AddAsync(ApplicationUser type, string? password = null); Task<(bool, string?)> AddRangeAsync(List type); diff --git a/src/Pages/Users.razor b/src/Pages/Users.razor index 41dbbd7a..e9f6bf5e 100644 --- a/src/Pages/Users.razor +++ b/src/Pages/Users.razor @@ -1,6 +1,7 @@ @using Humanizer @using Microsoft.AspNetCore.Identity @using System.Security.Claims +@using System.Threading @using Blazorise.Extensions @using Blazorise @using Blazorise.DataGrid @@ -11,7 +12,10 @@ @@ -56,7 +60,7 @@ - + @@ -121,6 +125,8 @@ @attribute [Authorize(Roles = "Superadmin")] @code { private List _users = new(); + private DataGrid? _usersDataGrid; + private int _totalItems; [CascadingParameter] private ApplicationUser? LoggedUser { get; set; } @@ -151,17 +157,26 @@ { if (LoggedUser != null) { - await GetData(); + await LoadNodesList(); } } - private async Task GetData() + private async Task LoadNodesList() { + _nodesList = await NodeRepository.GetAllManagedByNodeGuard(); + } - _users = await ApplicationUserRepository.GetAll(true); + private async Task OnReadData(DataGridReadDataEventArgs e) + { + if (LoggedUser == null) return; - _nodesList = await NodeRepository.GetAllManagedByNodeGuard(); + var (users, totalCount) = await ApplicationUserRepository.GetPaginatedAsync( + e.Page, + e.PageSize, + includeBanned: true); + _users = users; + _totalItems = totalCount; } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -218,7 +233,10 @@ AuditObjectType.User, arg.Item.Id, new { Username = arg.Item.UserName, Roles = string.Join(", ", _selectedRoles) }); - await GetData(); + if (_usersDataGrid != null) + { + await _usersDataGrid.Reload(); + } } else { @@ -258,7 +276,10 @@ else { ToastService.ShowSuccess("Success"); - await GetData(); + if (_usersDataGrid != null) + { + await _usersDataGrid.Reload(); + } } } } @@ -306,7 +327,10 @@ new { Username = arg.Item.UserName }); } - await GetData(); + if (_usersDataGrid != null) + { + await _usersDataGrid.Reload(); + } } private void NewItemDefaultSetter(ApplicationUser obj) @@ -390,7 +414,10 @@ new { Username = contextItem.UserName }); } - await GetData(); + if (_usersDataGrid != null) + { + await _usersDataGrid.Reload(); + } } } @@ -422,8 +449,30 @@ new { Username = contextItem.UserName }); } - await GetData(); - } } + if (_usersDataGrid != null) + { + await _usersDataGrid.Reload(); + } + } + } + + private async Task ValidateUsernameAsync(ValidatorEventArgs obj, CancellationToken cancellationToken, string currentUserId) + { + obj.Status = ValidationStatus.Success; + if (string.IsNullOrWhiteSpace((string)obj.Value)) + { + obj.ErrorText = "The Username cannot be empty"; + obj.Status = ValidationStatus.Error; + return; + } + + var existingUser = await ApplicationUserRepository.GetByUsername(obj.Value.ToString()); + if (existingUser != null && existingUser.Id != currentUserId) + { + obj.ErrorText = "A user with the same username already exists"; + obj.Status = ValidationStatus.Error; + } + } private void ValidateRole(ValidatorEventArgs obj) {