feat(ui): add Node column + filter to SiteCalls grid
This commit is contained in:
@@ -58,6 +58,17 @@
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
@* Task 17: free-text Node filter — exact match against the
|
||||
SiteCall.SourceNode column. The Source site dropdown narrows
|
||||
to a site; Node narrows further within that site (or across
|
||||
sites if Source site is "Any"). *@
|
||||
<div class="col-auto">
|
||||
<label class="form-label small mb-1" for="sc-node">Node</label>
|
||||
<input id="sc-node" type="text" class="form-control form-control-sm"
|
||||
style="min-width: 150px;" placeholder="Any"
|
||||
data-test="site-calls-filter-node"
|
||||
@bind="_nodeFilter" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label class="form-label small mb-1" for="sc-from">From</label>
|
||||
<input id="sc-from" type="datetime-local" class="form-control form-control-sm"
|
||||
@@ -125,6 +136,7 @@
|
||||
<tr>
|
||||
<th>Tracked operation</th>
|
||||
<th>Source site</th>
|
||||
<th>Node</th>
|
||||
<th>Channel</th>
|
||||
<th>Target</th>
|
||||
<th>Status</th>
|
||||
@@ -143,6 +155,7 @@
|
||||
title="Double-click for full detail">
|
||||
<td><code class="small" title="@c.TrackedOperationId">@ShortId(c.TrackedOperationId)</code></td>
|
||||
<td><span class="small">@SiteName(c.SourceSite)</span></td>
|
||||
<td><span class="small">@(c.SourceNode ?? "—")</span></td>
|
||||
<td>@c.Channel</td>
|
||||
<td>@c.Target</td>
|
||||
<td>
|
||||
@@ -253,6 +266,9 @@
|
||||
<dt class="col-sm-3">Source site</dt>
|
||||
<dd class="col-sm-9">@SiteName(det.SourceSite)</dd>
|
||||
|
||||
<dt class="col-sm-3">Source node</dt>
|
||||
<dd class="col-sm-9">@(string.IsNullOrEmpty(d.SourceNode) ? "—" : d.SourceNode)</dd>
|
||||
|
||||
<dt class="col-sm-3">Channel</dt>
|
||||
<dd class="col-sm-9">@det.Channel</dd>
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ public partial class SiteCallsReport
|
||||
private string _channelFilter = string.Empty;
|
||||
private string _siteFilter = string.Empty;
|
||||
private string _targetFilter = string.Empty;
|
||||
private string _nodeFilter = string.Empty;
|
||||
private bool _stuckOnly;
|
||||
private DateTime? _fromFilter;
|
||||
private DateTime? _toFilter;
|
||||
@@ -204,7 +205,8 @@ public partial class SiteCallsReport
|
||||
ToUtc: ToUtc(_toFilter),
|
||||
AfterCreatedAtUtc: cursor.AfterCreatedAtUtc,
|
||||
AfterId: cursor.AfterId,
|
||||
PageSize: PageSize);
|
||||
PageSize: PageSize,
|
||||
SourceNodeFilter: NullIfEmpty(_nodeFilter));
|
||||
|
||||
var response = await CommunicationService.QuerySiteCallsAsync(request);
|
||||
if (response.Success)
|
||||
@@ -393,6 +395,7 @@ public partial class SiteCallsReport
|
||||
_channelFilter = string.Empty;
|
||||
_siteFilter = string.Empty;
|
||||
_targetFilter = string.Empty;
|
||||
_nodeFilter = string.Empty;
|
||||
_stuckOnly = false;
|
||||
_fromFilter = null;
|
||||
_toFilter = null;
|
||||
@@ -403,6 +406,7 @@ public partial class SiteCallsReport
|
||||
!string.IsNullOrEmpty(_channelFilter) ||
|
||||
!string.IsNullOrEmpty(_siteFilter) ||
|
||||
!string.IsNullOrEmpty(_targetFilter) ||
|
||||
!string.IsNullOrEmpty(_nodeFilter) ||
|
||||
_stuckOnly ||
|
||||
_fromFilter != null ||
|
||||
_toFilter != null;
|
||||
|
||||
@@ -33,7 +33,8 @@ public sealed record SiteCallQueryRequest(
|
||||
DateTime? ToUtc,
|
||||
DateTime? AfterCreatedAtUtc,
|
||||
Guid? AfterId,
|
||||
int PageSize);
|
||||
int PageSize,
|
||||
string? SourceNodeFilter = null);
|
||||
|
||||
/// <summary>
|
||||
/// A single <c>SiteCalls</c> row summarised for the Site Calls UI grid. Carries
|
||||
@@ -61,7 +62,8 @@ public sealed record SiteCallSummary(
|
||||
DateTime CreatedAtUtc,
|
||||
DateTime UpdatedAtUtc,
|
||||
DateTime? TerminalAtUtc,
|
||||
bool IsStuck);
|
||||
bool IsStuck,
|
||||
string? SourceNode = null);
|
||||
|
||||
/// <summary>
|
||||
/// Central -> Site Calls UI: paginated response for a <see cref="SiteCallQueryRequest"/>.
|
||||
|
||||
@@ -26,6 +26,11 @@ namespace ScadaLink.Commons.Types.Audit;
|
||||
/// keeps the "StuckOnly" filter honest so paging never returns under-filled
|
||||
/// pages with a non-null next cursor.
|
||||
/// </param>
|
||||
/// <param name="SourceNode">
|
||||
/// Restrict to cached calls originating at a specific cluster node (e.g.
|
||||
/// <c>"site-plant-a-node-a"</c>). Exact match; <c>null</c> means "do not
|
||||
/// constrain". Rows with NULL <c>SourceNode</c> are excluded when set.
|
||||
/// </param>
|
||||
public sealed record SiteCallQueryFilter(
|
||||
string? Channel = null,
|
||||
string? SourceSite = null,
|
||||
@@ -33,4 +38,5 @@ public sealed record SiteCallQueryFilter(
|
||||
string? Target = null,
|
||||
DateTime? FromUtc = null,
|
||||
DateTime? ToUtc = null,
|
||||
DateTime? StuckCutoffUtc = null);
|
||||
DateTime? StuckCutoffUtc = null,
|
||||
string? SourceNode = null);
|
||||
|
||||
@@ -204,6 +204,7 @@ SELECT TOP ({paging.PageSize})
|
||||
FROM dbo.SiteCalls
|
||||
WHERE ({filter.Channel} IS NULL OR Channel = {filter.Channel})
|
||||
AND ({filter.SourceSite} IS NULL OR SourceSite = {filter.SourceSite})
|
||||
AND ({filter.SourceNode} IS NULL OR SourceNode = {filter.SourceNode})
|
||||
AND ({filter.Status} IS NULL OR Status = {filter.Status})
|
||||
AND ({filter.Target} IS NULL OR Target = {filter.Target})
|
||||
AND ({fromUtc} IS NULL OR CreatedAtUtc >= {fromUtc})
|
||||
|
||||
@@ -232,7 +232,8 @@ public class SiteCallAuditActor : ReceiveActor
|
||||
// TerminalAtUtc IS NULL AND CreatedAtUtc < cutoff composes with the
|
||||
// keyset cursor, so the page is always honest (full pages, no empty
|
||||
// pages with a non-null next cursor).
|
||||
StuckCutoffUtc: request.StuckOnly ? stuckCutoff : null);
|
||||
StuckCutoffUtc: request.StuckOnly ? stuckCutoff : null,
|
||||
SourceNode: NullIfBlank(request.SourceNodeFilter));
|
||||
|
||||
var pageSize = Math.Clamp(request.PageSize, 1, MaxPageSize);
|
||||
var paging = new SiteCallPaging(
|
||||
@@ -633,7 +634,8 @@ public class SiteCallAuditActor : ReceiveActor
|
||||
CreatedAtUtc: row.CreatedAtUtc,
|
||||
UpdatedAtUtc: row.UpdatedAtUtc,
|
||||
TerminalAtUtc: row.TerminalAtUtc,
|
||||
IsStuck: IsStuck(row, stuckCutoff));
|
||||
IsStuck: IsStuck(row, stuckCutoff),
|
||||
SourceNode: row.SourceNode);
|
||||
}
|
||||
|
||||
private static SiteCallDetail ToDetail(SiteCall row)
|
||||
|
||||
@@ -223,6 +223,43 @@ public class SiteCallAuditActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
|
||||
Assert.Equal(response.SiteCalls[^1].TrackedOperationId, response.NextAfterId);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task SiteCallQueryRequest_FilterBySourceNode_ReturnsMatchingSummaries()
|
||||
{
|
||||
// Task 17: the new Node filter input pushes a string into
|
||||
// SiteCallQueryRequest.SourceNodeFilter — the actor must thread it
|
||||
// onto SiteCallQueryFilter.SourceNode and the response summaries must
|
||||
// mirror the row's SourceNode column.
|
||||
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
|
||||
|
||||
var siteId = NewSiteId();
|
||||
await using var context = CreateContext();
|
||||
var repo = new SiteCallAuditRepository(context);
|
||||
var actor = CreateActor(repo);
|
||||
|
||||
var t0 = new DateTime(2026, 5, 20, 14, 0, 0, DateTimeKind.Utc);
|
||||
await repo.UpsertAsync(NewRow(
|
||||
TrackedOperationId.New(), siteId, status: "Attempted",
|
||||
createdAtUtc: t0, sourceNode: "site-plant-a-node-a"));
|
||||
await repo.UpsertAsync(NewRow(
|
||||
TrackedOperationId.New(), siteId, status: "Delivered",
|
||||
createdAtUtc: t0.AddMinutes(1), terminal: true, sourceNode: "site-plant-a-node-b"));
|
||||
|
||||
actor.Tell(
|
||||
new SiteCallQueryRequest(
|
||||
"corr-node", StatusFilter: null, SourceSiteFilter: siteId, ChannelFilter: null,
|
||||
TargetKeyword: null, StuckOnly: false, FromUtc: null, ToUtc: null,
|
||||
AfterCreatedAtUtc: null, AfterId: null, PageSize: 50,
|
||||
SourceNodeFilter: "site-plant-a-node-a"),
|
||||
TestActor);
|
||||
|
||||
var response = ExpectMsg<SiteCallQueryResponse>(TimeSpan.FromSeconds(10));
|
||||
Assert.True(response.Success);
|
||||
Assert.Single(response.SiteCalls);
|
||||
Assert.Equal("site-plant-a-node-a", response.SiteCalls[0].SourceNode);
|
||||
Assert.Equal("Attempted", response.SiteCalls[0].Status);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task SiteCallQueryRequest_KeysetPaging_AdvancesViaCursor()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user