From 29ac56006dba7779e064a2b59274c9eaa78c3f07 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Jan 2026 17:48:33 -0500 Subject: [PATCH] feat: implement ETL pipeline redesign and ConfigManager improvements - Add pipeline registry with JSON-based configuration and hot-reload support - Implement manual sync request feature with API, client UI, and database - Improve ConfigManager: connection string dropdown in pipeline editor, step delete/reorder functionality, and fix JSON parsing for ConnectionStrings --- .../ManualSync/CancelManualSyncRequestDto.cs | 12 + .../ManualSync/CreateManualSyncRequestDto.cs | 17 + .../ManualSync/ManualSyncRequestViewModel.cs | 57 +++ .../ManualSync/PipelineInfoViewModel.cs | 17 + .../Controllers/ManualSyncController.cs | 189 +++++++ .../Controllers/PipelineController.cs | 224 ++++++++ .../DataSync/NewSyncRequestDialog.razor | 47 ++ .../DataSync/NewSyncRequestDialog.razor.cs | 114 +++++ .../JdeScoping.Client/Layout/MainLayout.razor | 1 + .../Pages/DataSync/DataSyncRequests.razor | 78 +++ .../Pages/DataSync/DataSyncRequests.razor.cs | 232 +++++++++ NEW/src/JdeScoping.Client/Program.cs | 2 + .../Services/ManualSyncApiClient.cs | 68 +++ .../Services/PipelineApiClient.cs | 29 ++ .../JdeScoping.Core/ApiContracts/ApiRoutes.cs | 40 ++ .../ApiContracts/IManualSyncApiClient.cs | 42 ++ .../ApiContracts/IPipelineApiClient.cs | 120 +++++ .../ViewModels/CreateManualSyncRequestDto.cs | 17 + .../ViewModels/ManualSyncRequestViewModel.cs | 57 +++ .../ViewModels/PipelineInfoViewModel.cs | 17 + .../DependencyInjection.cs | 3 + .../JdeScoping.DataAccess.csproj | 1 + .../Services/IManualSyncRequestService.cs | 68 +++ .../Services/ManualSyncRequestService.cs | 202 ++++++++ .../Configuration/DestinationElement.cs | 22 + .../Configuration/EtlPipelineConfig.cs | 78 +++ .../Configuration/ParameterElement.cs | 27 + .../Configuration/ScriptElement.cs | 17 + .../Configuration/SourceElement.cs | 27 + .../Configuration/TransformElement.cs | 21 + .../Contracts/ISyncOrchestrator.cs | 10 + .../DependencyInjection.cs | 7 + .../Models/DataUpdateTask.cs | 61 ++- .../Options/DataSyncOptions.cs | 12 + .../Options/WorkProcessorOptions.cs | 8 + .../Services/IPipelineRegistry.cs | 115 +++++ .../Services/IPipelineValidator.cs | 38 ++ .../Services/PipelineRegistry.cs | 263 ++++++++++ .../Services/PipelineRegistryInitializer.cs | 89 ++++ .../Services/PipelineSnapshot.cs | 61 +++ .../Services/PipelineValidator.cs | 168 ++++++ .../Services/ScheduleChecker.cs | 93 ++-- .../Services/SyncOrchestrator.cs | 52 +- NEW/src/JdeScoping.DataSync/WorkProcessor.cs | 181 ++++++- .../060_CreateManualSyncRequestTable.sql | 43 ++ .../JdeScoping.Domain.csproj | 9 + .../Models/ManualSyncRequest.cs | 59 +++ .../Pipelines/pipeline.Branch.json | 24 + .../Pipelines/pipeline.FunctionCode.json | 21 + .../Pipelines/pipeline.Item.json | 24 + .../Pipelines/pipeline.JdeUser.json | 21 + .../Pipelines/pipeline.Lot.json | 24 + .../Pipelines/pipeline.LotUsage_Curr.json | 24 + .../Pipelines/pipeline.MisData_Curr.json | 29 ++ .../Pipelines/pipeline.MisData_Hist.json | 27 + .../Pipelines/pipeline.OrgHierarchy.json | 24 + .../Pipelines/pipeline.ProfitCenter.json | 24 + .../Pipelines/pipeline.RouteMaster.json | 24 + .../Pipelines/pipeline.StatusCode.json | 24 + .../Pipelines/pipeline.WorkCenter.json | 24 + .../pipeline.WorkOrderComponent_Curr.json | 24 + .../Pipelines/pipeline.WorkOrderRouting.json | 24 + .../pipeline.WorkOrderStep_Curr.json | 24 + .../pipeline.WorkOrderTime_Curr.json | 24 + .../Pipelines/pipeline.WorkOrder_Curr.json | 24 + NEW/src/JdeScoping.Host/appsettings.json | 2 + .../Models/ConnectionStringsSection.cs | 4 + .../ConnectionStringsSectionConverter.cs | 293 +++++++++++ .../Forms/PipelineEditorViewModel.cs | 86 +++- .../ViewModels/MainWindowViewModel.cs | 4 +- .../PipelineSteps/SourceStepViewModel.cs | 8 +- .../Views/Editors/SourceEditorView.axaml | 13 +- .../Forms/ConnectionStringsFormView.axaml | 99 ++-- .../Views/Forms/PipelineEditorView.axaml | 49 +- .../Views/MainWindow.axaml | 3 + .../Controllers/ManualSyncControllerTests.cs | 402 +++++++++++++++ .../Controllers/PipelineControllerTests.cs | 289 +++++++++++ .../ConnectionStringsSectionConverterTests.cs | 198 +++++++ .../Services/ManualSyncRequestServiceTests.cs | 382 ++++++++++++++ .../ScheduleCheckerTests.cs | 255 +++------ .../Services/PipelineRegistryTests.cs | 434 ++++++++++++++++ .../Services/PipelineValidatorTests.cs | 483 ++++++++++++++++++ 82 files changed, 6257 insertions(+), 296 deletions(-) create mode 100644 NEW/src/JdeScoping.Api/Contracts/ManualSync/CancelManualSyncRequestDto.cs create mode 100644 NEW/src/JdeScoping.Api/Contracts/ManualSync/CreateManualSyncRequestDto.cs create mode 100644 NEW/src/JdeScoping.Api/Contracts/ManualSync/ManualSyncRequestViewModel.cs create mode 100644 NEW/src/JdeScoping.Api/Contracts/ManualSync/PipelineInfoViewModel.cs create mode 100644 NEW/src/JdeScoping.Api/Controllers/ManualSyncController.cs create mode 100644 NEW/src/JdeScoping.Api/Controllers/PipelineController.cs create mode 100644 NEW/src/JdeScoping.Client/Components/DataSync/NewSyncRequestDialog.razor create mode 100644 NEW/src/JdeScoping.Client/Components/DataSync/NewSyncRequestDialog.razor.cs create mode 100644 NEW/src/JdeScoping.Client/Pages/DataSync/DataSyncRequests.razor create mode 100644 NEW/src/JdeScoping.Client/Pages/DataSync/DataSyncRequests.razor.cs create mode 100644 NEW/src/JdeScoping.Client/Services/ManualSyncApiClient.cs create mode 100644 NEW/src/JdeScoping.Client/Services/PipelineApiClient.cs create mode 100644 NEW/src/JdeScoping.Core/ApiContracts/IManualSyncApiClient.cs create mode 100644 NEW/src/JdeScoping.Core/ApiContracts/IPipelineApiClient.cs create mode 100644 NEW/src/JdeScoping.Core/ViewModels/CreateManualSyncRequestDto.cs create mode 100644 NEW/src/JdeScoping.Core/ViewModels/ManualSyncRequestViewModel.cs create mode 100644 NEW/src/JdeScoping.Core/ViewModels/PipelineInfoViewModel.cs create mode 100644 NEW/src/JdeScoping.DataAccess/Services/IManualSyncRequestService.cs create mode 100644 NEW/src/JdeScoping.DataAccess/Services/ManualSyncRequestService.cs create mode 100644 NEW/src/JdeScoping.DataSync/Configuration/DestinationElement.cs create mode 100644 NEW/src/JdeScoping.DataSync/Configuration/EtlPipelineConfig.cs create mode 100644 NEW/src/JdeScoping.DataSync/Configuration/ParameterElement.cs create mode 100644 NEW/src/JdeScoping.DataSync/Configuration/ScriptElement.cs create mode 100644 NEW/src/JdeScoping.DataSync/Configuration/SourceElement.cs create mode 100644 NEW/src/JdeScoping.DataSync/Configuration/TransformElement.cs create mode 100644 NEW/src/JdeScoping.DataSync/Services/IPipelineRegistry.cs create mode 100644 NEW/src/JdeScoping.DataSync/Services/IPipelineValidator.cs create mode 100644 NEW/src/JdeScoping.DataSync/Services/PipelineRegistry.cs create mode 100644 NEW/src/JdeScoping.DataSync/Services/PipelineRegistryInitializer.cs create mode 100644 NEW/src/JdeScoping.DataSync/Services/PipelineSnapshot.cs create mode 100644 NEW/src/JdeScoping.DataSync/Services/PipelineValidator.cs create mode 100644 NEW/src/JdeScoping.Database/Scripts/060_CreateManualSyncRequestTable.sql create mode 100644 NEW/src/JdeScoping.Domain/JdeScoping.Domain.csproj create mode 100644 NEW/src/JdeScoping.Domain/Models/ManualSyncRequest.cs create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.Branch.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.FunctionCode.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.Item.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.JdeUser.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.Lot.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.LotUsage_Curr.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.MisData_Curr.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.MisData_Hist.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.OrgHierarchy.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.ProfitCenter.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.RouteMaster.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.StatusCode.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.WorkCenter.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.WorkOrderComponent_Curr.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.WorkOrderRouting.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.WorkOrderStep_Curr.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.WorkOrderTime_Curr.json create mode 100644 NEW/src/JdeScoping.Host/Pipelines/pipeline.WorkOrder_Curr.json create mode 100644 NEW/src/Utils/JdeScoping.ConfigManager/Models/ConnectionStringsSectionConverter.cs create mode 100644 NEW/tests/JdeScoping.Api.Tests/Controllers/ManualSyncControllerTests.cs create mode 100644 NEW/tests/JdeScoping.Api.Tests/Controllers/PipelineControllerTests.cs create mode 100644 NEW/tests/JdeScoping.ConfigManager.Tests/Models/ConnectionStringsSectionConverterTests.cs create mode 100644 NEW/tests/JdeScoping.DataAccess.Tests/Services/ManualSyncRequestServiceTests.cs create mode 100644 NEW/tests/JdeScoping.DataSync.Tests/Services/PipelineRegistryTests.cs create mode 100644 NEW/tests/JdeScoping.DataSync.Tests/Services/PipelineValidatorTests.cs diff --git a/NEW/src/JdeScoping.Api/Contracts/ManualSync/CancelManualSyncRequestDto.cs b/NEW/src/JdeScoping.Api/Contracts/ManualSync/CancelManualSyncRequestDto.cs new file mode 100644 index 0000000..d25fdd8 --- /dev/null +++ b/NEW/src/JdeScoping.Api/Contracts/ManualSync/CancelManualSyncRequestDto.cs @@ -0,0 +1,12 @@ +namespace JdeScoping.Api.Contracts.ManualSync; + +/// +/// Request to cancel a manual sync request. +/// +public class CancelManualSyncRequestDto +{ + /// + /// The row version for optimistic concurrency (Base64 encoded). + /// + public string RowVersionBase64 { get; set; } = string.Empty; +} diff --git a/NEW/src/JdeScoping.Api/Contracts/ManualSync/CreateManualSyncRequestDto.cs b/NEW/src/JdeScoping.Api/Contracts/ManualSync/CreateManualSyncRequestDto.cs new file mode 100644 index 0000000..c026ffd --- /dev/null +++ b/NEW/src/JdeScoping.Api/Contracts/ManualSync/CreateManualSyncRequestDto.cs @@ -0,0 +1,17 @@ +namespace JdeScoping.Api.Contracts.ManualSync; + +/// +/// Request to create a new manual sync request. +/// +public class CreateManualSyncRequestDto +{ + /// + /// The name of the pipeline to sync. + /// + public string PipelineName { get; set; } = string.Empty; + + /// + /// The type of sync to perform (mass, daily, hourly). + /// + public string SyncType { get; set; } = string.Empty; +} diff --git a/NEW/src/JdeScoping.Api/Contracts/ManualSync/ManualSyncRequestViewModel.cs b/NEW/src/JdeScoping.Api/Contracts/ManualSync/ManualSyncRequestViewModel.cs new file mode 100644 index 0000000..dad7066 --- /dev/null +++ b/NEW/src/JdeScoping.Api/Contracts/ManualSync/ManualSyncRequestViewModel.cs @@ -0,0 +1,57 @@ +namespace JdeScoping.Api.Contracts.ManualSync; + +/// +/// View model for a manual sync request. +/// +public class ManualSyncRequestViewModel +{ + /// + /// The unique identifier for the sync request. + /// + public int Id { get; set; } + + /// + /// The name of the pipeline being synced. + /// + public string PipelineName { get; set; } = string.Empty; + + /// + /// The type of sync (mass, daily, hourly). + /// + public string SyncType { get; set; } = string.Empty; + + /// + /// The date and time the request was made. + /// + public DateTime RequestDT { get; set; } + + /// + /// The username of the person who requested the sync. + /// + public string RequestedBy { get; set; } = string.Empty; + + /// + /// The date and time the sync completed, if applicable. + /// + public DateTime? CompletedDT { get; set; } + + /// + /// The date and time the sync was cancelled, if applicable. + /// + public DateTime? CancelDT { get; set; } + + /// + /// The username of the person who cancelled the sync, if applicable. + /// + public string? CancelledBy { get; set; } + + /// + /// The current status of the sync request. + /// + public string Status { get; set; } = string.Empty; + + /// + /// The row version for optimistic concurrency (Base64 encoded). + /// + public string RowVersionBase64 { get; set; } = string.Empty; +} diff --git a/NEW/src/JdeScoping.Api/Contracts/ManualSync/PipelineInfoViewModel.cs b/NEW/src/JdeScoping.Api/Contracts/ManualSync/PipelineInfoViewModel.cs new file mode 100644 index 0000000..3ec1047 --- /dev/null +++ b/NEW/src/JdeScoping.Api/Contracts/ManualSync/PipelineInfoViewModel.cs @@ -0,0 +1,17 @@ +namespace JdeScoping.Api.Contracts.ManualSync; + +/// +/// View model for pipeline information. +/// +public class PipelineInfoViewModel +{ + /// + /// The name of the pipeline. + /// + public string Name { get; set; } = string.Empty; + + /// + /// The sync types supported by this pipeline. + /// + public List SupportedSyncTypes { get; set; } = new(); +} diff --git a/NEW/src/JdeScoping.Api/Controllers/ManualSyncController.cs b/NEW/src/JdeScoping.Api/Controllers/ManualSyncController.cs new file mode 100644 index 0000000..82a59c7 --- /dev/null +++ b/NEW/src/JdeScoping.Api/Controllers/ManualSyncController.cs @@ -0,0 +1,189 @@ +using JdeScoping.Api.Contracts.ManualSync; +using JdeScoping.DataAccess.Services; +using JdeScoping.DataSync.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace JdeScoping.Api.Controllers; + +/// +/// API endpoints for manual data sync request management. +/// +[Route("api/manual-sync")] +[ApiController] +[Authorize] +public class ManualSyncController : ApiControllerBase +{ + private readonly IManualSyncRequestService _manualSyncRequestService; + private readonly IPipelineRegistry _pipelineRegistry; + + /// + /// Initializes a new instance of the class. + /// + /// The service for managing manual sync requests. + /// The pipeline registry for querying pipeline information. + public ManualSyncController( + IManualSyncRequestService manualSyncRequestService, + IPipelineRegistry pipelineRegistry) + { + _manualSyncRequestService = manualSyncRequestService; + _pipelineRegistry = pipelineRegistry; + } + + /// + /// Gets all manual sync requests, optionally filtered to pending only. + /// + /// If true, returns only pending requests. Default is false. + /// Cancellation token. + /// A list of manual sync request view models. + [HttpGet] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task>> GetRequests( + [FromQuery] bool pendingOnly = false, + CancellationToken ct = default) + { + var requests = await _manualSyncRequestService.GetRequestsAsync(pendingOnly, ct); + + var viewModels = requests.Select(r => new ManualSyncRequestViewModel + { + Id = r.Id, + PipelineName = r.PipelineName, + SyncType = r.SyncType, + RequestDT = r.RequestDT, + RequestedBy = r.RequestedBy, + CompletedDT = r.CompletedDT, + CancelDT = r.CancelDT, + CancelledBy = r.CancelledBy, + Status = r.Status, + RowVersionBase64 = Convert.ToBase64String(r.RowVersion) + }).ToList(); + + return Ok(viewModels); + } + + /// + /// Gets all available pipelines with their supported sync types. + /// + /// A list of pipeline information view models. + [HttpGet("pipelines")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public ActionResult> GetPipelines() + { + var pipelines = _pipelineRegistry.GetEnabledPipelines() + .Select(p => new PipelineInfoViewModel + { + Name = p.Name, + SupportedSyncTypes = GetSupportedSyncTypes(p) + }) + .ToList(); + + return Ok(pipelines); + } + + /// + /// Creates a new manual sync request. + /// + /// The create request containing pipeline name and sync type. + /// Cancellation token. + /// The created manual sync request view model. + [HttpPost] + [ProducesResponseType(typeof(ManualSyncRequestViewModel), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public async Task> CreateRequest( + [FromBody] CreateManualSyncRequestDto dto, + CancellationToken ct = default) + { + var username = User.Identity?.Name; + if (string.IsNullOrEmpty(username)) + { + return Unauthorized(); + } + + // Validate pipeline and sync type combination using the registry + if (!_pipelineRegistry.IsValidPipelineAndSyncType(dto.PipelineName, dto.SyncType)) + { + return BadRequest($"Invalid pipeline/sync type combination. Pipeline '{dto.PipelineName}' does not support sync type '{dto.SyncType}'."); + } + + var request = await _manualSyncRequestService.CreateRequestAsync( + dto.PipelineName, + dto.SyncType, + username, + ct); + + var viewModel = new ManualSyncRequestViewModel + { + Id = request.Id, + PipelineName = request.PipelineName, + SyncType = request.SyncType, + RequestDT = request.RequestDT, + RequestedBy = request.RequestedBy, + CompletedDT = request.CompletedDT, + CancelDT = request.CancelDT, + CancelledBy = request.CancelledBy, + Status = request.Status, + RowVersionBase64 = Convert.ToBase64String(request.RowVersion) + }; + + return CreatedAtAction(nameof(GetRequests), viewModel); + } + + /// + /// Cancels a pending manual sync request. + /// + /// The ID of the request to cancel. + /// The cancel request containing the row version for concurrency. + /// Cancellation token. + /// A success message if cancelled, or a conflict error if already processed. + [HttpPost("{id:int}/cancel")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task CancelRequest( + int id, + [FromBody] CancelManualSyncRequestDto dto, + CancellationToken ct = default) + { + var username = User.Identity?.Name; + if (string.IsNullOrEmpty(username)) + { + return Unauthorized(); + } + + byte[] rowVersion; + try + { + rowVersion = Convert.FromBase64String(dto.RowVersionBase64); + } + catch (FormatException) + { + return BadRequest("Invalid RowVersionBase64 format."); + } + + var cancelled = await _manualSyncRequestService.CancelRequestAsync( + id, + username, + rowVersion, + ct); + + if (!cancelled) + { + return Conflict(new { message = "The request has already been completed or cancelled, or the data has been modified by another user." }); + } + + return Ok(new { message = "Request cancelled successfully." }); + } + + private static List GetSupportedSyncTypes(DataSync.Configuration.EtlPipelineConfig pipeline) + { + var types = new List(); + if (pipeline.SupportsMassSync) types.Add("mass"); + if (pipeline.SupportsDailySync) types.Add("daily"); + if (pipeline.SupportsHourlySync) types.Add("hourly"); + return types; + } +} diff --git a/NEW/src/JdeScoping.Api/Controllers/PipelineController.cs b/NEW/src/JdeScoping.Api/Controllers/PipelineController.cs new file mode 100644 index 0000000..8da9d7f --- /dev/null +++ b/NEW/src/JdeScoping.Api/Controllers/PipelineController.cs @@ -0,0 +1,224 @@ +using JdeScoping.Api.Contracts.ManualSync; +using JdeScoping.DataSync.Configuration; +using JdeScoping.DataSync.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace JdeScoping.Api.Controllers; + +/// +/// API endpoints for pipeline management and hot reload. +/// +[Route("api/pipelines")] +[ApiController] +[Authorize] +public class PipelineController : ApiControllerBase +{ + private readonly IPipelineRegistry _pipelineRegistry; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The pipeline registry. + /// The logger. + public PipelineController( + IPipelineRegistry pipelineRegistry, + ILogger logger) + { + _pipelineRegistry = pipelineRegistry; + _logger = logger; + } + + /// + /// Gets all enabled pipelines with their supported sync types. + /// + /// A list of pipeline information view models. + [HttpGet] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public ActionResult> GetPipelines() + { + var pipelines = _pipelineRegistry.GetEnabledPipelines() + .Select(p => new PipelineInfoViewModel + { + Name = p.Name, + SupportedSyncTypes = GetSupportedSyncTypes(p) + }) + .ToList(); + + return Ok(pipelines); + } + + /// + /// Gets registry metadata including version and last load time. + /// + /// Registry metadata. + [HttpGet("status")] + [ProducesResponseType(typeof(PipelineRegistryStatusViewModel), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + public ActionResult GetStatus() + { + var allPipelines = _pipelineRegistry.GetAllPipelines(); + var enabledPipelines = _pipelineRegistry.GetEnabledPipelines(); + + return Ok(new PipelineRegistryStatusViewModel + { + Version = _pipelineRegistry.Version, + LastLoadedAt = _pipelineRegistry.LastLoadedAt, + TotalPipelines = allPipelines.Count, + EnabledPipelines = enabledPipelines.Count + }); + } + + /// + /// Reloads all pipeline definitions from disk. + /// Requires Admin role. + /// + /// Cancellation token. + /// The reload result. + [HttpPost("reload")] + [Authorize(Roles = "Admin")] + [ProducesResponseType(typeof(PipelineReloadResultViewModel), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> ReloadPipelines(CancellationToken ct = default) + { + var username = User.Identity?.Name ?? "unknown"; + + _logger.LogInformation("Pipeline reload requested by {User}", username); + + try + { + var result = await _pipelineRegistry.ReloadAsync(ct); + + _logger.LogInformation( + "Pipeline reload completed: Success={Success}, Loaded={Loaded}, Skipped={Skipped}, Version={Version}", + result.Success, + result.PipelinesLoaded, + result.PipelinesSkipped, + result.NewVersion); + + return Ok(new PipelineReloadResultViewModel + { + Success = result.Success, + PipelinesLoaded = result.PipelinesLoaded, + PipelinesSkipped = result.PipelinesSkipped, + PreviousVersion = result.PreviousVersion, + NewVersion = result.NewVersion, + Errors = result.Errors.Select(e => new PipelineLoadErrorViewModel + { + FileName = e.FileName, + PipelineName = e.PipelineName, + ErrorType = e.ErrorType, + Messages = e.Messages + }).ToList() + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Pipeline reload failed"); + return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Pipeline reload failed. See server logs for details." }); + } + } + + private static List GetSupportedSyncTypes(EtlPipelineConfig pipeline) + { + var types = new List(); + if (pipeline.SupportsMassSync) types.Add("mass"); + if (pipeline.SupportsDailySync) types.Add("daily"); + if (pipeline.SupportsHourlySync) types.Add("hourly"); + return types; + } +} + +/// +/// View model for pipeline registry status. +/// +public class PipelineRegistryStatusViewModel +{ + /// + /// Gets or sets the current registry version. + /// + public int Version { get; set; } + + /// + /// Gets or sets the timestamp of the last successful load. + /// + public DateTime? LastLoadedAt { get; set; } + + /// + /// Gets or sets the total number of pipelines. + /// + public int TotalPipelines { get; set; } + + /// + /// Gets or sets the number of enabled pipelines. + /// + public int EnabledPipelines { get; set; } +} + +/// +/// View model for pipeline reload result. +/// +public class PipelineReloadResultViewModel +{ + /// + /// Gets or sets a value indicating whether the reload was successful. + /// + public bool Success { get; set; } + + /// + /// Gets or sets the number of pipelines loaded. + /// + public int PipelinesLoaded { get; set; } + + /// + /// Gets or sets the number of pipelines skipped. + /// + public int PipelinesSkipped { get; set; } + + /// + /// Gets or sets the previous version. + /// + public int PreviousVersion { get; set; } + + /// + /// Gets or sets the new version. + /// + public int NewVersion { get; set; } + + /// + /// Gets or sets the list of errors. + /// + public List Errors { get; set; } = []; +} + +/// +/// View model for pipeline load error. +/// +public class PipelineLoadErrorViewModel +{ + /// + /// Gets or sets the file name. + /// + public string FileName { get; set; } = string.Empty; + + /// + /// Gets or sets the pipeline name. + /// + public string PipelineName { get; set; } = string.Empty; + + /// + /// Gets or sets the error type. + /// + public string ErrorType { get; set; } = string.Empty; + + /// + /// Gets or sets the error messages. + /// + public List Messages { get; set; } = []; +} diff --git a/NEW/src/JdeScoping.Client/Components/DataSync/NewSyncRequestDialog.razor b/NEW/src/JdeScoping.Client/Components/DataSync/NewSyncRequestDialog.razor new file mode 100644 index 0000000..2f58377 --- /dev/null +++ b/NEW/src/JdeScoping.Client/Components/DataSync/NewSyncRequestDialog.razor @@ -0,0 +1,47 @@ +@* + NewSyncRequestDialog.razor - Dialog for creating new manual sync requests. + + Allows users to select a pipeline and sync type to queue a new data sync request. + The sync type dropdown is filtered based on the selected pipeline's supported types. +*@ +@namespace JdeScoping.Client.Components.DataSync +@using JdeScoping.Core.ViewModels + + + + + + + + + + + @if (!string.IsNullOrEmpty(_selectedPipeline) && !string.IsNullOrEmpty(_selectedSyncType)) + { + + @_selectedSyncType sync will be queued for @_selectedPipeline. + + } + + + + + + diff --git a/NEW/src/JdeScoping.Client/Components/DataSync/NewSyncRequestDialog.razor.cs b/NEW/src/JdeScoping.Client/Components/DataSync/NewSyncRequestDialog.razor.cs new file mode 100644 index 0000000..de4cfd7 --- /dev/null +++ b/NEW/src/JdeScoping.Client/Components/DataSync/NewSyncRequestDialog.razor.cs @@ -0,0 +1,114 @@ +using System.Net.Http.Json; +using JdeScoping.Core.ViewModels; +using Microsoft.AspNetCore.Components; +using Radzen; + +namespace JdeScoping.Client.Components.DataSync; + +/// +/// Dialog component for creating new manual sync requests. +/// +public partial class NewSyncRequestDialog : ComponentBase +{ + /// + /// Gets or sets the Radzen dialog service for closing the dialog. + /// + [Inject] + private DialogService DialogService { get; set; } = default!; + + /// + /// Gets or sets the HTTP client for API calls. + /// + [Inject] + private HttpClient HttpClient { get; set; } = default!; + + /// + /// Gets or sets the available pipelines to select from. + /// + [Parameter] + public List Pipelines { get; set; } = new(); + + private string? _selectedPipeline; + private string? _selectedSyncType; + private List _availableSyncTypes = new(); + private bool _isCreating; + + /// + /// Determines if the Create button should be enabled. + /// + private bool CanCreate => !string.IsNullOrEmpty(_selectedPipeline) && + !string.IsNullOrEmpty(_selectedSyncType) && + !_isCreating; + + /// + /// Handles pipeline selection change by filtering available sync types. + /// + private void OnPipelineChanged() + { + // Reset sync type when pipeline changes + _selectedSyncType = null; + + if (string.IsNullOrEmpty(_selectedPipeline)) + { + _availableSyncTypes = new(); + return; + } + + // Find the selected pipeline and get its supported sync types + var pipeline = Pipelines.FirstOrDefault(p => p.Name == _selectedPipeline); + _availableSyncTypes = pipeline?.SupportedSyncTypes ?? new(); + } + + /// + /// Handles the Cancel button click by closing the dialog with null result. + /// + private void OnCancelAsync() + { + DialogService.Close(null); + } + + /// + /// Handles the Create button click by calling the API and closing with the result. + /// + private async Task OnCreateAsync() + { + if (!CanCreate) + { + return; + } + + _isCreating = true; + StateHasChanged(); + + try + { + var createDto = new CreateManualSyncRequestDto + { + PipelineName = _selectedPipeline!, + SyncType = _selectedSyncType! + }; + + var response = await HttpClient.PostAsJsonAsync("api/manual-sync", createDto); + + if (response.IsSuccessStatusCode) + { + var createdRequest = await response.Content.ReadFromJsonAsync(); + DialogService.Close(createdRequest); + } + else + { + // On error, close with null (parent can show notification if needed) + DialogService.Close(null); + } + } + catch + { + // On exception, close with null + DialogService.Close(null); + } + finally + { + _isCreating = false; + } + } +} diff --git a/NEW/src/JdeScoping.Client/Layout/MainLayout.razor b/NEW/src/JdeScoping.Client/Layout/MainLayout.razor index c78b451..55995fa 100644 --- a/NEW/src/JdeScoping.Client/Layout/MainLayout.razor +++ b/NEW/src/JdeScoping.Client/Layout/MainLayout.razor @@ -13,6 +13,7 @@ New Search Search Queue Refresh Status + Data Sync