Redesign refresh status table with summary counts and detail popup, sort pipeline dropdown alphabetically

Replace 11 per-table record columns on /refresh-status with Passed/Failed summary counts and a click-to-expand detail dialog showing per-table results. Add date-range SQL query to push filtering to the database. Sort pipeline dropdown alphabetically on /data-sync/requests.
This commit is contained in:
Joseph Doherty
2026-02-11 19:00:53 -05:00
parent 12cf94a9dc
commit 1b9367dcbb
10 changed files with 141 additions and 66 deletions
@@ -73,6 +73,7 @@ public class ManualSyncController : ApiControllerBase
public ActionResult<List<PipelineInfoViewModel>> GetPipelines()
{
var pipelines = _pipelineRegistry.GetEnabledPipelines()
.OrderBy(p => p.Name)
.Select(p => new PipelineInfoViewModel
{
Name = p.Name,
@@ -39,33 +39,25 @@ public class RefreshStatusController : ApiControllerBase
[FromQuery] DateTime maxDT,
CancellationToken ct)
{
// Get raw data updates from repository
var updates = await _repository.GetLastDataUpdatesAsync(ct);
// Filter by date range
var filtered = updates
.Where(u => u.StartDt >= minDT && u.StartDt <= maxDT.AddDays(1))
.ToList();
// Get data updates filtered in SQL by date range (end of day for maxDT)
var updates = await _repository.GetDataUpdatesInRangeAsync(minDT, maxDT.Date.AddDays(1), ct);
// Group by StartDt (rounded to minute) to aggregate multiple table updates into single rows
var aggregated = filtered
var aggregated = updates
.GroupBy(u => new DateTime(u.StartDt.Year, u.StartDt.Month, u.StartDt.Day, u.StartDt.Hour, u.StartDt.Minute, 0))
.Select(g => new DataUpdateDto
{
StartDt = g.Key,
EndDt = g.Max(u => u.EndDt),
WasSuccessful = g.All(u => u.WasSuccessful),
BranchRecords = (int)(g.FirstOrDefault(u => u.TableName == "Branch")?.NumberRecords ?? 0),
ProfitCenterRecords = (int)(g.FirstOrDefault(u => u.TableName == "ProfitCenter")?.NumberRecords ?? 0),
WorkCenterRecords = (int)(g.FirstOrDefault(u => u.TableName == "WorkCenter")?.NumberRecords ?? 0),
OrgHierarchyRecords = (int)(g.FirstOrDefault(u => u.TableName == "OrgHierarchy")?.NumberRecords ?? 0),
StatusCodeRecords = (int)(g.FirstOrDefault(u => u.TableName == "StatusCode")?.NumberRecords ?? 0),
UserRecords = (int)(g.FirstOrDefault(u => u.TableName == "JdeUser")?.NumberRecords ?? 0),
ItemRecords = (int)(g.FirstOrDefault(u => u.TableName == "Item")?.NumberRecords ?? 0),
LotRecords = (int)(g.FirstOrDefault(u => u.TableName == "Lot")?.NumberRecords ?? 0),
WorkOrderRecords = (int)(g.FirstOrDefault(u => u.TableName.Contains("WorkOrder_"))?.NumberRecords ?? 0),
WorkOrderStepRecords = (int)(g.FirstOrDefault(u => u.TableName.Contains("WorkOrderStep"))?.NumberRecords ?? 0),
WorkOrderComponentRecords = (int)(g.FirstOrDefault(u => u.TableName.Contains("WorkOrderComponent"))?.NumberRecords ?? 0)
PassedCount = g.Count(u => u.WasSuccessful),
FailedCount = g.Count(u => !u.WasSuccessful),
Items = g.Select(u => new DataUpdateItemDto
{
TableName = u.TableName,
WasSuccessful = u.WasSuccessful,
NumberRecords = u.NumberRecords
}).OrderBy(i => i.TableName).ToList()
})
.OrderByDescending(d => d.StartDt)
.ToList();
@@ -0,0 +1,38 @@
@*
RefreshStatusDetailDialog.razor - Per-table detail popup for a sync run.
Displays a grid of individual table sync results (table name, record count, pass/fail).
*@
<RadzenText TextStyle="TextStyle.Subtitle1" class="rz-mb-2">
@SyncRun.StartDt.ToString("MM/dd/yyyy hh:mm tt") — @(SyncRun.EndDt?.ToString("hh:mm tt") ?? "In Progress")
</RadzenText>
<RadzenDataGrid Data="@SyncRun.Items" TItem="DataUpdateItemDto" AllowSorting="true" Style="text-align: center;">
<Columns>
<RadzenDataGridColumn TItem="DataUpdateItemDto" Property="TableName" Title="Table" Width="200px" TextAlign="TextAlign.Left" SortOrder="SortOrder.Ascending" />
<RadzenDataGridColumn TItem="DataUpdateItemDto" Property="NumberRecords" Title="Records" Width="120px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateItemDto" Property="WasSuccessful" Title="Status" Width="100px" TextAlign="TextAlign.Center">
<Template Context="item">
@if (item.WasSuccessful)
{
<RadzenBadge BadgeStyle="BadgeStyle.Success" Text="PASS" />
}
else
{
<RadzenBadge BadgeStyle="BadgeStyle.Danger" Text="FAIL" />
}
</Template>
</RadzenDataGridColumn>
</Columns>
</RadzenDataGrid>
<div class="rz-mt-4" style="text-align: right;">
<RadzenButton Text="Close" ButtonStyle="ButtonStyle.Light" Click="@(() => DialogService.Close())" />
</div>
@code {
[Parameter] public DataUpdateDto SyncRun { get; set; } = default!;
[Inject] private DialogService DialogService { get; set; } = default!;
}
@@ -2,11 +2,12 @@
RefreshStatus.razor - Data cache refresh status dashboard.
Shows the status of JDE/CMS data synchronization jobs (hourly, daily, mass).
Allows filtering by date range and entity name.
Allows filtering by date range. Click a row to see per-table detail.
*@
@page "/refresh-status"
@attribute [Authorize]
@inject IRefreshStatusService RefreshStatusService
@inject DialogService DialogService
<PageTitle>Cache Refresh Status - JDE Scoping Tool</PageTitle>
@@ -38,38 +39,30 @@
else
{
<RadzenDataGrid Data="@_results" TItem="DataUpdateDto" AllowSorting="true" AllowPaging="true" PageSize="20"
PagerHorizontalAlign="HorizontalAlign.Center" AllowColumnResize="true" Style="text-align: center;">
PagerHorizontalAlign="HorizontalAlign.Center" AllowColumnResize="true" Style="text-align: center;"
SelectionMode="DataGridSelectionMode.Single" RowSelect="@OnRowSelect">
<Columns>
<RadzenDataGridColumn TItem="DataUpdateDto" Property="StartDT" Title="Start" Width="160px">
<RadzenDataGridColumn TItem="DataUpdateDto" Property="StartDt" Title="Start" Width="180px">
<Template Context="item">
@item.StartDt.ToString("MM/dd/yyyy hh:mm tt")
</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="DataUpdateDto" Property="EndDT" Title="End" Width="160px">
<RadzenDataGridColumn TItem="DataUpdateDto" Property="EndDt" Title="End" Width="180px">
<Template Context="item">
@(item.EndDt?.ToString("MM/dd/yyyy hh:mm tt") ?? "")
</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="DataUpdateDto" Property="BranchRecords" Title="Branch" Width="80px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="ProfitCenterRecords" Title="Profit Center" Width="100px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="WorkCenterRecords" Title="Work Center" Width="100px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="OrgHierarchyRecords" Title="Org Hierarchy" Width="100px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="StatusCodeRecords" Title="Status Code" Width="100px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="UserRecords" Title="User" Width="80px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="ItemRecords" Title="Item" Width="80px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="LotRecords" Title="Lot" Width="80px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="WorkOrderRecords" Title="Work Order" Width="100px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="WorkOrderStepRecords" Title="WO Step" Width="90px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="WorkOrderComponentRecords" Title="WO Component" Width="110px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="WasSuccessful" Title="Was Successful?" Width="120px" TextAlign="TextAlign.Center">
<RadzenDataGridColumn TItem="DataUpdateDto" Property="PassedCount" Title="Passed" Width="100px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="FailedCount" Title="Failed" Width="100px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="WasSuccessful" Title="Status" Width="120px" TextAlign="TextAlign.Center">
<Template Context="item">
@if (item.WasSuccessful)
{
<RadzenBadge BadgeStyle="BadgeStyle.Success" Text="YES" />
<RadzenBadge BadgeStyle="BadgeStyle.Success" Text="PASS" />
}
else
{
<RadzenBadge BadgeStyle="BadgeStyle.Danger" Text="NO" />
<RadzenBadge BadgeStyle="BadgeStyle.Danger" Text="FAIL" />
}
</Template>
</RadzenDataGridColumn>
@@ -96,7 +89,6 @@ else
try
{
_results = await RefreshStatusService.GetRefreshStatusAsync(_minDt, _maxDt);
// Sort by StartDT descending
_results = _results.OrderByDescending(r => r.StartDt).ToList();
}
finally
@@ -104,4 +96,12 @@ else
_isLoading = false;
}
}
private async Task OnRowSelect(DataUpdateDto row)
{
await DialogService.OpenAsync<JdeScoping.Client.Components.RefreshStatus.RefreshStatusDetailDialog>(
"Sync Run Detail",
new Dictionary<string, object> { { "SyncRun", row } },
new DialogOptions { Width = "600px", Resizable = true, Draggable = true });
}
}
@@ -13,4 +13,13 @@ public partial interface ILotFinderRepository
/// <param name="ct">Cancellation token.</param>
/// <returns>Latest data updates.</returns>
Task<List<DataUpdate>> GetLastDataUpdatesAsync(CancellationToken ct = default);
/// <summary>
/// Gets all data update records within a date range.
/// </summary>
/// <param name="minDt">Start of range (inclusive).</param>
/// <param name="maxDt">End of range (inclusive, end of day).</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Data updates within the range.</returns>
Task<List<DataUpdate>> GetDataUpdatesInRangeAsync(DateTime minDt, DateTime maxDt, CancellationToken ct = default);
}
@@ -2,38 +2,25 @@ namespace JdeScoping.Core.Models.Infrastructure;
/// <summary>
/// DTO for data refresh/sync status display.
/// Aggregates record counts from multiple table updates into a single row.
/// Summarises a single sync run with pass/fail counts and per-table detail.
/// </summary>
public class DataUpdateDto
{
/// <summary>The start time of the data update.</summary>
public DateTime StartDt { get; set; }
/// <summary>The end time of the data update.</summary>
public DateTime? EndDt { get; set; }
/// <summary>The number of branch records updated.</summary>
public int BranchRecords { get; set; }
/// <summary>The number of profit center records updated.</summary>
public int ProfitCenterRecords { get; set; }
/// <summary>The number of work center records updated.</summary>
public int WorkCenterRecords { get; set; }
/// <summary>The number of organizational hierarchy records updated.</summary>
public int OrgHierarchyRecords { get; set; }
/// <summary>The number of status code records updated.</summary>
public int StatusCodeRecords { get; set; }
/// <summary>The number of user records updated.</summary>
public int UserRecords { get; set; }
/// <summary>The number of item records updated.</summary>
public int ItemRecords { get; set; }
/// <summary>The number of lot records updated.</summary>
public int LotRecords { get; set; }
/// <summary>The number of work order records updated.</summary>
public int WorkOrderRecords { get; set; }
/// <summary>The number of work order step records updated.</summary>
public int WorkOrderStepRecords { get; set; }
/// <summary>The number of work order component records updated.</summary>
public int WorkOrderComponentRecords { get; set; }
/// <summary>Whether the data update was successful.</summary>
/// <summary>Whether the data update was successful overall.</summary>
public bool WasSuccessful { get; set; }
/// <summary>Number of table syncs that succeeded.</summary>
public int PassedCount { get; set; }
/// <summary>Number of table syncs that failed.</summary>
public int FailedCount { get; set; }
/// <summary>Per-table detail for this sync run.</summary>
public List<DataUpdateItemDto> Items { get; set; } = [];
}
@@ -0,0 +1,16 @@
namespace JdeScoping.Core.Models.Infrastructure;
/// <summary>
/// Per-table detail within a data sync run.
/// </summary>
public class DataUpdateItemDto
{
/// <summary>The cache table that was synced.</summary>
public string TableName { get; set; } = string.Empty;
/// <summary>Whether this table sync succeeded.</summary>
public bool WasSuccessful { get; set; }
/// <summary>Number of records synced for this table.</summary>
public long NumberRecords { get; set; }
}
@@ -24,4 +24,21 @@ public static partial class LotFinderQueries
cte.NumberRecords
FROM DU_CTE cte
WHERE cte.RN = 1";
/// <summary>
/// Gets all data update records within a date range, ordered by StartDT descending.
/// </summary>
public const string SqlGetDataUpdatesInRange = @"
SELECT du.SourceSystem,
du.SourceData,
du.TableName,
du.StartDT,
du.EndDT,
du.UpdateType,
du.WasSuccessful,
du.NumberRecords
FROM dbo.DataUpdate AS du
WHERE du.StartDT >= @minDt
AND du.StartDT <= @maxDt
ORDER BY du.StartDT DESC";
}
@@ -20,4 +20,17 @@ public partial class LotFinderRepository
commandTimeout: _options.Value.DefaultTimeoutSeconds)).ToList(),
ct);
}
/// <inheritdoc/>
public async Task<List<DataUpdate>> GetDataUpdatesInRangeAsync(DateTime minDt, DateTime maxDt, CancellationToken ct = default)
{
return await ExecuteQueryAsync(
nameof(GetDataUpdatesInRangeAsync),
"SQL_GET_DATA_UPDATES_IN_RANGE",
async connection => (await connection.QueryAsync<DataUpdate>(
LotFinderQueries.SqlGetDataUpdatesInRange,
new { minDt, maxDt },
commandTimeout: _options.Value.DefaultTimeoutSeconds)).ToList(),
ct);
}
}
@@ -111,11 +111,13 @@ public class ManualSyncControllerTests
var okResult = (OkObjectResult)result.Result!;
var viewModels = okResult.Value.ShouldBeAssignableTo<List<PipelineInfoViewModel>>()!;
viewModels.Count.ShouldBe(2);
viewModels[0].Name.ShouldBe("WorkOrders");
viewModels[0].Name.ShouldBe("Items");
viewModels[0].SupportedSyncTypes.ShouldContain("mass");
viewModels[0].SupportedSyncTypes.ShouldContain("daily");
viewModels[0].SupportedSyncTypes.ShouldContain("hourly");
viewModels[1].Name.ShouldBe("Items");
viewModels[1].Name.ShouldBe("WorkOrders");
viewModels[1].SupportedSyncTypes.ShouldContain("mass");
viewModels[1].SupportedSyncTypes.ShouldContain("daily");
viewModels[1].SupportedSyncTypes.ShouldContain("hourly");
}
[Fact]