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:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,5 +54,6 @@ builder.Services.AddScoped<ISearchApiClient, SearchApiClient>();
|
|||||||
builder.Services.AddScoped<ILookupApiClient, LookupApiClient>();
|
builder.Services.AddScoped<ILookupApiClient, LookupApiClient>();
|
||||||
builder.Services.AddScoped<IAuthApiClient, AuthApiClient>();
|
builder.Services.AddScoped<IAuthApiClient, AuthApiClient>();
|
||||||
builder.Services.AddScoped<IFileApiClient, FileApiClient>();
|
builder.Services.AddScoped<IFileApiClient, FileApiClient>();
|
||||||
|
builder.Services.AddScoped<IPipelineApiClient, PipelineApiClient>();
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
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);
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
@using Radzen.Blazor
|
@using Radzen.Blazor
|
||||||
@using JdeScoping.Client
|
@using JdeScoping.Client
|
||||||
@using JdeScoping.Client.Auth
|
@using JdeScoping.Client.Auth
|
||||||
|
@using JdeScoping.Client.Components.Admin
|
||||||
@using JdeScoping.Client.Components.FilterPanels
|
@using JdeScoping.Client.Components.FilterPanels
|
||||||
@using JdeScoping.Client.Extensions
|
@using JdeScoping.Client.Extensions
|
||||||
@using JdeScoping.Client.Components.Shared
|
@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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user