Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/Data/Repositories/ChannelOperationRequestRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,5 +235,86 @@ public async Task<List<ChannelOperationRequest>> GetPendingRequests()
.Include(x => x.Utxos).AsSplitQuery()
.ToListAsync();
}

public async Task<(List<ChannelOperationRequest> requests, int totalCount)> GetPaginatedAsync(
int pageNumber,
int pageSize,
ChannelOperationRequestStatus? status = null,
OperationRequestType? requestType = null,
int? sourceNodeId = null,
int? destNodeId = null,
int? walletId = null,
string? userId = null,
DateTimeOffset? fromDate = null,
DateTimeOffset? toDate = null,
IEnumerable<int>? excludedIds = null)
{
await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync();

var query = applicationDbContext.ChannelOperationRequests
.Include(request => request.Wallet)
.Include(request => request.SourceNode)
.Include(request => request.DestNode)
.Include(request => request.ChannelOperationRequestPsbts)
.Include(request => request.User)
.Include(x => x.Utxos)
.AsSplitQuery()
.AsQueryable();

if (status.HasValue)
{
query = query.Where(r => r.Status == status.Value);
}

if (requestType.HasValue)
{
query = query.Where(r => r.RequestType == requestType.Value);
}

if (sourceNodeId.HasValue)
{
query = query.Where(r => r.SourceNodeId == sourceNodeId.Value);
}

if (destNodeId.HasValue)
{
query = query.Where(r => r.DestNodeId == destNodeId.Value);
}

if (walletId.HasValue)
{
query = query.Where(r => r.WalletId == walletId.Value);
}

if (!string.IsNullOrEmpty(userId))
{
query = query.Where(r => r.UserId == userId);
}

if (fromDate.HasValue)
{
query = query.Where(r => r.CreationDatetime >= fromDate.Value);
}

if (toDate.HasValue)
{
query = query.Where(r => r.CreationDatetime <= toDate.Value);
}

if (excludedIds != null && excludedIds.Any())
{
query = query.Where(r => !excludedIds.Contains(r.Id));
}

var totalCount = await query.CountAsync();

var requests = await query
.OrderByDescending(r => r.CreationDatetime)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();

return (requests, totalCount);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,17 @@ public interface IChannelOperationRequestRepository : IBitcoinRequestRepository
/// </summary>
/// <returns></returns>
Task<List<ChannelOperationRequest>> GetPendingRequests();

Task<(List<ChannelOperationRequest> requests, int totalCount)> GetPaginatedAsync(
int pageNumber,
int pageSize,
ChannelOperationRequestStatus? status = null,
OperationRequestType? requestType = null,
int? sourceNodeId = null,
int? destNodeId = null,
int? walletId = null,
string? userId = null,
DateTimeOffset? fromDate = null,
DateTimeOffset? toDate = null,
IEnumerable<int>? excludedIds = null);
}
197 changes: 196 additions & 1 deletion src/Pages/ChannelRequests.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
@using Quartz
@using Humanizer
@using NBitcoin
@using Blazorise.Components
@using NodeGuard.Jobs
@using Google.Protobuf
@using NBXplorer.Models
Expand Down Expand Up @@ -287,10 +288,127 @@
</Tooltip>
</h3>
<br/>
<Row Class="mb-3" @key="_filtersResetKey">
<Column ColumnSize="ColumnSize.Is1">
<Field>
<FieldLabel>Status</FieldLabel>
<Autocomplete TItem="ChannelOperationRequestStatus?"
TValue="ChannelOperationRequestStatus?"
Data="@_statusOptions"
TextField="@(item => item?.Humanize() ?? "All")"
ValueField="@(item => item)"
@bind-SelectedValue="@_statusFilter"
@bind-SelectedValue:after="OnFiltersChanged"
Placeholder="All"
FreeTyping="false"
MinLength="0"
Filter="AutocompleteFilter.Contains" />
</Field>
</Column>
<Column ColumnSize="ColumnSize.Is1">
<Field>
<FieldLabel>Type</FieldLabel>
<Autocomplete TItem="OperationRequestType?"
TValue="OperationRequestType?"
Data="@_requestTypeOptions"
TextField="@(item => item?.Humanize() ?? "All")"
ValueField="@(item => item)"
@bind-SelectedValue="@_requestTypeFilter"
@bind-SelectedValue:after="OnFiltersChanged"
Placeholder="All"
FreeTyping="false"
MinLength="0"
Filter="AutocompleteFilter.Contains" />
</Field>
</Column>
<Column ColumnSize="ColumnSize.Is2">
<Field>
<FieldLabel>Source Node</FieldLabel>
<Autocomplete TItem="Node"
TValue="int?"
Data="@_availableNodes"
TextField="@(item => item?.Name ?? "All")"
ValueField="@(item => (int?)item?.Id)"
@bind-SelectedValue="@_sourceNodeFilter"
@bind-SelectedValue:after="OnFiltersChanged"
Placeholder="All"
FreeTyping="false"
MinLength="0"
Filter="AutocompleteFilter.Contains" />
</Field>
</Column>
<Column ColumnSize="ColumnSize.Is2">
<Field>
<FieldLabel>Dest Node</FieldLabel>
<Autocomplete TItem="Node"
TValue="int?"
Data="@_availableNodes"
TextField="@(item => item?.Name ?? "All")"
ValueField="@(item => (int?)item?.Id)"
@bind-SelectedValue="@_destNodeFilter"
@bind-SelectedValue:after="OnFiltersChanged"
Placeholder="All"
FreeTyping="false"
MinLength="0"
Filter="AutocompleteFilter.Contains" />
</Field>
</Column>
<Column ColumnSize="ColumnSize.Is2">
<Field>
<FieldLabel>Wallet</FieldLabel>
<Autocomplete TItem="Wallet"
TValue="int?"
Data="@_allWallets"
TextField="@(item => item?.Name ?? "All")"
ValueField="@(item => (int?)item?.Id)"
@bind-SelectedValue="@_walletFilter"
@bind-SelectedValue:after="OnFiltersChanged"
Placeholder="All"
FreeTyping="false"
MinLength="0"
Filter="AutocompleteFilter.Contains" />
</Field>
</Column>
<Column ColumnSize="ColumnSize.Is1">
<Field>
<FieldLabel>User</FieldLabel>
<Autocomplete TItem="ApplicationUser"
TValue="string?"
Data="@_availableUsers"
TextField="@(item => item?.UserName ?? "All")"
ValueField="@(item => item?.Id)"
@bind-SelectedValue="@_userFilter"
@bind-SelectedValue:after="OnFiltersChanged"
Placeholder="All"
FreeTyping="false"
MinLength="0"
Filter="AutocompleteFilter.Contains" />
</Field>
</Column>
<Column ColumnSize="ColumnSize.Is1">
<Field>
<FieldLabel>From</FieldLabel>
<DatePicker TValue="DateTime?" @bind-Date="@_fromDate" @bind-Date:after="OnFiltersChanged" InputMode="DateInputMode.Date" Placeholder="Start" />
</Field>
</Column>
<Column ColumnSize="ColumnSize.Is1">
<Field>
<FieldLabel>To</FieldLabel>
<DatePicker TValue="DateTime?" @bind-Date="@_toDate" @bind-Date:after="OnFiltersChanged" InputMode="DateInputMode.Date" Placeholder="End" />
</Field>
</Column>
<Column ColumnSize="ColumnSize.Is1" Class="d-flex align-items-end pb-3">
<Button Color="Color.Secondary" Clicked="ClearAllFilters">
<Icon Name="IconName.Times" /> Clear
</Button>
</Column>
</Row>
@* TODO: Convert this grid to paginated ReadData to avoid loading all records in memory. *@
<DataGrid TItem="ChannelOperationRequest"
@ref="@_allRequestsDatagrid"
Data="@_allRequests"
Filterable="true"
ReadData="@OnAllRequestsReadData"
TotalItems="@_totalAllRequests"
ShowPager="true"
ShowPageSizes="true"
Striped="true">
Expand Down Expand Up @@ -414,7 +532,7 @@
TemplatePsbtString="@_templatePSBTString"
SignedPSBT="@_psbt"/>

<CancelOrRejectPopup

Check warning on line 535 in src/Pages/ChannelRequests.razor

View workflow job for this annotation

GitHub Actions / build-and-test

Component 'CancelOrRejectPopup' expects a value for the parameter 'Reason', but a value may not have been provided.
@ref=@_rejectCancelModalRef
Title='@(_selectedStatusActionString + " operation: " + _selectedRequest?.Id)'
Validator="@RejectReasonValidator"
Expand Down Expand Up @@ -455,10 +573,30 @@
@inject IPriceConversionService PriceConversionService
@inject IAuditService AuditService
@inject IHttpContextAccessor HttpContextAccessor
@inject IApplicationUserRepository ApplicationUserRepository

@code {
private List<ChannelOperationRequest>? _channelRequests;
private List<ChannelOperationRequest>? _allRequests;
private int _totalAllRequests;
private List<ApplicationUser> _availableUsers = new();
private List<Node> _availableNodes = new();

// Filter state
private ChannelOperationRequestStatus? _statusFilter;
private OperationRequestType? _requestTypeFilter;
private int? _sourceNodeFilter;
private int? _destNodeFilter;
private int? _walletFilter;
private string? _userFilter;
private int _filtersResetKey;
private DateTime? _fromDate;
private DateTime? _toDate;

// Filter options
private List<ChannelOperationRequestStatus?> _statusOptions = new() { null };
private List<OperationRequestType?> _requestTypeOptions = new() { null };

private ChannelOperationRequest? _selectedRequest;
private ChannelOperationRequestStatus _selectedStatus;
private ChannelOperationRequest _selectedRequestForMarkingAsFailed;
Expand Down Expand Up @@ -1136,8 +1274,65 @@
private async Task RefreshChannelRequestsInformation()
{
await FetchRequests();
if (_allRequestsDatagrid != null)
{
await _allRequestsDatagrid.Reload();
}
StateHasChanged();
}

private async Task OnAllRequestsReadData(DataGridReadDataEventArgs<ChannelOperationRequest> e)
{
if (!e.CancellationToken.IsCancellationRequested)
{
var fromDateOffset = _fromDate.HasValue ? new DateTimeOffset(_fromDate.Value, TimeSpan.Zero) : (DateTimeOffset?)null;
var toDateOffset = _toDate.HasValue ? new DateTimeOffset(_toDate.Value.AddDays(1).AddSeconds(-1), TimeSpan.Zero) : (DateTimeOffset?)null;

var excludedIds = _channelRequests?.Select(r => r.Id) ?? Enumerable.Empty<int>();

var (requests, totalCount) = await ChannelOperationRequestRepository.GetPaginatedAsync(
e.Page,
e.PageSize,
_statusFilter,
_requestTypeFilter,
_sourceNodeFilter,
_destNodeFilter,
_walletFilter,
_userFilter,
fromDateOffset,
toDateOffset,
excludedIds);

_allRequests = requests;
_totalAllRequests = totalCount;
}
}

private async Task OnFiltersChanged()
{
if (_allRequestsDatagrid != null)
{
await _allRequestsDatagrid.Reload();
}
}

private async Task ClearAllFilters()
{
_statusFilter = null;
_requestTypeFilter = null;
_sourceNodeFilter = null;
_destNodeFilter = null;
_walletFilter = null;
_userFilter = null;
_fromDate = null;
_toDate = null;
_filtersResetKey++;

if (_allRequestsDatagrid != null)
{
await _allRequestsDatagrid.Reload();
}
}

private async Task OnChangelessChanged(bool value)
{
Expand Down
Loading