Skip to content

Commit 57fead9

Browse files
committed
[GEN-1863] add pagination support to ChannelOperationRequestRepository + new filtering
Add GetPaginatedAsync method with comprehensive filtering options including status, request type, node IDs, wallet ID, user ID, date range, and excluded IDs. Update interface and implement pagination UI with filters in ChannelRequests page. stack-info: PR: #479, branch: Jossec101/stack/12
1 parent cb6d2b8 commit 57fead9

3 files changed

Lines changed: 290 additions & 1 deletion

File tree

src/Data/Repositories/ChannelOperationRequestRepository.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,5 +235,86 @@ public async Task<List<ChannelOperationRequest>> GetPendingRequests()
235235
.Include(x => x.Utxos).AsSplitQuery()
236236
.ToListAsync();
237237
}
238+
239+
public async Task<(List<ChannelOperationRequest> requests, int totalCount)> GetPaginatedAsync(
240+
int pageNumber,
241+
int pageSize,
242+
ChannelOperationRequestStatus? status = null,
243+
OperationRequestType? requestType = null,
244+
int? sourceNodeId = null,
245+
int? destNodeId = null,
246+
int? walletId = null,
247+
string? userId = null,
248+
DateTimeOffset? fromDate = null,
249+
DateTimeOffset? toDate = null,
250+
IEnumerable<int>? excludedIds = null)
251+
{
252+
await using var applicationDbContext = await _dbContextFactory.CreateDbContextAsync();
253+
254+
var query = applicationDbContext.ChannelOperationRequests
255+
.Include(request => request.Wallet)
256+
.Include(request => request.SourceNode)
257+
.Include(request => request.DestNode)
258+
.Include(request => request.ChannelOperationRequestPsbts)
259+
.Include(request => request.User)
260+
.Include(x => x.Utxos)
261+
.AsSplitQuery()
262+
.AsQueryable();
263+
264+
if (status.HasValue)
265+
{
266+
query = query.Where(r => r.Status == status.Value);
267+
}
268+
269+
if (requestType.HasValue)
270+
{
271+
query = query.Where(r => r.RequestType == requestType.Value);
272+
}
273+
274+
if (sourceNodeId.HasValue)
275+
{
276+
query = query.Where(r => r.SourceNodeId == sourceNodeId.Value);
277+
}
278+
279+
if (destNodeId.HasValue)
280+
{
281+
query = query.Where(r => r.DestNodeId == destNodeId.Value);
282+
}
283+
284+
if (walletId.HasValue)
285+
{
286+
query = query.Where(r => r.WalletId == walletId.Value);
287+
}
288+
289+
if (!string.IsNullOrEmpty(userId))
290+
{
291+
query = query.Where(r => r.UserId == userId);
292+
}
293+
294+
if (fromDate.HasValue)
295+
{
296+
query = query.Where(r => r.CreationDatetime >= fromDate.Value);
297+
}
298+
299+
if (toDate.HasValue)
300+
{
301+
query = query.Where(r => r.CreationDatetime <= toDate.Value);
302+
}
303+
304+
if (excludedIds != null && excludedIds.Any())
305+
{
306+
query = query.Where(r => !excludedIds.Contains(r.Id));
307+
}
308+
309+
var totalCount = await query.CountAsync();
310+
311+
var requests = await query
312+
.OrderByDescending(r => r.CreationDatetime)
313+
.Skip((pageNumber - 1) * pageSize)
314+
.Take(pageSize)
315+
.ToListAsync();
316+
317+
return (requests, totalCount);
318+
}
238319
}
239320
}

src/Data/Repositories/Interfaces/IChannelOperationRequestRepository.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,17 @@ public interface IChannelOperationRequestRepository : IBitcoinRequestRepository
4444
/// </summary>
4545
/// <returns></returns>
4646
Task<List<ChannelOperationRequest>> GetPendingRequests();
47+
48+
Task<(List<ChannelOperationRequest> requests, int totalCount)> GetPaginatedAsync(
49+
int pageNumber,
50+
int pageSize,
51+
ChannelOperationRequestStatus? status = null,
52+
OperationRequestType? requestType = null,
53+
int? sourceNodeId = null,
54+
int? destNodeId = null,
55+
int? walletId = null,
56+
string? userId = null,
57+
DateTimeOffset? fromDate = null,
58+
DateTimeOffset? toDate = null,
59+
IEnumerable<int>? excludedIds = null);
4760
}

src/Pages/ChannelRequests.razor

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
@using Quartz
55
@using Humanizer
66
@using NBitcoin
7+
@using Blazorise.Components
78
@using NodeGuard.Jobs
89
@using Google.Protobuf
910
@using NBXplorer.Models
@@ -287,10 +288,127 @@
287288
</Tooltip>
288289
</h3>
289290
<br/>
291+
<Row Class="mb-3" @key="_filtersResetKey">
292+
<Column ColumnSize="ColumnSize.Is1">
293+
<Field>
294+
<FieldLabel>Status</FieldLabel>
295+
<Autocomplete TItem="ChannelOperationRequestStatus?"
296+
TValue="ChannelOperationRequestStatus?"
297+
Data="@_statusOptions"
298+
TextField="@(item => item?.Humanize() ?? "All")"
299+
ValueField="@(item => item)"
300+
@bind-SelectedValue="@_statusFilter"
301+
@bind-SelectedValue:after="OnFiltersChanged"
302+
Placeholder="All"
303+
FreeTyping="false"
304+
MinLength="0"
305+
Filter="AutocompleteFilter.Contains" />
306+
</Field>
307+
</Column>
308+
<Column ColumnSize="ColumnSize.Is1">
309+
<Field>
310+
<FieldLabel>Type</FieldLabel>
311+
<Autocomplete TItem="OperationRequestType?"
312+
TValue="OperationRequestType?"
313+
Data="@_requestTypeOptions"
314+
TextField="@(item => item?.Humanize() ?? "All")"
315+
ValueField="@(item => item)"
316+
@bind-SelectedValue="@_requestTypeFilter"
317+
@bind-SelectedValue:after="OnFiltersChanged"
318+
Placeholder="All"
319+
FreeTyping="false"
320+
MinLength="0"
321+
Filter="AutocompleteFilter.Contains" />
322+
</Field>
323+
</Column>
324+
<Column ColumnSize="ColumnSize.Is2">
325+
<Field>
326+
<FieldLabel>Source Node</FieldLabel>
327+
<Autocomplete TItem="Node"
328+
TValue="int?"
329+
Data="@_availableNodes"
330+
TextField="@(item => item?.Name ?? "All")"
331+
ValueField="@(item => (int?)item?.Id)"
332+
@bind-SelectedValue="@_sourceNodeFilter"
333+
@bind-SelectedValue:after="OnFiltersChanged"
334+
Placeholder="All"
335+
FreeTyping="false"
336+
MinLength="0"
337+
Filter="AutocompleteFilter.Contains" />
338+
</Field>
339+
</Column>
340+
<Column ColumnSize="ColumnSize.Is2">
341+
<Field>
342+
<FieldLabel>Dest Node</FieldLabel>
343+
<Autocomplete TItem="Node"
344+
TValue="int?"
345+
Data="@_availableNodes"
346+
TextField="@(item => item?.Name ?? "All")"
347+
ValueField="@(item => (int?)item?.Id)"
348+
@bind-SelectedValue="@_destNodeFilter"
349+
@bind-SelectedValue:after="OnFiltersChanged"
350+
Placeholder="All"
351+
FreeTyping="false"
352+
MinLength="0"
353+
Filter="AutocompleteFilter.Contains" />
354+
</Field>
355+
</Column>
356+
<Column ColumnSize="ColumnSize.Is2">
357+
<Field>
358+
<FieldLabel>Wallet</FieldLabel>
359+
<Autocomplete TItem="Wallet"
360+
TValue="int?"
361+
Data="@_allWallets"
362+
TextField="@(item => item?.Name ?? "All")"
363+
ValueField="@(item => (int?)item?.Id)"
364+
@bind-SelectedValue="@_walletFilter"
365+
@bind-SelectedValue:after="OnFiltersChanged"
366+
Placeholder="All"
367+
FreeTyping="false"
368+
MinLength="0"
369+
Filter="AutocompleteFilter.Contains" />
370+
</Field>
371+
</Column>
372+
<Column ColumnSize="ColumnSize.Is1">
373+
<Field>
374+
<FieldLabel>User</FieldLabel>
375+
<Autocomplete TItem="ApplicationUser"
376+
TValue="string?"
377+
Data="@_availableUsers"
378+
TextField="@(item => item?.UserName ?? "All")"
379+
ValueField="@(item => item?.Id)"
380+
@bind-SelectedValue="@_userFilter"
381+
@bind-SelectedValue:after="OnFiltersChanged"
382+
Placeholder="All"
383+
FreeTyping="false"
384+
MinLength="0"
385+
Filter="AutocompleteFilter.Contains" />
386+
</Field>
387+
</Column>
388+
<Column ColumnSize="ColumnSize.Is1">
389+
<Field>
390+
<FieldLabel>From</FieldLabel>
391+
<DatePicker TValue="DateTime?" @bind-Date="@_fromDate" @bind-Date:after="OnFiltersChanged" InputMode="DateInputMode.Date" Placeholder="Start" />
392+
</Field>
393+
</Column>
394+
<Column ColumnSize="ColumnSize.Is1">
395+
<Field>
396+
<FieldLabel>To</FieldLabel>
397+
<DatePicker TValue="DateTime?" @bind-Date="@_toDate" @bind-Date:after="OnFiltersChanged" InputMode="DateInputMode.Date" Placeholder="End" />
398+
</Field>
399+
</Column>
400+
<Column ColumnSize="ColumnSize.Is1" Class="d-flex align-items-end pb-3">
401+
<Button Color="Color.Secondary" Clicked="ClearAllFilters">
402+
<Icon Name="IconName.Times" /> Clear
403+
</Button>
404+
</Column>
405+
</Row>
406+
@* TODO: Convert this grid to paginated ReadData to avoid loading all records in memory. *@
290407
<DataGrid TItem="ChannelOperationRequest"
291408
@ref="@_allRequestsDatagrid"
292409
Data="@_allRequests"
293-
Filterable="true"
410+
ReadData="@OnAllRequestsReadData"
411+
TotalItems="@_totalAllRequests"
294412
ShowPager="true"
295413
ShowPageSizes="true"
296414
Striped="true">
@@ -455,10 +573,30 @@
455573
@inject IPriceConversionService PriceConversionService
456574
@inject IAuditService AuditService
457575
@inject IHttpContextAccessor HttpContextAccessor
576+
@inject IApplicationUserRepository ApplicationUserRepository
458577

459578
@code {
460579
private List<ChannelOperationRequest>? _channelRequests;
461580
private List<ChannelOperationRequest>? _allRequests;
581+
private int _totalAllRequests;
582+
private List<ApplicationUser> _availableUsers = new();
583+
private List<Node> _availableNodes = new();
584+
585+
// Filter state
586+
private ChannelOperationRequestStatus? _statusFilter;
587+
private OperationRequestType? _requestTypeFilter;
588+
private int? _sourceNodeFilter;
589+
private int? _destNodeFilter;
590+
private int? _walletFilter;
591+
private string? _userFilter;
592+
private int _filtersResetKey;
593+
private DateTime? _fromDate;
594+
private DateTime? _toDate;
595+
596+
// Filter options
597+
private List<ChannelOperationRequestStatus?> _statusOptions = new() { null };
598+
private List<OperationRequestType?> _requestTypeOptions = new() { null };
599+
462600
private ChannelOperationRequest? _selectedRequest;
463601
private ChannelOperationRequestStatus _selectedStatus;
464602
private ChannelOperationRequest _selectedRequestForMarkingAsFailed;
@@ -1136,8 +1274,65 @@
11361274
private async Task RefreshChannelRequestsInformation()
11371275
{
11381276
await FetchRequests();
1277+
if (_allRequestsDatagrid != null)
1278+
{
1279+
await _allRequestsDatagrid.Reload();
1280+
}
11391281
StateHasChanged();
11401282
}
1283+
1284+
private async Task OnAllRequestsReadData(DataGridReadDataEventArgs<ChannelOperationRequest> e)
1285+
{
1286+
if (!e.CancellationToken.IsCancellationRequested)
1287+
{
1288+
var fromDateOffset = _fromDate.HasValue ? new DateTimeOffset(_fromDate.Value, TimeSpan.Zero) : (DateTimeOffset?)null;
1289+
var toDateOffset = _toDate.HasValue ? new DateTimeOffset(_toDate.Value.AddDays(1).AddSeconds(-1), TimeSpan.Zero) : (DateTimeOffset?)null;
1290+
1291+
var excludedIds = _channelRequests?.Select(r => r.Id) ?? Enumerable.Empty<int>();
1292+
1293+
var (requests, totalCount) = await ChannelOperationRequestRepository.GetPaginatedAsync(
1294+
e.Page,
1295+
e.PageSize,
1296+
_statusFilter,
1297+
_requestTypeFilter,
1298+
_sourceNodeFilter,
1299+
_destNodeFilter,
1300+
_walletFilter,
1301+
_userFilter,
1302+
fromDateOffset,
1303+
toDateOffset,
1304+
excludedIds);
1305+
1306+
_allRequests = requests;
1307+
_totalAllRequests = totalCount;
1308+
}
1309+
}
1310+
1311+
private async Task OnFiltersChanged()
1312+
{
1313+
if (_allRequestsDatagrid != null)
1314+
{
1315+
await _allRequestsDatagrid.Reload();
1316+
}
1317+
}
1318+
1319+
private async Task ClearAllFilters()
1320+
{
1321+
_statusFilter = null;
1322+
_requestTypeFilter = null;
1323+
_sourceNodeFilter = null;
1324+
_destNodeFilter = null;
1325+
_walletFilter = null;
1326+
_userFilter = null;
1327+
_fromDate = null;
1328+
_toDate = null;
1329+
_filtersResetKey++;
1330+
1331+
if (_allRequestsDatagrid != null)
1332+
{
1333+
await _allRequestsDatagrid.Reload();
1334+
}
1335+
}
11411336

11421337
private async Task OnChangelessChanged(bool value)
11431338
{

0 commit comments

Comments
 (0)