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:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user