From 07bfb93328223dd4f5b0db63166993d92380add4 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:26:58 +0100 Subject: [PATCH] [GEN-1863] Pagination channels + filters - Add GetPaginatedAsync method to ChannelRepository with comprehensive filtering options (status, nodes, wallet, date range) - Implement IChannelRepository interface with pagination support and detailed XML documentation - Create ChannelManagement component with filtering UI including status, node, and date filters - Add pagination controls and channel display with creation date, status, and capacity information stack-info: PR: https://github.com/Elenpay/NodeGuard/pull/480, branch: Jossec101/stack/13 --- src/Data/Repositories/ChannelRepository.cs | 76 +++++ .../Interfaces/IChannelRepository.cs | 14 + src/Pages/ChannelRequests.razor | 6 + src/Pages/Channels.razor | 321 +++++++++++------- 4 files changed, 295 insertions(+), 122 deletions(-) diff --git a/src/Data/Repositories/ChannelRepository.cs b/src/Data/Repositories/ChannelRepository.cs index e64bda12..3633075d 100644 --- a/src/Data/Repositories/ChannelRepository.cs +++ b/src/Data/Repositories/ChannelRepository.cs @@ -218,6 +218,82 @@ public async Task> GetAllManagedByUserNodes(string loggedUserId) return channels; } + public async Task<(List, int)> GetPaginatedAsync( + string loggedUserId, + int pageNumber, + int pageSize, + int? statusFilter = null, + int? sourceNodeIdFilter = null, + int? destinationNodeIdFilter = null, + int? walletIdFilter = null, + DateTimeOffset? fromDate = null, + DateTimeOffset? toDate = null) + { + if (string.IsNullOrWhiteSpace(loggedUserId)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(loggedUserId)); + + await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync(); + + var query = applicationDbContext.Channels + .Include(channel => channel.ChannelOperationRequests).ThenInclude(request => request.SourceNode).ThenInclude(x => x.Users) + .Include(channel => channel.ChannelOperationRequests).ThenInclude(request => request.DestNode).ThenInclude(x => x.Users) + .Include(channel => channel.ChannelOperationRequests).ThenInclude(request => request.Wallet) + .Include(channel => channel.ChannelOperationRequests).ThenInclude(request => request.ChannelOperationRequestPsbts) + .Include(x => x.SourceNode) + .Include(x => x.DestinationNode) + .Include(x => x.LiquidityRules) + .ThenInclude(x => x.Node) + .Include(x => x.LiquidityRules) + .ThenInclude(x => x.SwapWallet) + .Include(x => x.LiquidityRules) + .ThenInclude(x => x.ReverseSwapWallet) + .AsSplitQuery() + .Where(x => x.SourceNode.Users.Select(user => user.Id).Contains(loggedUserId) || + x.DestinationNode.Users.Select(user => user.Id).Contains(loggedUserId)); + + // Apply filters + if (statusFilter.HasValue) + { + var status = (Channel.ChannelStatus)statusFilter.Value; + query = query.Where(x => x.Status == status); + } + + if (sourceNodeIdFilter.HasValue) + { + query = query.Where(x => x.SourceNodeId == sourceNodeIdFilter.Value); + } + + if (destinationNodeIdFilter.HasValue) + { + query = query.Where(x => x.DestinationNodeId == destinationNodeIdFilter.Value); + } + + if (walletIdFilter.HasValue) + { + query = query.Where(x => x.ChannelOperationRequests.Any(r => r.WalletId == walletIdFilter.Value)); + } + + if (fromDate.HasValue) + { + query = query.Where(x => x.CreationDatetime >= fromDate.Value); + } + + if (toDate.HasValue) + { + query = query.Where(x => x.CreationDatetime <= toDate.Value); + } + + var totalCount = await query.CountAsync(); + + var channels = await query + .OrderByDescending(x => x.CreationDatetime) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + return (channels, totalCount); + } + public async Task<(bool, string?)> MarkAsClosed(Channel channel) { diff --git a/src/Data/Repositories/Interfaces/IChannelRepository.cs b/src/Data/Repositories/Interfaces/IChannelRepository.cs index f99ed872..2bad775e 100644 --- a/src/Data/Repositories/Interfaces/IChannelRepository.cs +++ b/src/Data/Repositories/Interfaces/IChannelRepository.cs @@ -54,4 +54,18 @@ public interface IChannelRepository /// /// Task> GetAllManagedByUserNodes(string loggedUserId); + + /// + /// Retrieves paginated channels managed by user with filters + /// + Task<(List, int)> GetPaginatedAsync( + string loggedUserId, + int pageNumber, + int pageSize, + int? statusFilter = null, + int? sourceNodeIdFilter = null, + int? destinationNodeIdFilter = null, + int? walletIdFilter = null, + DateTimeOffset? fromDate = null, + DateTimeOffset? toDate = null); } \ No newline at end of file diff --git a/src/Pages/ChannelRequests.razor b/src/Pages/ChannelRequests.razor index 09aa05aa..5ef5dcca 100644 --- a/src/Pages/ChannelRequests.razor +++ b/src/Pages/ChannelRequests.razor @@ -695,6 +695,12 @@ { ToastService.ShowError("Bitcoin price in USD could not be retrieved."); } + _availableNodes = await NodeRepository.GetAll(); + _availableUsers = await ApplicationUserRepository.GetAll(); + + _statusOptions.AddRange(Enum.GetValues().Cast()); + _requestTypeOptions.AddRange(Enum.GetValues().Cast()); + await FetchRequests(); await LoadData(); } diff --git a/src/Pages/Channels.razor b/src/Pages/Channels.razor index 42d9cfcf..8fe4e1ec 100644 --- a/src/Pages/Channels.razor +++ b/src/Pages/Channels.razor @@ -2,6 +2,7 @@ @using System.Security.Claims @using Humanizer @using NBitcoin +@using Blazorise.Components @using Channel = NodeGuard.Data.Models.Channel @attribute [Authorize(Roles = "NodeManager")] Active Channels @@ -13,17 +14,117 @@ + + + + Status + + + + + + Source Node + + + + + + Dest Node + + + + + + Wallet + + + + + + Availability + + + + + + From + + + + + + To + + + + + + + + PageSize="25"> @@ -49,16 +150,7 @@ - - - - + @if (context.SourceNode != null) @@ -67,16 +159,7 @@ } - - - - + @if (context.DestinationNode != null) @@ -85,29 +168,12 @@ } - - - - + @context.Status.ToString("G") - - - - + @{ Wallet wallet = null; @@ -148,14 +214,7 @@ } - - - - + @{ var isActive = GetAvailability(context.ChanId); @@ -211,6 +270,7 @@ + @* TODO: Convert this grid to paginated ReadData to avoid loading all records in memory. *@ _availableWallets = new List(); private LiquidityRule? _currentLiquidityRule = new LiquidityRule(); private Validations? _channelManagementValidationsRef; - private string _statusFilter = "Open"; - private int _availabilityFilter = 0; - private int _sourceNodeIdFilter; - private int _destinationNodeIdFilter; + + // Server-side filter values + private string? _statusFilterValue = "Open"; + private int? _sourceNodeIdFilterValue; + private int? _destinationNodeIdFilterValue; + private int? _walletIdFilterValue; + private DateTimeOffset? _fromDateFilter; + private DateTimeOffset? _toDateFilter; + + // Client-side availability filter + private string? _availabilityFilterValue; + + private int _filtersResetKey; + + // Filter options + private List _statusOptions = new List { "Open", "Closed" }; + private List _availabilityOptions = new List { "Active", "Inactive" }; + private List _nodes = new List(); private List _wallets = new List(); - private int _filterWalletId; + private int _totalItems; [CascadingParameter] private ApplicationUser? LoggedUser { get; set; } @@ -446,25 +520,87 @@ } if (LoggedUser != null) { - await FetchData(); + _nodes = await NodeRepository.GetAll(); + _wallets = await WalletRepository.GetAll(); + _availableWallets = await WalletRepository.GetAvailableWallets(true); if (ClaimsPrincipal != null && ClaimsPrincipal.IsInRole(ApplicationUserRole.NodeManager.ToString())) { _isUserNodeManager = true; } } + _channelsBalance = await LightningService.GetChannelsState(); } - private async Task FetchData() + private async Task OnReadData(DataGridReadDataEventArgs e) { - _availableWallets = await WalletRepository.GetAvailableWallets(true); - if (LoggedUser?.Id != null) + if (LoggedUser?.Id == null) return; + + var pageNumber = e.Page; + var pageSize = e.PageSize; + + // Parse status filter + int? statusFilter = null; + if (!string.IsNullOrEmpty(_statusFilterValue)) { - _channels = await ChannelRepository.GetAllManagedByUserNodes(LoggedUser.Id); + statusFilter = _statusFilterValue == "Open" ? (int)Channel.ChannelStatus.Open : (int)Channel.ChannelStatus.Closed; } - _nodes = await NodeRepository.GetAll(); - _wallets = await WalletRepository.GetAll(); - _channelsDataGridRef?.FilterData(); - _channelsBalance = await LightningService.GetChannelsState(); + + var fromDate = _fromDateFilter.HasValue + ? new DateTimeOffset(DateTime.SpecifyKind(_fromDateFilter.Value.Date, DateTimeKind.Local)).ToUniversalTime() + : (DateTimeOffset?)null; + var toDate = _toDateFilter.HasValue + ? new DateTimeOffset(DateTime.SpecifyKind(_toDateFilter.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Local)).ToUniversalTime() + : (DateTimeOffset?)null; + + var (channels, totalCount) = await ChannelRepository.GetPaginatedAsync( + LoggedUser.Id, + pageNumber, + pageSize, + statusFilter, + _sourceNodeIdFilterValue, + _destinationNodeIdFilterValue, + _walletIdFilterValue, + fromDate, + toDate); + + // Apply client-side availability filter + if (!string.IsNullOrEmpty(_availabilityFilterValue)) + { + if (_availabilityFilterValue == "Active") + { + channels = channels.Where(c => GetAvailability(c.ChanId)).ToList(); + } + else if (_availabilityFilterValue == "Inactive") + { + channels = channels.Where(c => !GetAvailability(c.ChanId)).ToList(); + } + } + + _channels = channels; + _totalItems = totalCount; + } + + private async Task OnFiltersChanged() + { + await _channelsDataGridRef.Reload(); + } + + private async Task FetchData() + { + await _channelsDataGridRef.Reload(); + } + + private async Task ClearAllFilters() + { + _statusFilterValue = null; + _sourceNodeIdFilterValue = null; + _destinationNodeIdFilterValue = null; + _walletIdFilterValue = null; + _availabilityFilterValue = null; + _fromDateFilter = null; + _toDateFilter = null; + _filtersResetKey++; + await _channelsDataGridRef.Reload(); } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -849,56 +985,6 @@ } } - private bool OnStatusFilter(object? itemValue, object? searchValue) - { - //If searchValue is null, we set it to the filter initial value in the field - searchValue ??= _statusFilter; - if (searchValue is string statusFilter) - { - return statusFilter == "*" || statusFilter == itemValue?.ToString(); - } - - return true; - } - - private bool OnWalletFilter(object? itemValue, object? searchValue) - { - if (searchValue == null || searchValue is 0) - { - return true; - } - if (searchValue is -1) - { - return itemValue == null; - } - - return itemValue is int itemIntValue && searchValue is int searchIntValue && itemIntValue == searchIntValue; - } - - private bool OnSourceNodeIdFilter(object? itemValue, object? searchValue) - { - //If searchValue is null, we set it to the filter initial value in the field - searchValue ??= _sourceNodeIdFilter; - if (searchValue is int nodeIdFilter) - { - return nodeIdFilter == 0 || nodeIdFilter == itemValue as int?; - } - - return true; - } - - private bool OnDestinationNodeIdFilter(object? itemValue, object? searchValue) - { - //If searchValue is null, we set it to the filter initial value in the field - searchValue ??= _destinationNodeIdFilter; - if (searchValue is int nodeIdFilter) - { - return nodeIdFilter == 0 || nodeIdFilter == itemValue as int?; - } - - return true; - } - private async Task MarkAsClosed(Channel contextItem) { @@ -956,15 +1042,6 @@ StateHasChanged(); } - private bool OnAvailabilityFilter(object? itemvalue, object? searchvalue) - { - if (searchvalue == null || (int)searchvalue == 0) return true; - var isActive = GetAvailability((ulong?)itemvalue); - if ((int)searchvalue == 1 && isActive) return true; - if ((int)searchvalue == -1 && !isActive) return true; - return false; - } - private string GetBalanceTooltipInformation(Channel channel) { var sourcePubKey = StringHelper.TruncateHeadAndTail(channel.SourceNode.PubKey, 5);