feat(client): add PipelineApiClient and admin components

- Add IPipelineApiClient interface in Core ApiContracts
- Add PipelineApiClient implementation extending ApiClientBase
- Create Components/Admin directory for admin UI components
- Add SqlQueryModal component for displaying SQL queries with copy-to-clipboard
- Add PipelineScheduleSection component for pipeline schedule display
- Register IPipelineApiClient in Program.cs DI container
- Add Admin components namespace to _Imports.razor
This commit is contained in:
Joseph Doherty
2026-01-07 08:14:37 -05:00
parent 676f090fc8
commit 2a15028e00
6 changed files with 264 additions and 0 deletions
@@ -0,0 +1,92 @@
@namespace JdeScoping.Client.Components.Admin
@using JdeScoping.Core.ApiContracts.Pipelines
@using JdeScoping.Core.Models.Enums
<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 (!string.IsNullOrWhiteSpace(QueryPreview))
{
<RadzenText TextStyle="TextStyle.Subtitle1" class="rz-mb-2">Query</RadzenText>
<div style="background: #f5f5f5; padding: 0.75rem; border-radius: 4px; font-family: monospace; font-size: 0.875rem;">
@QueryPreview
</div>
@if (!string.IsNullOrWhiteSpace(FullQuery))
{
<RadzenButton Text="View Full Query" Icon="open_in_new" ButtonStyle="ButtonStyle.Light" Size="ButtonSize.Small"
Click="@(() => OnViewQuery.InvokeAsync(FullQuery))" class="rz-mt-2" />
}
}
}
</RadzenCard>
@code {
[Parameter] public UpdateTypes ScheduleType { get; set; }
[Parameter] public PipelineScheduleDto? Config { get; set; }
[Parameter] public string? QueryPreview { get; set; }
[Parameter] public string? FullQuery { get; set; }
[Parameter] public EventCallback<string> OnViewQuery { 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";
}
}
@@ -0,0 +1,123 @@
@namespace JdeScoping.Client.Components.Admin
@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 => FormatSql(Sql);
private static string FormatSql(string? sql)
{
if (string.IsNullOrWhiteSpace(sql))
return "";
// Basic SQL formatting - add line breaks before major clauses
return sql
.Replace(" SELECT ", "\nSELECT ")
.Replace(" FROM ", "\nFROM ")
.Replace(" WHERE ", "\nWHERE ")
.Replace(" AND ", "\n AND ")
.Replace(" OR ", "\n OR ")
.Replace(" LEFT ", "\nLEFT ")
.Replace(" RIGHT ", "\nRIGHT ")
.Replace(" INNER ", "\nINNER ")
.Replace(" OUTER ", "\nOUTER ")
.Replace(" JOIN ", " JOIN\n ")
.Replace(" ORDER BY ", "\nORDER BY ")
.Replace(" GROUP BY ", "\nGROUP BY ")
.Replace(" HAVING ", "\nHAVING ")
.Trim();
}
private async Task CopyToClipboard()
{
if (!string.IsNullOrWhiteSpace(Sql))
{
await JS.InvokeVoidAsync("navigator.clipboard.writeText", Sql);
}
}
private async Task Close()
{
await VisibleChanged.InvokeAsync(false);
}
}
+1
View File
@@ -54,5 +54,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>();
await builder.Build().RunAsync();
@@ -0,0 +1,25 @@
using JdeScoping.Core.ApiContracts;
using JdeScoping.Core.ApiContracts.Pipelines;
using JdeScoping.Core.ApiContracts.Results;
namespace JdeScoping.Client.Services;
/// <summary>
/// HTTP client implementation for pipeline configuration API.
/// </summary>
public class PipelineApiClient : ApiClientBase, IPipelineApiClient
{
public PipelineApiClient(HttpClient httpClient) : base(httpClient) { }
public Task<ApiResult<PipelineListResponse>> GetPipelineNamesAsync(CancellationToken ct = default)
=> GetAsync<PipelineListResponse>(ApiRoutes.Pipelines.Base, ct);
public Task<ApiResult<PipelineConfigDto>> GetPipelineAsync(string name, CancellationToken ct = default)
=> GetAsync<PipelineConfigDto>(ApiRoutes.Pipelines.GetByName(name), ct);
public Task<ApiResult<PipelineStatusResponse>> GetStatusAsync(string name, CancellationToken ct = default)
=> GetAsync<PipelineStatusResponse>(ApiRoutes.Pipelines.GetStatus(name), ct);
public Task<ApiResult<PipelineExecutionsResponse>> GetExecutionsAsync(string name, int count = 30, CancellationToken ct = default)
=> GetAsync<PipelineExecutionsResponse>(ApiRoutes.Pipelines.GetExecutions(name, count), ct);
}
+1
View File
@@ -13,6 +13,7 @@
@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.Shared
@@ -0,0 +1,22 @@
using JdeScoping.Core.ApiContracts.Pipelines;
using JdeScoping.Core.ApiContracts.Results;
namespace JdeScoping.Core.ApiContracts;
/// <summary>
/// Client contract for pipeline configuration API operations.
/// </summary>
public interface IPipelineApiClient
{
/// <summary>Gets list of all available pipeline names.</summary>
Task<ApiResult<PipelineListResponse>> GetPipelineNamesAsync(CancellationToken ct = default);
/// <summary>Gets configuration for a specific pipeline.</summary>
Task<ApiResult<PipelineConfigDto>> GetPipelineAsync(string name, CancellationToken ct = default);
/// <summary>Gets schedule status for a pipeline.</summary>
Task<ApiResult<PipelineStatusResponse>> GetStatusAsync(string name, CancellationToken ct = default);
/// <summary>Gets recent execution history for a pipeline.</summary>
Task<ApiResult<PipelineExecutionsResponse>> GetExecutionsAsync(string name, int count = 30, CancellationToken ct = default);
}