refactor(webui): remove pipeline viewer feature

Remove the read-only pipeline viewer from the web UI:
- Delete PipelineViewer.razor page and supporting components
- Delete PipelineController and PipelineMapper from API
- Delete Pipeline DTOs from Core
- Delete PipelineApiClient from Client
- Remove navigation link and DI registrations
- Delete obsolete plan documents

The ConfigManager utility retains pipeline editing capabilities.
This commit is contained in:
Joseph Doherty
2026-01-21 10:14:43 -05:00
parent 94d5a864e0
commit ceb63bfefb
20 changed files with 329 additions and 2776 deletions
@@ -1,116 +0,0 @@
@namespace JdeScoping.Client.Components.Admin
@using JdeScoping.Core.Models.Pipelines
@using JdeScoping.Core.Models.Enums
@using JdeScoping.Client.Helpers
<RadzenCard class="rz-mb-4">
<RadzenRow AlignItems="AlignItems.Center" class="rz-mb-3">
<RadzenColumn>
<RadzenText TextStyle="TextStyle.H5" class="rz-m-0">
@GetScheduleTypeName(ScheduleType) Refresh
</RadzenText>
</RadzenColumn>
<RadzenColumn Size="2" class="rz-text-align-end">
@if (Config?.Enabled == true)
{
<RadzenBadge BadgeStyle="BadgeStyle.Success" Text="Enabled" />
}
else
{
<RadzenBadge BadgeStyle="BadgeStyle.Light" Text="Disabled" />
}
</RadzenColumn>
</RadzenRow>
@if (Config is not null)
{
<RadzenText TextStyle="TextStyle.Subtitle1" class="rz-mb-2">Schedule Settings</RadzenText>
<table class="rz-mb-4" style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: 1px solid #dee2e6;">
<th style="text-align: left; padding: 0.5rem;">Setting</th>
<th style="text-align: left; padding: 0.5rem;">Value</th>
<th style="text-align: left; padding: 0.5rem;">Source</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 0.5rem;">Interval</td>
<td style="padding: 0.5rem;">@FormatInterval(Config.IntervalMinutes)</td>
<td style="padding: 0.5rem;">@(Config.IntervalIsOverride ? "Override" : "Default")</td>
</tr>
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 0.5rem;">Pre-Purge</td>
<td style="padding: 0.5rem;">@(Config.PrePurge ? "Yes" : "No")</td>
<td style="padding: 0.5rem;">@(Config.PrePurgeIsOverride ? "Override" : "Default")</td>
</tr>
<tr style="border-bottom: 1px solid #dee2e6;">
<td style="padding: 0.5rem;">Re-Index</td>
<td style="padding: 0.5rem;">@(Config.ReIndex ? "Yes" : "No")</td>
<td style="padding: 0.5rem;">@(Config.ReIndexIsOverride ? "Override" : "Default")</td>
</tr>
</tbody>
</table>
@if (Config.Parameters?.Count > 0)
{
<RadzenText TextStyle="TextStyle.Subtitle1" class="rz-mb-2">Parameters</RadzenText>
<ul class="rz-mb-4">
@foreach (var param in Config.Parameters)
{
<li><strong>@param.Name</strong>: @(param.Format ?? "default") (source: @param.Source)</li>
}
</ul>
}
@if (!string.IsNullOrWhiteSpace(Config.Query))
{
<RadzenText TextStyle="TextStyle.Subtitle1" class="rz-mb-2">Query</RadzenText>
<pre style="background: #f8f9fa; padding: 1rem; border-radius: 4px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 0.875rem; line-height: 1.5; overflow-x: auto; white-space: pre-wrap; word-break: break-word; max-height: 400px; overflow-y: auto;">@SqlFormatHelper.FormatSql(Config.Query)</pre>
}
@if (Config.PreScripts?.Count > 0)
{
<RadzenText TextStyle="TextStyle.Subtitle1" class="rz-mt-4 rz-mb-2">Pre-Scripts (@Config.PreScripts.Count)</RadzenText>
@for (int i = 0; i < Config.PreScripts.Count; i++)
{
var script = Config.PreScripts[i];
<RadzenText TextStyle="TextStyle.Body2" class="rz-mb-1"><strong>Script @(i + 1):</strong></RadzenText>
<pre style="background: #fff3cd; padding: 0.75rem; border-radius: 4px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 0.8rem; line-height: 1.4; overflow-x: auto; white-space: pre-wrap; word-break: break-word; max-height: 200px; overflow-y: auto; margin-bottom: 0.5rem;">@SqlFormatHelper.FormatSql(script)</pre>
}
}
@if (Config.PostScripts?.Count > 0)
{
<RadzenText TextStyle="TextStyle.Subtitle1" class="rz-mt-4 rz-mb-2">Post-Scripts (@Config.PostScripts.Count)</RadzenText>
@for (int i = 0; i < Config.PostScripts.Count; i++)
{
var script = Config.PostScripts[i];
<RadzenText TextStyle="TextStyle.Body2" class="rz-mb-1"><strong>Script @(i + 1):</strong></RadzenText>
<pre style="background: #d1ecf1; padding: 0.75rem; border-radius: 4px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 0.8rem; line-height: 1.4; overflow-x: auto; white-space: pre-wrap; word-break: break-word; max-height: 200px; overflow-y: auto; margin-bottom: 0.5rem;">@SqlFormatHelper.FormatSql(script)</pre>
}
}
}
</RadzenCard>
@code {
[Parameter] public UpdateTypes ScheduleType { get; set; }
[Parameter] public PipelineScheduleDto? Config { get; set; }
private static string GetScheduleTypeName(UpdateTypes type) => type switch
{
UpdateTypes.Mass => "Mass",
UpdateTypes.Daily => "Daily",
UpdateTypes.Hourly => "Hourly",
_ => type.ToString()
};
private static string FormatInterval(int minutes)
{
if (minutes >= 1440)
return $"{minutes / 1440} day(s) ({minutes} min)";
if (minutes >= 60)
return $"{minutes / 60} hour(s) ({minutes} min)";
return $"{minutes} minutes";
}
}
@@ -1,101 +0,0 @@
@namespace JdeScoping.Client.Components.Admin
@using JdeScoping.Client.Helpers
@inject IJSRuntime JS
@if (Visible)
{
<div class="sql-modal-overlay" @onclick="Close">
<div class="sql-modal-content" @onclick:stopPropagation="true">
<div class="sql-modal-header">
<RadzenText TextStyle="TextStyle.H5" class="rz-m-0">@Title</RadzenText>
<RadzenButton Icon="close" ButtonStyle="ButtonStyle.Light" Size="ButtonSize.Small" Click="Close" />
</div>
<div class="sql-modal-body">
<pre>@FormattedSql</pre>
</div>
<div class="sql-modal-footer">
<RadzenButton Text="Copy to Clipboard" Icon="content_copy" ButtonStyle="ButtonStyle.Secondary" Click="CopyToClipboard" class="rz-mr-2" />
<RadzenButton Text="Close" Click="Close" />
</div>
</div>
</div>
}
<style>
.sql-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.sql-modal-content {
background: white;
border-radius: 8px;
width: 80vw;
max-width: 1200px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
.sql-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid #dee2e6;
}
.sql-modal-body {
flex: 1;
overflow: auto;
padding: 1.5rem;
background: #f8f9fa;
}
.sql-modal-body pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.875rem;
line-height: 1.5;
}
.sql-modal-footer {
display: flex;
justify-content: flex-end;
padding: 1rem 1.5rem;
border-top: 1px solid #dee2e6;
}
</style>
@code {
[Parameter] public bool Visible { get; set; }
[Parameter] public EventCallback<bool> VisibleChanged { get; set; }
[Parameter] public string? Title { get; set; }
[Parameter] public string? Sql { get; set; }
private string FormattedSql => SqlFormatHelper.FormatSql(Sql);
private async Task CopyToClipboard()
{
if (!string.IsNullOrWhiteSpace(Sql))
{
await JS.InvokeVoidAsync("navigator.clipboard.writeText", Sql);
}
}
private async Task Close()
{
await VisibleChanged.InvokeAsync(false);
}
}
@@ -13,7 +13,6 @@
<NavLink class="nav-link" href="/search">New Search</NavLink>
<NavLink class="nav-link" href="/search/queue">Search Queue</NavLink>
<NavLink class="nav-link" href="/refresh-status">Refresh Status</NavLink>
<NavLink class="nav-link" href="/admin/pipeline-viewer">Pipeline Viewer</NavLink>
</nav>
</div>
<div class="navbar-right">
@@ -1,243 +0,0 @@
@*
PipelineViewer.razor - ETL pipeline configuration viewer (admin).
Displays all configured data sync pipelines with their schedules, queries, and mappings.
Read-only view for inspecting pipeline configuration without modifying.
*@
@page "/admin/pipeline-viewer"
@attribute [Authorize]
@using JdeScoping.Core.ApiContracts
@using JdeScoping.Core.Models.Pipelines
@using JdeScoping.Core.Models.Enums
@inject IPipelineApiClient PipelineApi
<PageTitle>Pipeline Configuration Viewer - JDE Scoping Tool</PageTitle>
<RadzenText TextStyle="TextStyle.H4" class="rz-mb-4">ETL Pipeline Configuration Viewer</RadzenText>
<!-- Pipeline Selector -->
<RadzenCard class="rz-mb-4">
<RadzenFormField Text="Select Pipeline" Style="width: 400px;">
<RadzenDropDown @bind-Value="_selectedPipeline" Data="@_pipelineNames" Style="width: 100%;"
Change="@OnPipelineChanged" Placeholder="Select a pipeline..." />
</RadzenFormField>
</RadzenCard>
@if (_isLoading)
{
<LoadingIndicator Message="Loading pipeline data..." />
}
else if (_config is not null)
{
<!-- Status Summary Table -->
<RadzenCard class="rz-mb-4">
<RadzenText TextStyle="TextStyle.H5" class="rz-mb-3">Schedule Status Summary</RadzenText>
<RadzenDataGrid Data="@_statuses" TItem="PipelineScheduleStatusDto">
<Columns>
<RadzenDataGridColumn TItem="PipelineScheduleStatusDto" Property="ScheduleType" Title="Type" Width="100px">
<Template Context="item">@item.ScheduleType.ToString()</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="PipelineScheduleStatusDto" Title="Last Run" Width="160px">
<Template Context="item">@(item.LastRun?.ToString("MM/dd/yyyy hh:mm tt") ?? "Never")</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="PipelineScheduleStatusDto" Title="Success?" Width="80px" TextAlign="TextAlign.Center">
<Template Context="item">
@if (item.LastRun.HasValue)
{
@if (item.LastRunWasSuccessful)
{
<RadzenIcon Icon="check_circle" Style="color: green;" />
}
else
{
<RadzenIcon Icon="cancel" Style="color: red;" />
}
}
</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="PipelineScheduleStatusDto" Title="Next Required" Width="160px">
<Template Context="item">@(item.NextRequiredRun?.ToString("MM/dd/yyyy hh:mm tt") ?? "N/A")</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="PipelineScheduleStatusDto" Title="Status" Width="100px" TextAlign="TextAlign.Center">
<Template Context="item">
@if (item.IsOverdue)
{
<RadzenBadge BadgeStyle="BadgeStyle.Warning" Text="Overdue" />
}
else
{
<RadzenBadge BadgeStyle="BadgeStyle.Success" Text="OK" />
}
</Template>
</RadzenDataGridColumn>
</Columns>
</RadzenDataGrid>
</RadzenCard>
<!-- Execution History Table -->
<RadzenCard class="rz-mb-4">
<RadzenText TextStyle="TextStyle.H5" class="rz-mb-3">Recent Execution History</RadzenText>
<RadzenDataGrid Data="@_executions" TItem="PipelineExecutionDto" AllowSorting="true" PageSize="10" AllowPaging="true">
<Columns>
<RadzenDataGridColumn TItem="PipelineExecutionDto" Property="ScheduleType" Title="Type" Width="100px">
<Template Context="item">@item.ScheduleType.ToString()</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="PipelineExecutionDto" Title="Start" Width="160px">
<Template Context="item">@item.StartTime.ToString("MM/dd/yyyy hh:mm tt")</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="PipelineExecutionDto" Title="End" Width="160px">
<Template Context="item">@(item.EndTime?.ToString("MM/dd/yyyy hh:mm tt") ?? "In Progress")</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="PipelineExecutionDto" Title="Duration" Width="100px">
<Template Context="item">@FormatDuration(item.Duration)</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="PipelineExecutionDto" Property="RecordCount" Title="Records" Width="100px" TextAlign="TextAlign.Right">
<Template Context="item">@item.RecordCount.ToString("N0")</Template>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="PipelineExecutionDto" Title="Result" Width="100px" TextAlign="TextAlign.Center">
<Template Context="item">
@if (item.WasSuccessful)
{
<RadzenBadge BadgeStyle="BadgeStyle.Success" Text="Success" />
}
else
{
<RadzenBadge BadgeStyle="BadgeStyle.Danger" Text="Failed" />
}
</Template>
</RadzenDataGridColumn>
</Columns>
</RadzenDataGrid>
</RadzenCard>
<!-- Common Pipeline Info -->
<RadzenRow Gap="1rem" class="rz-mb-4">
<!-- Source Card -->
<RadzenColumn Size="6">
<RadzenCard Style="height: 100%;">
<RadzenText TextStyle="TextStyle.H6" class="rz-mb-2">Source</RadzenText>
<p><strong>Connection:</strong>
@switch (_config.Source.Connection.ToLower())
{
case "jde":
<RadzenBadge BadgeStyle="BadgeStyle.Info" Text="JDE" />
break;
case "cms":
<RadzenBadge BadgeStyle="BadgeStyle.Success" Text="CMS" />
break;
case "giw":
<RadzenBadge BadgeStyle="BadgeStyle.Warning" Text="GIW" />
break;
default:
<RadzenBadge Text="@_config.Source.Connection" />
break;
}
</p>
</RadzenCard>
</RadzenColumn>
<!-- Destination Card -->
<RadzenColumn Size="6">
<RadzenCard Style="height: 100%;">
<RadzenText TextStyle="TextStyle.H6" class="rz-mb-2">Destination</RadzenText>
<p><strong>Table:</strong> @_config.Destination.Table</p>
<p><strong>Operation:</strong>
<RadzenBadge BadgeStyle="@(_config.Destination.OperationType == "BulkMerge" ? BadgeStyle.Primary : BadgeStyle.Secondary)"
Text="@_config.Destination.OperationType" />
</p>
@if (_config.Destination.MatchColumns?.Count > 0)
{
<p><strong>Match Columns:</strong></p>
<ul>
@foreach (var col in _config.Destination.MatchColumns)
{
<li>@col</li>
}
</ul>
}
@if (_config.Destination.ExcludeFromUpdate?.Count > 0)
{
<p><strong>Exclude:</strong></p>
<ul>
@foreach (var col in _config.Destination.ExcludeFromUpdate)
{
<li>@col</li>
}
</ul>
}
</RadzenCard>
</RadzenColumn>
</RadzenRow>
<!-- Schedule Sections -->
<PipelineScheduleSection ScheduleType="UpdateTypes.Mass" Config="@_config.Schedules.Mass" />
<PipelineScheduleSection ScheduleType="UpdateTypes.Daily" Config="@_config.Schedules.Daily" />
<PipelineScheduleSection ScheduleType="UpdateTypes.Hourly" Config="@_config.Schedules.Hourly" />
}
@code {
private List<string> _pipelineNames = [];
private string? _selectedPipeline;
private bool _isLoading;
private PipelineConfigDto? _config;
private List<PipelineScheduleStatusDto> _statuses = [];
private List<PipelineExecutionDto> _executions = [];
protected override async Task OnInitializedAsync()
{
var result = await PipelineApi.GetPipelineNamesAsync();
if (result.IsSuccess)
{
_pipelineNames = result.Value.PipelineNames;
}
}
private async Task OnPipelineChanged()
{
if (string.IsNullOrWhiteSpace(_selectedPipeline))
{
_config = null;
_statuses = [];
_executions = [];
return;
}
_isLoading = true;
try
{
var configTask = PipelineApi.GetPipelineAsync(_selectedPipeline);
var statusTask = PipelineApi.GetStatusAsync(_selectedPipeline);
var executionsTask = PipelineApi.GetExecutionsAsync(_selectedPipeline);
await Task.WhenAll(configTask, statusTask, executionsTask);
var configResult = configTask.Result;
var statusResult = statusTask.Result;
var executionsResult = executionsTask.Result;
if (configResult.IsSuccess)
_config = configResult.Value;
if (statusResult.IsSuccess)
_statuses = statusResult.Value.Statuses;
if (executionsResult.IsSuccess)
_executions = executionsResult.Value.Executions;
}
finally
{
_isLoading = false;
}
}
private static string FormatDuration(TimeSpan? duration)
{
if (!duration.HasValue) return "-";
var d = duration.Value;
if (d.TotalHours >= 1) return $"{d.Hours}h {d.Minutes}m";
if (d.TotalMinutes >= 1) return $"{d.Minutes}m {d.Seconds}s";
return $"{d.Seconds}s";
}
}
-1
View File
@@ -55,7 +55,6 @@ builder.Services.AddScoped<ISearchApiClient, SearchApiClient>();
builder.Services.AddScoped<ILookupApiClient, LookupApiClient>();
builder.Services.AddScoped<IAuthApiClient, AuthApiClient>();
builder.Services.AddScoped<IFileApiClient, FileApiClient>();
builder.Services.AddScoped<IPipelineApiClient, PipelineApiClient>();
// Search services
builder.Services.AddScoped<ISearchValidationService, SearchValidationService>();
@@ -1,53 +0,0 @@
using JdeScoping.Core.ApiContracts;
using JdeScoping.Core.Models.Pipelines;
using JdeScoping.Core.ApiContracts.Results;
namespace JdeScoping.Client.Services;
/// <summary>
/// HTTP client implementation for pipeline configuration API.
/// </summary>
public class PipelineApiClient : ApiClientBase, IPipelineApiClient
{
/// <summary>
/// Initializes a new instance of the <see cref="PipelineApiClient"/> class.
/// </summary>
/// <param name="httpClient">The HTTP client for API communication.</param>
public PipelineApiClient(HttpClient httpClient) : base(httpClient) { }
/// <summary>
/// Gets the list of available pipeline names from the API.
/// </summary>
/// <param name="ct">Cancellation token for the request.</param>
/// <returns>The API result containing the pipeline list.</returns>
public Task<ApiResult<PipelineListResponse>> GetPipelineNamesAsync(CancellationToken ct = default)
=> GetAsync<PipelineListResponse>(ApiRoutes.Pipelines.Base, ct);
/// <summary>
/// Gets the configuration for a specific pipeline by name.
/// </summary>
/// <param name="name">The pipeline name.</param>
/// <param name="ct">Cancellation token for the request.</param>
/// <returns>The API result containing the pipeline configuration.</returns>
public Task<ApiResult<PipelineConfigDto>> GetPipelineAsync(string name, CancellationToken ct = default)
=> GetAsync<PipelineConfigDto>(ApiRoutes.Pipelines.GetByName(name), ct);
/// <summary>
/// Gets the current execution status of a pipeline.
/// </summary>
/// <param name="name">The pipeline name.</param>
/// <param name="ct">Cancellation token for the request.</param>
/// <returns>The API result containing the pipeline status.</returns>
public Task<ApiResult<PipelineStatusResponse>> GetStatusAsync(string name, CancellationToken ct = default)
=> GetAsync<PipelineStatusResponse>(ApiRoutes.Pipelines.GetStatus(name), ct);
/// <summary>
/// Gets the recent execution history for a pipeline.
/// </summary>
/// <param name="name">The pipeline name.</param>
/// <param name="count">The maximum number of execution records to retrieve.</param>
/// <param name="ct">Cancellation token for the request.</param>
/// <returns>The API result containing the execution history.</returns>
public Task<ApiResult<PipelineExecutionsResponse>> GetExecutionsAsync(string name, int count = 30, CancellationToken ct = default)
=> GetAsync<PipelineExecutionsResponse>(ApiRoutes.Pipelines.GetExecutions(name, count), ct);
}
-2
View File
@@ -13,7 +13,6 @@
@using Radzen.Blazor
@using JdeScoping.Client
@using JdeScoping.Client.Auth
@using JdeScoping.Client.Components.Admin
@using JdeScoping.Client.Components.FilterPanels
@using JdeScoping.Client.Extensions
@using JdeScoping.Client.Components.Search
@@ -25,6 +24,5 @@
@using JdeScoping.Core.ApiContracts
@using JdeScoping.Core.Models.Auth
@using JdeScoping.Core.Models.Infrastructure
@using JdeScoping.Core.Models.Pipelines
@using JdeScoping.Core.ViewModels
@using ClientSearchViewModel = JdeScoping.Client.Models.SearchViewModel