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() public ActionResult<List<PipelineInfoViewModel>> GetPipelines()
{ {
var pipelines = _pipelineRegistry.GetEnabledPipelines() var pipelines = _pipelineRegistry.GetEnabledPipelines()
.OrderBy(p => p.Name)
.Select(p => new PipelineInfoViewModel .Select(p => new PipelineInfoViewModel
{ {
Name = p.Name, Name = p.Name,
@@ -39,33 +39,25 @@ public class RefreshStatusController : ApiControllerBase
[FromQuery] DateTime maxDT, [FromQuery] DateTime maxDT,
CancellationToken ct) CancellationToken ct)
{ {
// Get raw data updates from repository // Get data updates filtered in SQL by date range (end of day for maxDT)
var updates = await _repository.GetLastDataUpdatesAsync(ct); var updates = await _repository.GetDataUpdatesInRangeAsync(minDT, maxDT.Date.AddDays(1), ct);
// Filter by date range
var filtered = updates
.Where(u => u.StartDt >= minDT && u.StartDt <= maxDT.AddDays(1))
.ToList();
// Group by StartDt (rounded to minute) to aggregate multiple table updates into single rows // 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)) .GroupBy(u => new DateTime(u.StartDt.Year, u.StartDt.Month, u.StartDt.Day, u.StartDt.Hour, u.StartDt.Minute, 0))
.Select(g => new DataUpdateDto .Select(g => new DataUpdateDto
{ {
StartDt = g.Key, StartDt = g.Key,
EndDt = g.Max(u => u.EndDt), EndDt = g.Max(u => u.EndDt),
WasSuccessful = g.All(u => u.WasSuccessful), WasSuccessful = g.All(u => u.WasSuccessful),
BranchRecords = (int)(g.FirstOrDefault(u => u.TableName == "Branch")?.NumberRecords ?? 0), PassedCount = g.Count(u => u.WasSuccessful),
ProfitCenterRecords = (int)(g.FirstOrDefault(u => u.TableName == "ProfitCenter")?.NumberRecords ?? 0), FailedCount = g.Count(u => !u.WasSuccessful),
WorkCenterRecords = (int)(g.FirstOrDefault(u => u.TableName == "WorkCenter")?.NumberRecords ?? 0), Items = g.Select(u => new DataUpdateItemDto
OrgHierarchyRecords = (int)(g.FirstOrDefault(u => u.TableName == "OrgHierarchy")?.NumberRecords ?? 0), {
StatusCodeRecords = (int)(g.FirstOrDefault(u => u.TableName == "StatusCode")?.NumberRecords ?? 0), TableName = u.TableName,
UserRecords = (int)(g.FirstOrDefault(u => u.TableName == "JdeUser")?.NumberRecords ?? 0), WasSuccessful = u.WasSuccessful,
ItemRecords = (int)(g.FirstOrDefault(u => u.TableName == "Item")?.NumberRecords ?? 0), NumberRecords = u.NumberRecords
LotRecords = (int)(g.FirstOrDefault(u => u.TableName == "Lot")?.NumberRecords ?? 0), }).OrderBy(i => i.TableName).ToList()
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)
}) })
.OrderByDescending(d => d.StartDt) .OrderByDescending(d => d.StartDt)
.ToList(); .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. RefreshStatus.razor - Data cache refresh status dashboard.
Shows the status of JDE/CMS data synchronization jobs (hourly, daily, mass). 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" @page "/refresh-status"
@attribute [Authorize] @attribute [Authorize]
@inject IRefreshStatusService RefreshStatusService @inject IRefreshStatusService RefreshStatusService
@inject DialogService DialogService
<PageTitle>Cache Refresh Status - JDE Scoping Tool</PageTitle> <PageTitle>Cache Refresh Status - JDE Scoping Tool</PageTitle>
@@ -38,38 +39,30 @@
else else
{ {
<RadzenDataGrid Data="@_results" TItem="DataUpdateDto" AllowSorting="true" AllowPaging="true" PageSize="20" <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> <Columns>
<RadzenDataGridColumn TItem="DataUpdateDto" Property="StartDT" Title="Start" Width="160px"> <RadzenDataGridColumn TItem="DataUpdateDto" Property="StartDt" Title="Start" Width="180px">
<Template Context="item"> <Template Context="item">
@item.StartDt.ToString("MM/dd/yyyy hh:mm tt") @item.StartDt.ToString("MM/dd/yyyy hh:mm tt")
</Template> </Template>
</RadzenDataGridColumn> </RadzenDataGridColumn>
<RadzenDataGridColumn TItem="DataUpdateDto" Property="EndDT" Title="End" Width="160px"> <RadzenDataGridColumn TItem="DataUpdateDto" Property="EndDt" Title="End" Width="180px">
<Template Context="item"> <Template Context="item">
@(item.EndDt?.ToString("MM/dd/yyyy hh:mm tt") ?? "") @(item.EndDt?.ToString("MM/dd/yyyy hh:mm tt") ?? "")
</Template> </Template>
</RadzenDataGridColumn> </RadzenDataGridColumn>
<RadzenDataGridColumn TItem="DataUpdateDto" Property="BranchRecords" Title="Branch" Width="80px" TextAlign="TextAlign.Center" /> <RadzenDataGridColumn TItem="DataUpdateDto" Property="PassedCount" Title="Passed" Width="100px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="ProfitCenterRecords" Title="Profit Center" Width="100px" TextAlign="TextAlign.Center" /> <RadzenDataGridColumn TItem="DataUpdateDto" Property="FailedCount" Title="Failed" Width="100px" TextAlign="TextAlign.Center" />
<RadzenDataGridColumn TItem="DataUpdateDto" Property="WorkCenterRecords" Title="Work Center" Width="100px" TextAlign="TextAlign.Center" /> <RadzenDataGridColumn TItem="DataUpdateDto" Property="WasSuccessful" Title="Status" Width="120px" 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">
<Template Context="item"> <Template Context="item">
@if (item.WasSuccessful) @if (item.WasSuccessful)
{ {
<RadzenBadge BadgeStyle="BadgeStyle.Success" Text="YES" /> <RadzenBadge BadgeStyle="BadgeStyle.Success" Text="PASS" />
} }
else else
{ {
<RadzenBadge BadgeStyle="BadgeStyle.Danger" Text="NO" /> <RadzenBadge BadgeStyle="BadgeStyle.Danger" Text="FAIL" />
} }
</Template> </Template>
</RadzenDataGridColumn> </RadzenDataGridColumn>
@@ -96,7 +89,6 @@ else
try try
{ {
_results = await RefreshStatusService.GetRefreshStatusAsync(_minDt, _maxDt); _results = await RefreshStatusService.GetRefreshStatusAsync(_minDt, _maxDt);
// Sort by StartDT descending
_results = _results.OrderByDescending(r => r.StartDt).ToList(); _results = _results.OrderByDescending(r => r.StartDt).ToList();
} }
finally finally
@@ -104,4 +96,12 @@ else
_isLoading = false; _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> /// <param name="ct">Cancellation token.</param>
/// <returns>Latest data updates.</returns> /// <returns>Latest data updates.</returns>
Task<List<DataUpdate>> GetLastDataUpdatesAsync(CancellationToken ct = default); 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> /// <summary>
/// DTO for data refresh/sync status display. /// 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> /// </summary>
public class DataUpdateDto public class DataUpdateDto
{ {
/// <summary>The start time of the data update.</summary> /// <summary>The start time of the data update.</summary>
public DateTime StartDt { get; set; } public DateTime StartDt { get; set; }
/// <summary>The end time of the data update.</summary> /// <summary>The end time of the data update.</summary>
public DateTime? EndDt { get; set; } public DateTime? EndDt { get; set; }
/// <summary>The number of branch records updated.</summary> /// <summary>Whether the data update was successful overall.</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>
public bool WasSuccessful { get; set; } 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 cte.NumberRecords
FROM DU_CTE cte FROM DU_CTE cte
WHERE cte.RN = 1"; 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(), commandTimeout: _options.Value.DefaultTimeoutSeconds)).ToList(),
ct); 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 okResult = (OkObjectResult)result.Result!;
var viewModels = okResult.Value.ShouldBeAssignableTo<List<PipelineInfoViewModel>>()!; var viewModels = okResult.Value.ShouldBeAssignableTo<List<PipelineInfoViewModel>>()!;
viewModels.Count.ShouldBe(2); viewModels.Count.ShouldBe(2);
viewModels[0].Name.ShouldBe("WorkOrders"); viewModels[0].Name.ShouldBe("Items");
viewModels[0].SupportedSyncTypes.ShouldContain("mass"); viewModels[0].SupportedSyncTypes.ShouldContain("mass");
viewModels[0].SupportedSyncTypes.ShouldContain("daily"); viewModels[0].SupportedSyncTypes.ShouldContain("daily");
viewModels[0].SupportedSyncTypes.ShouldContain("hourly"); viewModels[1].Name.ShouldBe("WorkOrders");
viewModels[1].Name.ShouldBe("Items"); viewModels[1].SupportedSyncTypes.ShouldContain("mass");
viewModels[1].SupportedSyncTypes.ShouldContain("daily");
viewModels[1].SupportedSyncTypes.ShouldContain("hourly");
} }
[Fact] [Fact]