diff --git a/NEW/docs/plans/2026-01-07-pipeline-viewer-design.md b/NEW/docs/plans/2026-01-07-pipeline-viewer-design.md deleted file mode 100644 index ad3a9bc..0000000 --- a/NEW/docs/plans/2026-01-07-pipeline-viewer-design.md +++ /dev/null @@ -1,488 +0,0 @@ -# ETL Pipeline Viewer Component Design - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Build a Blazor component that visualizes ETL pipeline configuration and execution status. - -**Architecture:** Server-side API loads pipeline config and exposes DTOs; Blazor client consumes via typed HttpClient following existing ApiClientBase pattern. - -**Tech Stack:** Blazor WebAssembly, Radzen components, existing DataSync config types, existing DataUpdateRepository - ---- - -## Codex Review Feedback (Addressed) - -| Issue | Resolution | -|-------|------------| -| Client loading pipelines.json directly breaks CLEAN | API loads config, returns DTO to client | -| Duplicate config records diverge from DataSync | Reuse existing DataSync Configuration types | -| GetRecentUpdatesAsync missing UpdateType filter | Add UpdateType parameter to signature | -| Status/overdue logic diverges from DataSync | Reuse existing `DataUpdateRepository.IsOverdue()` | -| Only LastSuccessfulRun, hiding failures | Add LastRun + LastRunWasSuccessful | -| API patterns don't match ApiRoutes + ApiClientBase | Add `ApiRoutes.Pipelines`, implement `PipelineApiClient` | -| Duration non-nullable for in-progress | Make Duration nullable | -| ScheduleType as string | Use `UpdateTypes` enum | -| Missing authorization | Add `[Authorize]` to page and controller | - ---- - -## Component Structure - -### Page Layout - -``` -┌─────────────────────────────────────────────────────────────┐ -│ ETL Pipeline Configuration Viewer [Authorize] │ -├─────────────────────────────────────────────────────────────┤ -│ Pipeline: [Dropdown - Alphabetical list] │ -├─────────────────────────────────────────────────────────────┤ -│ SCHEDULE STATUS SUMMARY │ -│ ┌─────────┬────────────┬────────────┬─────────────┬───────┐ │ -│ │ Type │ Last Run │ Success? │ Next Req. │Status │ │ -│ ├─────────┼────────────┼────────────┼─────────────┼───────┤ │ -│ │ Mass │ 01/05 02:00│ ✓ │ 01/12 02:00 │ OK │ │ -│ │ Daily │ 01/07 01:00│ ✓ │ 01/08 01:00 │ OK │ │ -│ │ Hourly │ 01/07 10:00│ ✗ │ 01/07 11:00 │ Over │ │ -│ └─────────┴────────────┴────────────┴─────────────┴───────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ RECENT EXECUTION HISTORY (Last 10 per type) │ -│ ┌─────────┬────────────┬──────────┬─────────┬─────┬───────┐ │ -│ │ Type │ Start │ End │ Duration│ Rows│ Result│ │ -│ └─────────┴────────────┴──────────┴─────────┴─────┴───────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ COMMON PIPELINE INFO │ -│ [Source Card] [Destination Card] [Scripts Card] │ -├─────────────────────────────────────────────────────────────┤ -│ MASS REFRESH [Enabled] │ -│ [Schedule Settings] [Query Preview + Modal] │ -├─────────────────────────────────────────────────────────────┤ -│ DAILY REFRESH [Enabled] │ -├─────────────────────────────────────────────────────────────┤ -│ HOURLY REFRESH [Enabled] │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Architecture - -### CLEAN Architecture Layers - -``` -┌─────────────────────────────────────────────────────────────┐ -│ PRESENTATION (JdeScoping.Client) │ -│ - PipelineViewer.razor (page) │ -│ - PipelineScheduleSection.razor (component) │ -│ - SqlQueryModal.razor (component) │ -│ - PipelineApiClient : ApiClientBase │ -└─────────────────────────────────────────────────────────────┘ - │ HTTP via ApiRoutes.Pipelines - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ APPLICATION (JdeScoping.Api) │ -│ - PipelineController │ -│ - Returns DTOs from Core.ApiContracts │ -└─────────────────────────────────────────────────────────────┘ - │ Depends on - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ INFRASTRUCTURE (JdeScoping.DataSync) │ -│ - IEtlPipelineFactory (existing - loads pipelines.json) │ -│ - IDataUpdateRepository (add GetRecentUpdatesAsync) │ -│ - Reuse existing Configuration/* types │ -└─────────────────────────────────────────────────────────────┘ - │ Depends on - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ DOMAIN (JdeScoping.Core) │ -│ - ApiRoutes.Pipelines (route constants) │ -│ - ApiContracts.Pipelines/* (DTOs) │ -│ - Models.Enums.UpdateTypes (existing) │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## Files to Create/Modify - -### Core Project (`src/JdeScoping.Core/`) - -| File | Action | Description | -|------|--------|-------------| -| `ApiContracts/ApiRoutes.cs` | Modify | Add `Pipelines` routes | -| `ApiContracts/Pipelines/PipelineListResponse.cs` | Create | List of pipeline names | -| `ApiContracts/Pipelines/PipelineConfigDto.cs` | Create | Pipeline config DTO (sanitized) | -| `ApiContracts/Pipelines/PipelineStatusDto.cs` | Create | Schedule status DTO | -| `ApiContracts/Pipelines/PipelineExecutionDto.cs` | Create | Execution history DTO | - -### DataSync Project (`src/JdeScoping.DataSync/`) - -| File | Action | Description | -|------|--------|-------------| -| `Contracts/IDataUpdateRepository.cs` | Modify | Add `GetRecentUpdatesAsync` | -| `Services/DataUpdateRepository.cs` | Modify | Implement new method | - -### API Project (`src/JdeScoping.Api/`) - -| File | Action | Description | -|------|--------|-------------| -| `Controllers/PipelineController.cs` | Create | API endpoints | - -### Client Project (`src/JdeScoping.Client/`) - -| File | Action | Description | -|------|--------|-------------| -| `Services/IPipelineApiClient.cs` | Create | Client interface | -| `Services/PipelineApiClient.cs` | Create | Client implementation | -| `Pages/Admin/PipelineViewer.razor` | Create | Main page | -| `Components/Admin/PipelineScheduleSection.razor` | Create | Schedule section | -| `Components/Admin/SqlQueryModal.razor` | Create | SQL modal | - ---- - -## API Routes (add to `ApiRoutes.cs`) - -```csharp -/// -/// Routes for pipeline configuration API endpoints. -/// -public static class Pipelines -{ - /// Base route for pipeline endpoints. - public const string Base = "api/pipelines"; - - /// Route template for getting a pipeline by name. - public const string ByName = "{name}"; - - /// Route template for getting pipeline status. - public const string Status = "{name}/status"; - - /// Route template for getting pipeline executions. - public const string Executions = "{name}/executions"; - - /// Builds the route to get a specific pipeline config. - public static string GetByName(string name) => $"api/pipelines/{Uri.EscapeDataString(name)}"; - - /// Builds the route to get pipeline status. - public static string GetStatus(string name) => $"api/pipelines/{Uri.EscapeDataString(name)}/status"; - - /// Builds the route to get pipeline executions. - public static string GetExecutions(string name, int count = 10) => - $"api/pipelines/{Uri.EscapeDataString(name)}/executions?count={count}"; -} -``` - ---- - -## DTOs (in `Core/ApiContracts/Pipelines/`) - -### PipelineListResponse.cs - -```csharp -namespace JdeScoping.Core.ApiContracts.Pipelines; - -/// -/// Response containing list of available pipeline names. -/// -public record PipelineListResponse(List PipelineNames); -``` - -### PipelineConfigDto.cs - -```csharp -namespace JdeScoping.Core.ApiContracts.Pipelines; - -/// -/// Pipeline configuration DTO for display (sanitized - no raw SQL exposed by default). -/// -public record PipelineConfigDto( - string Name, - PipelineSourceDto Source, - PipelineDestinationDto Destination, - PipelineSchedulesDto Schedules, - int PreScriptCount, - int PostScriptCount, - List? PreScripts, // Only populated if user has admin role - List? PostScripts); // Only populated if user has admin role - -public record PipelineSourceDto( - string Connection, - string? QueryPreview, // First 100 chars - string? MassQueryPreview, // First 100 chars - string? Query, // Full query - only if admin - string? MassQuery, // Full query - only if admin - List Parameters); - -public record PipelineParameterDto( - string Name, - string? Format, - string Source); - -public record PipelineDestinationDto( - string Table, - string OperationType, // "BulkMerge" or "BulkImport" - List? MatchColumns, - List? ExcludeFromUpdate); - -public record PipelineSchedulesDto( - PipelineScheduleDto Mass, - PipelineScheduleDto Daily, - PipelineScheduleDto Hourly); - -public record PipelineScheduleDto( - bool Enabled, - int IntervalMinutes, - bool PrePurge, - bool ReIndex, - bool IntervalIsOverride, - bool PrePurgeIsOverride, - bool ReIndexIsOverride); -``` - -### PipelineStatusDto.cs - -```csharp -namespace JdeScoping.Core.ApiContracts.Pipelines; - -using JdeScoping.Core.Models.Enums; - -/// -/// Pipeline schedule status for each update type. -/// -public record PipelineStatusResponse(List Statuses); - -public record PipelineScheduleStatusDto( - UpdateTypes ScheduleType, - DateTime? LastRun, - bool LastRunWasSuccessful, - DateTime? LastSuccessfulRun, - DateTime? NextRequiredRun, - bool IsOverdue, - int IntervalMinutes); -``` - -### PipelineExecutionDto.cs - -```csharp -namespace JdeScoping.Core.ApiContracts.Pipelines; - -using JdeScoping.Core.Models.Enums; - -/// -/// Pipeline execution history. -/// -public record PipelineExecutionsResponse(List Executions); - -public record PipelineExecutionDto( - UpdateTypes ScheduleType, - DateTime StartTime, - DateTime? EndTime, - TimeSpan? Duration, // Nullable for in-progress runs - long RecordCount, - bool WasSuccessful); -``` - ---- - -## Repository Update (IDataUpdateRepository) - -```csharp -/// -/// Gets the last N execution records for a specific table and optional update type. -/// -/// The table name to filter by. -/// Optional update type filter. If null, returns all types. -/// Maximum records to return per update type. -/// Cancellation token. -/// List of DataUpdate records ordered by StartDt descending. -Task> GetRecentUpdatesAsync( - string tableName, - UpdateTypes? updateType = null, - int count = 10, - CancellationToken cancellationToken = default); - -/// -/// Gets the last run (successful or not) for each update type for a table. -/// -/// The table name. -/// Cancellation token. -/// Dictionary keyed by UpdateType. -Task> GetLastRunsAsync( - string tableName, - CancellationToken cancellationToken = default); -``` - ---- - -## API Controller - -```csharp -[ApiController] -[Route(ApiRoutes.Pipelines.Base)] -[Authorize] -public class PipelineController : ControllerBase -{ - private readonly IEtlPipelineFactory _pipelineFactory; - private readonly IDataUpdateRepository _dataUpdateRepository; - - [HttpGet] - public ActionResult GetPipelineNames() - { - var names = _pipelineFactory.GetAvailableTables() - .OrderBy(n => n) - .ToList(); - return Ok(new PipelineListResponse(names)); - } - - [HttpGet(ApiRoutes.Pipelines.ByName)] - public ActionResult GetPipeline(string name) - { - // Map from internal config to DTO - // Truncate queries for preview, include full if admin - } - - [HttpGet(ApiRoutes.Pipelines.Status)] - public async Task> GetStatus(string name) - { - // Get last runs, calculate IsOverdue using DataUpdateRepository.IsOverdue() - } - - [HttpGet(ApiRoutes.Pipelines.Executions)] - public async Task> GetExecutions( - string name, [FromQuery] int count = 10) - { - // Get recent updates from repository - } -} -``` - ---- - -## Client Implementation - -### PipelineApiClient.cs - -```csharp -public interface IPipelineApiClient -{ - Task> GetPipelineNamesAsync(CancellationToken ct = default); - Task> GetPipelineAsync(string name, CancellationToken ct = default); - Task> GetStatusAsync(string name, CancellationToken ct = default); - Task> GetExecutionsAsync(string name, int count = 10, CancellationToken ct = default); -} - -public class PipelineApiClient : ApiClientBase, IPipelineApiClient -{ - public PipelineApiClient(HttpClient httpClient) : base(httpClient) { } - - public Task> GetPipelineNamesAsync(CancellationToken ct = default) - => GetAsync(ApiRoutes.Pipelines.Base, ct); - - public Task> GetPipelineAsync(string name, CancellationToken ct = default) - => GetAsync(ApiRoutes.Pipelines.GetByName(name), ct); - - public Task> GetStatusAsync(string name, CancellationToken ct = default) - => GetAsync(ApiRoutes.Pipelines.GetStatus(name), ct); - - public Task> GetExecutionsAsync(string name, int count = 10, CancellationToken ct = default) - => GetAsync(ApiRoutes.Pipelines.GetExecutions(name, count), ct); -} -``` - ---- - -## Blazor Components - -### PipelineViewer.razor Structure - -```razor -@page "/admin/pipeline-viewer" -@attribute [Authorize] -@inject IPipelineApiClient PipelineApi - -Pipeline Configuration Viewer - -ETL Pipeline Configuration Viewer - - - - -@if (_config is not null) -{ - - - - -} - - -``` - -### SqlQueryModal.razor - -- Large modal (80% width) -- `
` with monospace font
-- Copy to clipboard button
-- Basic SQL formatting (line breaks at clauses)
-
-### PipelineScheduleSection.razor
-
-- Props: ScheduleType (UpdateTypes), Config (PipelineScheduleDto), Query, OnViewQuery callback
-- Shows enabled badge
-- Settings table with Default/Override indicators
-- Query preview with "View Full" button
-
----
-
-## Implementation Notes
-
-### Override Detection (in API)
-
-```csharp
-bool IsOverride(T? pipelineValue, T defaultValue) where T : struct =>
-    pipelineValue.HasValue && !pipelineValue.Value.Equals(defaultValue);
-```
-
-### Overdue Calculation (reuse existing)
-
-```csharp
-// Use DataUpdateRepository.IsOverdue() which includes 50% grace period
-var isOverdue = DataUpdateRepository.IsOverdue(
-    lastSuccessfulRun, tableName, updateType, customIntervals);
-```
-
-### Connection Type Badge Colors
-
-| Connection | Badge Style |
-|------------|-------------|
-| jde | `BadgeStyle.Info` (blue) |
-| cms | `BadgeStyle.Success` (green) |
-| giw | `BadgeStyle.Warning` (orange) |
-
-### SQL Query Truncation
-
-```csharp
-static string Truncate(string? sql, int maxLength = 100) =>
-    sql is null ? "" :
-    sql.Length <= maxLength ? sql : sql[..maxLength] + "...";
-```
-
----
-
-## Security Considerations
-
-1. **Authorization**: Page and API require `[Authorize]`
-2. **SQL Visibility**: Full SQL/scripts only exposed if user has appropriate permissions (consider admin-only)
-3. **No write operations**: This is read-only, no mutations exposed
-
----
-
-## Dependencies
-
-- Radzen.Blazor (existing)
-- Existing `IEtlPipelineFactory` and `IDataUpdateRepository`
-- Existing `pipelines.json` configuration
-- Existing `UpdateTypes` enum
-- Existing `ApiClientBase` pattern
diff --git a/NEW/docs/plans/2026-01-07-pipeline-viewer-implementation.md b/NEW/docs/plans/2026-01-07-pipeline-viewer-implementation.md
deleted file mode 100644
index dec6b51..0000000
--- a/NEW/docs/plans/2026-01-07-pipeline-viewer-implementation.md
+++ /dev/null
@@ -1,1327 +0,0 @@
-# ETL Pipeline Viewer Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Implement the ETL Pipeline Viewer Blazor component with API backend.
-
-**Architecture:** API-first approach - build DTOs and API endpoints first, then client components.
-
-**Tech Stack:** .NET 10, ASP.NET Core API, Blazor WebAssembly, Radzen, Dapper
-
----
-
-## Task 1: Create Pipeline DTOs in Core
-
-**Files:**
-- Create: `src/JdeScoping.Core/ApiContracts/Pipelines/PipelineListResponse.cs`
-- Create: `src/JdeScoping.Core/ApiContracts/Pipelines/PipelineConfigDto.cs`
-- Create: `src/JdeScoping.Core/ApiContracts/Pipelines/PipelineStatusDto.cs`
-- Create: `src/JdeScoping.Core/ApiContracts/Pipelines/PipelineExecutionDto.cs`
-
-**Step 1: Create the Pipelines directory**
-
-```bash
-mkdir -p src/JdeScoping.Core/ApiContracts/Pipelines
-```
-
-**Step 2: Create PipelineListResponse.cs**
-
-```csharp
-namespace JdeScoping.Core.ApiContracts.Pipelines;
-
-/// 
-/// Response containing list of available pipeline names.
-/// 
-public record PipelineListResponse(List PipelineNames);
-```
-
-**Step 3: Create PipelineConfigDto.cs**
-
-```csharp
-namespace JdeScoping.Core.ApiContracts.Pipelines;
-
-/// 
-/// Pipeline configuration DTO for display.
-/// 
-public record PipelineConfigDto(
-    string Name,
-    PipelineSourceDto Source,
-    PipelineDestinationDto Destination,
-    PipelineSchedulesDto Schedules,
-    int PreScriptCount,
-    int PostScriptCount,
-    List? PreScripts,
-    List? PostScripts);
-
-public record PipelineSourceDto(
-    string Connection,
-    string? QueryPreview,
-    string? MassQueryPreview,
-    string? Query,
-    string? MassQuery,
-    List Parameters);
-
-public record PipelineParameterDto(
-    string Name,
-    string? Format,
-    string Source);
-
-public record PipelineDestinationDto(
-    string Table,
-    string OperationType,
-    List? MatchColumns,
-    List? ExcludeFromUpdate);
-
-public record PipelineSchedulesDto(
-    PipelineScheduleDto Mass,
-    PipelineScheduleDto Daily,
-    PipelineScheduleDto Hourly);
-
-public record PipelineScheduleDto(
-    bool Enabled,
-    int IntervalMinutes,
-    bool PrePurge,
-    bool ReIndex,
-    bool IntervalIsOverride,
-    bool PrePurgeIsOverride,
-    bool ReIndexIsOverride);
-```
-
-**Step 4: Create PipelineStatusDto.cs**
-
-```csharp
-namespace JdeScoping.Core.ApiContracts.Pipelines;
-
-using JdeScoping.Core.Models.Enums;
-
-/// 
-/// Pipeline schedule status for each update type.
-/// 
-public record PipelineStatusResponse(List Statuses);
-
-public record PipelineScheduleStatusDto(
-    UpdateTypes ScheduleType,
-    DateTime? LastRun,
-    bool LastRunWasSuccessful,
-    DateTime? LastSuccessfulRun,
-    DateTime? NextRequiredRun,
-    bool IsOverdue,
-    int IntervalMinutes);
-```
-
-**Step 5: Create PipelineExecutionDto.cs**
-
-```csharp
-namespace JdeScoping.Core.ApiContracts.Pipelines;
-
-using JdeScoping.Core.Models.Enums;
-
-/// 
-/// Pipeline execution history.
-/// 
-public record PipelineExecutionsResponse(List Executions);
-
-public record PipelineExecutionDto(
-    UpdateTypes ScheduleType,
-    DateTime StartTime,
-    DateTime? EndTime,
-    TimeSpan? Duration,
-    long RecordCount,
-    bool WasSuccessful);
-```
-
-**Step 6: Verify build**
-
-```bash
-dotnet build src/JdeScoping.Core
-```
-
-**Step 7: Commit**
-
-```bash
-git add src/JdeScoping.Core/ApiContracts/Pipelines/
-git commit -m "feat(core): add pipeline viewer DTOs"
-```
-
----
-
-## Task 2: Add Pipeline Routes to ApiRoutes
-
-**Files:**
-- Modify: `src/JdeScoping.Core/ApiContracts/ApiRoutes.cs`
-
-**Step 1: Add Pipelines routes class after FileIO class**
-
-Add at the end of `ApiRoutes.cs`, before the closing brace:
-
-```csharp
-    /// 
-    /// Routes for pipeline configuration API endpoints.
-    /// 
-    public static class Pipelines
-    {
-        /// Base route for pipeline endpoints.
-        public const string Base = "api/pipelines";
-
-        /// Route template for getting a pipeline by name.
-        public const string ByName = "{name}";
-
-        /// Route template for getting pipeline status.
-        public const string Status = "{name}/status";
-
-        /// Route template for getting pipeline executions.
-        public const string Executions = "{name}/executions";
-
-        /// Builds the route to get a specific pipeline config.
-        public static string GetByName(string name) => $"api/pipelines/{Uri.EscapeDataString(name)}";
-
-        /// Builds the route to get pipeline status.
-        public static string GetStatus(string name) => $"api/pipelines/{Uri.EscapeDataString(name)}/status";
-
-        /// Builds the route to get pipeline executions.
-        public static string GetExecutions(string name, int count = 10) =>
-            $"api/pipelines/{Uri.EscapeDataString(name)}/executions?count={count}";
-    }
-```
-
-**Step 2: Verify build**
-
-```bash
-dotnet build src/JdeScoping.Core
-```
-
-**Step 3: Commit**
-
-```bash
-git add src/JdeScoping.Core/ApiContracts/ApiRoutes.cs
-git commit -m "feat(core): add pipeline API routes"
-```
-
----
-
-## Task 3: Add Repository Methods to IDataUpdateRepository
-
-**Files:**
-- Modify: `src/JdeScoping.DataSync/Contracts/IDataUpdateRepository.cs`
-
-**Step 1: Add new method signatures**
-
-Add these methods to the interface:
-
-```csharp
-    /// 
-    /// Gets the last N execution records for a specific table.
-    /// 
-    /// The table name to filter by.
-    /// Optional update type filter. If null, returns all types.
-    /// Maximum records to return (total, not per type).
-    /// Cancellation token.
-    /// List of DataUpdate records ordered by StartDt descending.
-    Task> GetRecentUpdatesAsync(
-        string tableName,
-        UpdateTypes? updateType = null,
-        int count = 30,
-        CancellationToken cancellationToken = default);
-
-    /// 
-    /// Gets the last run (successful or not) for each update type for a table.
-    /// 
-    /// The table name.
-    /// Cancellation token.
-    /// Dictionary keyed by UpdateType.
-    Task> GetLastRunsAsync(
-        string tableName,
-        CancellationToken cancellationToken = default);
-```
-
-**Step 2: Verify build**
-
-```bash
-dotnet build src/JdeScoping.DataSync
-```
-
-Expected: Build fails (not implemented yet)
-
-**Step 3: Commit interface changes**
-
-```bash
-git add src/JdeScoping.DataSync/Contracts/IDataUpdateRepository.cs
-git commit -m "feat(datasync): add repository methods for pipeline viewer"
-```
-
----
-
-## Task 4: Implement Repository Methods
-
-**Files:**
-- Modify: `src/JdeScoping.DataSync/Services/DataUpdateRepository.cs`
-
-**Step 1: Implement GetRecentUpdatesAsync**
-
-Add this method to `DataUpdateRepository`:
-
-```csharp
-    /// 
-    public async Task> GetRecentUpdatesAsync(
-        string tableName,
-        UpdateTypes? updateType = null,
-        int count = 30,
-        CancellationToken cancellationToken = default)
-    {
-        var sql = updateType.HasValue
-            ? @"SELECT TOP (@count) du.Id, du.SourceSystem, du.SourceData, du.TableName,
-                       du.StartDt, du.EndDt, du.UpdateType, du.WasSuccessful, du.NumberRecords
-                FROM dbo.DataUpdate du
-                WHERE du.TableName = @tableName AND du.UpdateType = @updateType
-                ORDER BY du.StartDt DESC"
-            : @"SELECT TOP (@count) du.Id, du.SourceSystem, du.SourceData, du.TableName,
-                       du.StartDt, du.EndDt, du.UpdateType, du.WasSuccessful, du.NumberRecords
-                FROM dbo.DataUpdate du
-                WHERE du.TableName = @tableName
-                ORDER BY du.StartDt DESC";
-
-        await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(cancellationToken);
-        var results = await connection.QueryAsync(
-            sql,
-            new { tableName, updateType = updateType.HasValue ? (int)updateType.Value : 0, count },
-            commandTimeout: 30);
-
-        return results.ToList();
-    }
-```
-
-**Step 2: Implement GetLastRunsAsync**
-
-Add this method to `DataUpdateRepository`:
-
-```csharp
-    /// 
-    public async Task> GetLastRunsAsync(
-        string tableName,
-        CancellationToken cancellationToken = default)
-    {
-        const string sql = @"
-WITH LastRuns AS (
-    SELECT du.*,
-           ROW_NUMBER() OVER (PARTITION BY du.UpdateType ORDER BY du.StartDt DESC) AS RN
-    FROM dbo.DataUpdate du
-    WHERE du.TableName = @tableName
-)
-SELECT Id, SourceSystem, SourceData, TableName, StartDt, EndDt, UpdateType, WasSuccessful, NumberRecords
-FROM LastRuns
-WHERE RN = 1";
-
-        await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(cancellationToken);
-        var results = await connection.QueryAsync(sql, new { tableName }, commandTimeout: 30);
-
-        return results.ToDictionary(du => du.UpdateType, du => du);
-    }
-```
-
-**Step 3: Verify build**
-
-```bash
-dotnet build src/JdeScoping.DataSync
-```
-
-**Step 4: Run existing tests**
-
-```bash
-dotnet test tests/JdeScoping.DataSync.Tests --filter "DataUpdateRepository" --no-build
-```
-
-**Step 5: Commit**
-
-```bash
-git add src/JdeScoping.DataSync/Services/DataUpdateRepository.cs
-git commit -m "feat(datasync): implement GetRecentUpdatesAsync and GetLastRunsAsync"
-```
-
----
-
-## Task 5: Create PipelineController
-
-**Files:**
-- Create: `src/JdeScoping.Api/Controllers/PipelineController.cs`
-
-**Step 1: Create the controller**
-
-```csharp
-using JdeScoping.Core.ApiContracts;
-using JdeScoping.Core.ApiContracts.Pipelines;
-using JdeScoping.Core.Models.Enums;
-using JdeScoping.DataSync.Contracts;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-
-namespace JdeScoping.Api.Controllers;
-
-/// 
-/// API endpoints for pipeline configuration and status.
-/// 
-[ApiController]
-[Route(ApiRoutes.Pipelines.Base)]
-[Authorize]
-public class PipelineController : ControllerBase
-{
-    private readonly IEtlPipelineFactory _pipelineFactory;
-    private readonly IDataUpdateRepository _dataUpdateRepository;
-
-    public PipelineController(
-        IEtlPipelineFactory pipelineFactory,
-        IDataUpdateRepository dataUpdateRepository)
-    {
-        _pipelineFactory = pipelineFactory;
-        _dataUpdateRepository = dataUpdateRepository;
-    }
-
-    /// 
-    /// Gets list of all available pipeline names.
-    /// 
-    [HttpGet]
-    public ActionResult GetPipelineNames()
-    {
-        var names = _pipelineFactory.GetAvailableTables()
-            .OrderBy(n => n)
-            .ToList();
-        return Ok(new PipelineListResponse(names));
-    }
-
-    /// 
-    /// Gets configuration for a specific pipeline.
-    /// 
-    [HttpGet(ApiRoutes.Pipelines.ByName)]
-    public ActionResult GetPipeline(string name)
-    {
-        var config = _pipelineFactory.GetPipelineConfig(name);
-        if (config is null)
-            return NotFound();
-
-        var defaults = _pipelineFactory.GetScheduleDefaults();
-        var dto = MapToDto(name, config, defaults);
-        return Ok(dto);
-    }
-
-    /// 
-    /// Gets schedule status for a pipeline.
-    /// 
-    [HttpGet(ApiRoutes.Pipelines.Status)]
-    public async Task> GetStatus(
-        string name,
-        CancellationToken cancellationToken)
-    {
-        var config = _pipelineFactory.GetPipelineConfig(name);
-        if (config is null)
-            return NotFound();
-
-        var tableName = config.Destination.Table;
-        var lastRuns = await _dataUpdateRepository.GetLastRunsAsync(tableName, cancellationToken);
-        var lastSuccessful = await _dataUpdateRepository.GetLastDataUpdatesAsync(cancellationToken);
-        var defaults = _pipelineFactory.GetScheduleDefaults();
-
-        var statuses = new List();
-        foreach (var updateType in new[] { UpdateTypes.Mass, UpdateTypes.Daily, UpdateTypes.Hourly })
-        {
-            var scheduleConfig = GetScheduleConfig(config, updateType);
-            var interval = GetEffectiveInterval(scheduleConfig, defaults, updateType);
-
-            lastRuns.TryGetValue(updateType, out var lastRun);
-            var successKey = $"{tableName}_{(int)updateType}";
-            lastSuccessful.TryGetValue(successKey, out var lastSuccess);
-
-            var nextRequired = lastSuccess?.EndDt.AddMinutes(interval);
-            var isOverdue = DataUpdateRepository.IsOverdue(
-                lastSuccess?.EndDt, tableName, updateType, null);
-
-            statuses.Add(new PipelineScheduleStatusDto(
-                updateType,
-                lastRun?.StartDt,
-                lastRun?.WasSuccessful ?? false,
-                lastSuccess?.EndDt,
-                nextRequired,
-                isOverdue,
-                interval));
-        }
-
-        return Ok(new PipelineStatusResponse(statuses));
-    }
-
-    /// 
-    /// Gets recent execution history for a pipeline.
-    /// 
-    [HttpGet(ApiRoutes.Pipelines.Executions)]
-    public async Task> GetExecutions(
-        string name,
-        [FromQuery] int count = 30,
-        CancellationToken cancellationToken = default)
-    {
-        var config = _pipelineFactory.GetPipelineConfig(name);
-        if (config is null)
-            return NotFound();
-
-        var tableName = config.Destination.Table;
-        var updates = await _dataUpdateRepository.GetRecentUpdatesAsync(
-            tableName, null, count, cancellationToken);
-
-        var executions = updates.Select(u => new PipelineExecutionDto(
-            u.UpdateType,
-            u.StartDt,
-            u.EndDt == default ? null : u.EndDt,
-            u.EndDt == default ? null : u.EndDt - u.StartDt,
-            u.NumberRecords,
-            u.WasSuccessful
-        )).ToList();
-
-        return Ok(new PipelineExecutionsResponse(executions));
-    }
-
-    private PipelineConfigDto MapToDto(
-        string name,
-        DataSync.Configuration.PipelineConfig config,
-        DataSync.Configuration.ScheduleDefaults defaults)
-    {
-        var source = new PipelineSourceDto(
-            config.Source.Connection,
-            Truncate(config.Source.Query),
-            Truncate(config.Source.MassQuery),
-            config.Source.Query,
-            config.Source.MassQuery,
-            config.Source.Parameters?.Select(p => new PipelineParameterDto(
-                p.Key, p.Value.Format, p.Value.Source)).ToList() ?? []);
-
-        var matchCols = config.Destination.MatchColumns?.ToList();
-        var destination = new PipelineDestinationDto(
-            config.Destination.Table,
-            matchCols?.Count > 0 ? "BulkMerge" : "BulkImport",
-            matchCols,
-            config.Destination.ExcludeFromUpdate?.ToList());
-
-        var schedules = new PipelineSchedulesDto(
-            MapSchedule(config.Schedules?.Mass, defaults.Mass),
-            MapSchedule(config.Schedules?.Daily, defaults.Daily),
-            MapSchedule(config.Schedules?.Hourly, defaults.Hourly));
-
-        return new PipelineConfigDto(
-            name,
-            source,
-            destination,
-            schedules,
-            config.PreScripts?.Count ?? 0,
-            config.PostScripts?.Count ?? 0,
-            config.PreScripts,
-            config.PostScripts);
-    }
-
-    private static PipelineScheduleDto MapSchedule(
-        DataSync.Configuration.ScheduleConfig? config,
-        DataSync.Configuration.ScheduleConfig defaults)
-    {
-        return new PipelineScheduleDto(
-            config?.Enabled ?? defaults.Enabled ?? true,
-            config?.IntervalMinutes ?? defaults.IntervalMinutes ?? 60,
-            config?.PrePurge ?? defaults.PrePurge ?? false,
-            config?.ReIndex ?? defaults.ReIndex ?? false,
-            config?.IntervalMinutes.HasValue == true && config.IntervalMinutes != defaults.IntervalMinutes,
-            config?.PrePurge.HasValue == true && config.PrePurge != defaults.PrePurge,
-            config?.ReIndex.HasValue == true && config.ReIndex != defaults.ReIndex);
-    }
-
-    private static DataSync.Configuration.ScheduleConfig? GetScheduleConfig(
-        DataSync.Configuration.PipelineConfig config,
-        UpdateTypes updateType) => updateType switch
-    {
-        UpdateTypes.Mass => config.Schedules?.Mass,
-        UpdateTypes.Daily => config.Schedules?.Daily,
-        UpdateTypes.Hourly => config.Schedules?.Hourly,
-        _ => null
-    };
-
-    private static int GetEffectiveInterval(
-        DataSync.Configuration.ScheduleConfig? config,
-        DataSync.Configuration.ScheduleDefaults defaults,
-        UpdateTypes updateType)
-    {
-        if (config?.IntervalMinutes.HasValue == true)
-            return config.IntervalMinutes.Value;
-
-        return updateType switch
-        {
-            UpdateTypes.Mass => defaults.Mass?.IntervalMinutes ?? 10080,
-            UpdateTypes.Daily => defaults.Daily?.IntervalMinutes ?? 1440,
-            UpdateTypes.Hourly => defaults.Hourly?.IntervalMinutes ?? 60,
-            _ => 60
-        };
-    }
-
-    private static string? Truncate(string? value, int maxLength = 100) =>
-        value is null ? null :
-        value.Length <= maxLength ? value : value[..maxLength] + "...";
-}
-```
-
-**Step 2: Verify IEtlPipelineFactory has needed methods**
-
-Check if `GetPipelineConfig` and `GetScheduleDefaults` exist. If not, we need to add them.
-
-**Step 3: Build and fix any issues**
-
-```bash
-dotnet build src/JdeScoping.Api
-```
-
-**Step 4: Commit**
-
-```bash
-git add src/JdeScoping.Api/Controllers/PipelineController.cs
-git commit -m "feat(api): add PipelineController for pipeline viewer"
-```
-
----
-
-## Task 6: Add Missing Factory Methods (if needed)
-
-**Files:**
-- Modify: `src/JdeScoping.DataSync/Contracts/IEtlPipelineFactory.cs`
-- Modify: `src/JdeScoping.DataSync/Services/EtlPipelineFactory.cs`
-
-**Step 1: Check if methods exist**
-
-Read `IEtlPipelineFactory.cs` and check for `GetPipelineConfig` and `GetScheduleDefaults`.
-
-**Step 2: If missing, add to interface**
-
-```csharp
-    /// 
-    /// Gets the configuration for a specific pipeline.
-    /// 
-    PipelineConfig? GetPipelineConfig(string tableName);
-
-    /// 
-    /// Gets the schedule defaults.
-    /// 
-    ScheduleDefaults GetScheduleDefaults();
-```
-
-**Step 3: Implement in EtlPipelineFactory**
-
-```csharp
-    public PipelineConfig? GetPipelineConfig(string tableName)
-    {
-        return _config.Pipelines.TryGetValue(tableName, out var config) ? config : null;
-    }
-
-    public ScheduleDefaults GetScheduleDefaults()
-    {
-        return _config.ScheduleDefaults ?? new ScheduleDefaults(
-            new ScheduleConfig { IntervalMinutes = 10080, PrePurge = true, ReIndex = true },
-            new ScheduleConfig { IntervalMinutes = 1440 },
-            new ScheduleConfig { IntervalMinutes = 60 });
-    }
-```
-
-**Step 4: Build**
-
-```bash
-dotnet build src/JdeScoping.DataSync && dotnet build src/JdeScoping.Api
-```
-
-**Step 5: Commit**
-
-```bash
-git add src/JdeScoping.DataSync/
-git commit -m "feat(datasync): add GetPipelineConfig and GetScheduleDefaults to factory"
-```
-
----
-
-## Task 7: Create Client API Service
-
-**Files:**
-- Create: `src/JdeScoping.Client/Services/IPipelineApiClient.cs`
-- Create: `src/JdeScoping.Client/Services/PipelineApiClient.cs`
-
-**Step 1: Create IPipelineApiClient.cs**
-
-```csharp
-using JdeScoping.Core.ApiContracts.Pipelines;
-using JdeScoping.Core.ApiContracts.Results;
-
-namespace JdeScoping.Client.Services;
-
-/// 
-/// Client for pipeline configuration API.
-/// 
-public interface IPipelineApiClient
-{
-    Task> GetPipelineNamesAsync(CancellationToken ct = default);
-    Task> GetPipelineAsync(string name, CancellationToken ct = default);
-    Task> GetStatusAsync(string name, CancellationToken ct = default);
-    Task> GetExecutionsAsync(string name, int count = 30, CancellationToken ct = default);
-}
-```
-
-**Step 2: Create PipelineApiClient.cs**
-
-```csharp
-using JdeScoping.Core.ApiContracts;
-using JdeScoping.Core.ApiContracts.Pipelines;
-using JdeScoping.Core.ApiContracts.Results;
-
-namespace JdeScoping.Client.Services;
-
-/// 
-/// Client implementation for pipeline configuration API.
-/// 
-public class PipelineApiClient : ApiClientBase, IPipelineApiClient
-{
-    public PipelineApiClient(HttpClient httpClient) : base(httpClient) { }
-
-    public Task> GetPipelineNamesAsync(CancellationToken ct = default)
-        => GetAsync(ApiRoutes.Pipelines.Base, ct);
-
-    public Task> GetPipelineAsync(string name, CancellationToken ct = default)
-        => GetAsync(ApiRoutes.Pipelines.GetByName(name), ct);
-
-    public Task> GetStatusAsync(string name, CancellationToken ct = default)
-        => GetAsync(ApiRoutes.Pipelines.GetStatus(name), ct);
-
-    public Task> GetExecutionsAsync(string name, int count = 30, CancellationToken ct = default)
-        => GetAsync(ApiRoutes.Pipelines.GetExecutions(name, count), ct);
-}
-```
-
-**Step 3: Register in DI (Program.cs)**
-
-Add to client DI registration:
-
-```csharp
-builder.Services.AddScoped();
-```
-
-**Step 4: Build**
-
-```bash
-dotnet build src/JdeScoping.Client
-```
-
-**Step 5: Commit**
-
-```bash
-git add src/JdeScoping.Client/Services/IPipelineApiClient.cs src/JdeScoping.Client/Services/PipelineApiClient.cs
-git commit -m "feat(client): add PipelineApiClient"
-```
-
----
-
-## Task 8: Create SqlQueryModal Component
-
-**Files:**
-- Create: `src/JdeScoping.Client/Components/Admin/SqlQueryModal.razor`
-
-**Step 1: Create Admin directory**
-
-```bash
-mkdir -p src/JdeScoping.Client/Components/Admin
-```
-
-**Step 2: Create SqlQueryModal.razor**
-
-```razor
-@namespace JdeScoping.Client.Components.Admin
-
-
-    
-        @Title
-    
-    
-        
-
@FormattedSql
-
-
- - - - -
- -@code { - [Parameter] public bool Visible { get; set; } - [Parameter] public EventCallback VisibleChanged { get; set; } - [Parameter] public string? Title { get; set; } - [Parameter] public string? Sql { get; set; } - - [Inject] private IJSRuntime JS { get; set; } = default!; - - 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); - } -} -``` - -**Step 3: Build** - -```bash -dotnet build src/JdeScoping.Client -``` - -**Step 4: Commit** - -```bash -git add src/JdeScoping.Client/Components/Admin/ -git commit -m "feat(client): add SqlQueryModal component" -``` - ---- - -## Task 9: Create PipelineScheduleSection Component - -**Files:** -- Create: `src/JdeScoping.Client/Components/Admin/PipelineScheduleSection.razor` - -**Step 1: Create PipelineScheduleSection.razor** - -```razor -@namespace JdeScoping.Client.Components.Admin -@using JdeScoping.Core.ApiContracts.Pipelines -@using JdeScoping.Core.Models.Enums - - - - - - @GetScheduleTypeName(ScheduleType) Refresh - - - - @if (Config?.Enabled == true) - { - - } - else - { - - } - - - - @if (Config is not null) - { - Schedule Settings - - - - - - - - - - - - - - - - - - - - - - - - - -
SettingValueSource
Interval@FormatInterval(Config.IntervalMinutes)@(Config.IntervalIsOverride ? "Override" : "Default")
Pre-Purge@(Config.PrePurge ? "Yes" : "No")@(Config.PrePurgeIsOverride ? "Override" : "Default")
Re-Index@(Config.ReIndex ? "Yes" : "No")@(Config.ReIndexIsOverride ? "Override" : "Default")
- - @if (!string.IsNullOrWhiteSpace(QueryPreview)) - { - Query -
- @QueryPreview -
- @if (!string.IsNullOrWhiteSpace(FullQuery)) - { - - } - } - } -
- -@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 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"; - } -} -``` - -**Step 2: Build** - -```bash -dotnet build src/JdeScoping.Client -``` - -**Step 3: Commit** - -```bash -git add src/JdeScoping.Client/Components/Admin/PipelineScheduleSection.razor -git commit -m "feat(client): add PipelineScheduleSection component" -``` - ---- - -## Task 10: Create PipelineViewer Page - -**Files:** -- Create: `src/JdeScoping.Client/Pages/Admin/PipelineViewer.razor` - -**Step 1: Create Admin directory** - -```bash -mkdir -p src/JdeScoping.Client/Pages/Admin -``` - -**Step 2: Create PipelineViewer.razor** - -```razor -@page "/admin/pipeline-viewer" -@attribute [Authorize] -@using JdeScoping.Client.Components.Admin -@using JdeScoping.Client.Components.Shared -@using JdeScoping.Core.ApiContracts.Pipelines -@using JdeScoping.Core.Models.Enums -@inject IPipelineApiClient PipelineApi - -Pipeline Configuration Viewer - JDE Scoping Tool - -ETL Pipeline Configuration Viewer - - - - - - - - -@if (_isLoading) -{ - -} -else if (_config is not null) -{ - - - Schedule Status Summary - - - - - - - - - - - - - - - - - - - - - - - - Recent Execution History - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Source -

Connection: - @switch (_config.Source.Connection.ToLower()) - { - case "jde": - - break; - case "cms": - - break; - case "giw": - - break; - default: - - break; - } -

- @if (_config.Source.Parameters.Count > 0) - { -

Parameters:

-
    - @foreach (var param in _config.Source.Parameters) - { -
  • @param.Name (@(param.Format ?? "default"))
  • - } -
- } -
-
- - - - - Destination -

Table: @_config.Destination.Table

-

Operation: - -

- @if (_config.Destination.MatchColumns?.Count > 0) - { -

Match Columns: @string.Join(", ", _config.Destination.MatchColumns)

- } - @if (_config.Destination.ExcludeFromUpdate?.Count > 0) - { -

Exclude: @string.Join(", ", _config.Destination.ExcludeFromUpdate)

- } -
-
- - - - - Scripts -

Pre-Scripts: @_config.PreScriptCount

-

Post-Scripts: @_config.PostScriptCount

- @if (_config.PreScripts?.Count > 0) - { - Pre-Scripts: - @for (int i = 0; i < _config.PreScripts.Count; i++) - { - var script = _config.PreScripts[i]; - var index = i + 1; -
- -
- } - } - @if (_config.PostScripts?.Count > 0) - { - Post-Scripts: - @for (int i = 0; i < _config.PostScripts.Count; i++) - { - var script = _config.PostScripts[i]; - var index = i + 1; -
- -
- } - } -
-
-
- - - - - - - -} - - - -@code { - private List _pipelineNames = []; - private string? _selectedPipeline; - private bool _isLoading; - private PipelineConfigDto? _config; - private List _statuses = []; - private List _executions = []; - - private bool _showSqlModal; - private string? _sqlModalTitle; - private string? _sqlModalContent; - - protected override async Task OnInitializedAsync() - { - var result = await PipelineApi.GetPipelineNamesAsync(); - if (result.TryGetValue(out var response)) - { - _pipelineNames = response.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); - - if (configTask.Result.TryGetValue(out var config)) - _config = config; - - if (statusTask.Result.TryGetValue(out var status)) - _statuses = status.Statuses; - - if (executionsTask.Result.TryGetValue(out var execs)) - _executions = execs.Executions; - } - finally - { - _isLoading = false; - } - } - - private void ShowSqlModal(string title, string sql) - { - _sqlModalTitle = $"{title} - {_selectedPipeline}"; - _sqlModalContent = sql; - _showSqlModal = true; - } - - 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"; - } -} -``` - -**Step 3: Build** - -```bash -dotnet build src/JdeScoping.Client -``` - -**Step 4: Commit** - -```bash -git add src/JdeScoping.Client/Pages/Admin/ -git commit -m "feat(client): add PipelineViewer page" -``` - ---- - -## Task 11: Register Services and Build Full Solution - -**Files:** -- Modify: `src/JdeScoping.Client/Program.cs` - -**Step 1: Register PipelineApiClient** - -Add to Program.cs DI section: - -```csharp -builder.Services.AddScoped(); -``` - -**Step 2: Build full solution** - -```bash -dotnet build -``` - -**Step 3: Run tests** - -```bash -dotnet test -``` - -**Step 4: Commit** - -```bash -git add src/JdeScoping.Client/Program.cs -git commit -m "feat(client): register PipelineApiClient in DI" -``` - ---- - -## Task 12: Add Navigation Link (Optional) - -**Files:** -- Modify: `src/JdeScoping.Client/Layout/MainLayout.razor` (or NavMenu) - -**Step 1: Add link to admin section** - -Find navigation and add: - -```razor - -``` - -**Step 2: Build and test manually** - -```bash -dotnet run --project src/JdeScoping.Host -``` - -Navigate to `/admin/pipeline-viewer` in browser. - -**Step 3: Commit** - -```bash -git add src/JdeScoping.Client/ -git commit -m "feat(client): add pipeline viewer to navigation" -``` - ---- - -## Final Verification - -```bash -# Build -dotnet build - -# Run tests -dotnet test - -# Start application -dotnet run --project src/JdeScoping.Host -``` - -Navigate to `/admin/pipeline-viewer`, select a pipeline, and verify: -- Pipeline list loads -- Status summary shows -- Execution history shows -- Configuration cards display -- Schedule sections show with Default/Override indicators -- SQL modal works with copy button diff --git a/NEW/docs/plans/2026-01-21-remove-pipeline-viewer.md b/NEW/docs/plans/2026-01-21-remove-pipeline-viewer.md new file mode 100644 index 0000000..27383e9 --- /dev/null +++ b/NEW/docs/plans/2026-01-21-remove-pipeline-viewer.md @@ -0,0 +1,329 @@ +# Remove Pipeline Viewer Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Remove the pipeline viewer feature from the web UI and clean up all supporting code (controllers, DTOs, client services, routes). + +**Architecture:** Delete files in dependency order (DTOs/interfaces first would break compilation), so we delete leaf components first (views, client), then API layer, then shared contracts/DTOs. Modify DI registrations and navigation. + +**Tech Stack:** Blazor WebAssembly, ASP.NET Core API, C# + +--- + +## Task 1: Remove Client Components (Blazor Pages) + +**Files:** +- Delete: `src/JdeScoping.Client/Pages/Admin/PipelineViewer.razor` +- Delete: `src/JdeScoping.Client/Components/Admin/PipelineScheduleSection.razor` +- Delete: `src/JdeScoping.Client/Components/Admin/SqlQueryModal.razor` + +**Step 1: Delete PipelineViewer.razor** + +```bash +rm src/JdeScoping.Client/Pages/Admin/PipelineViewer.razor +``` + +**Step 2: Delete PipelineScheduleSection.razor** + +```bash +rm src/JdeScoping.Client/Components/Admin/PipelineScheduleSection.razor +``` + +**Step 3: Delete SqlQueryModal.razor (if exists)** + +```bash +rm -f src/JdeScoping.Client/Components/Admin/SqlQueryModal.razor +``` + +**Step 4: Verify files deleted** + +```bash +ls src/JdeScoping.Client/Pages/Admin/ | grep -i pipeline || echo "No pipeline pages remain" +ls src/JdeScoping.Client/Components/Admin/ | grep -i pipeline || echo "No pipeline components remain" +``` + +Expected: No pipeline-related files in Admin folders. + +--- + +## Task 2: Remove Navigation Link + +**Files:** +- Modify: `src/JdeScoping.Client/Layout/MainLayout.razor:16` + +**Step 1: Remove the pipeline viewer nav link** + +In `MainLayout.razor`, delete this line (around line 16): +```razor +Pipeline Viewer +``` + +**Step 2: Verify the change** + +```bash +grep -n "pipeline-viewer" src/JdeScoping.Client/Layout/MainLayout.razor || echo "Nav link removed" +``` + +Expected: "Nav link removed" + +--- + +## Task 3: Remove Client Service + +**Files:** +- Delete: `src/JdeScoping.Client/Services/PipelineApiClient.cs` +- Modify: `src/JdeScoping.Client/Program.cs:58` + +**Step 1: Delete PipelineApiClient.cs** + +```bash +rm src/JdeScoping.Client/Services/PipelineApiClient.cs +``` + +**Step 2: Remove DI registration from Program.cs** + +In `src/JdeScoping.Client/Program.cs`, delete this line (around line 58): +```csharp +builder.Services.AddScoped(); +``` + +**Step 3: Verify the changes** + +```bash +grep -n "PipelineApiClient" src/JdeScoping.Client/Program.cs || echo "DI registration removed" +ls src/JdeScoping.Client/Services/PipelineApiClient.cs 2>/dev/null || echo "File deleted" +``` + +Expected: Both checks show removal confirmed. + +--- + +## Task 4: Remove API Controller and Mapper + +**Files:** +- Delete: `src/JdeScoping.Api/Controllers/PipelineController.cs` +- Delete: `src/JdeScoping.Api/Mapping/PipelineMapper.cs` +- Delete: `src/JdeScoping.Api/Mapping/IPipelineMapper.cs` +- Modify: `src/JdeScoping.Api/DependencyInjection.cs:43` + +**Step 1: Delete PipelineController.cs** + +```bash +rm src/JdeScoping.Api/Controllers/PipelineController.cs +``` + +**Step 2: Delete PipelineMapper.cs** + +```bash +rm src/JdeScoping.Api/Mapping/PipelineMapper.cs +``` + +**Step 3: Delete IPipelineMapper.cs** + +```bash +rm src/JdeScoping.Api/Mapping/IPipelineMapper.cs +``` + +**Step 4: Remove DI registration from DependencyInjection.cs** + +In `src/JdeScoping.Api/DependencyInjection.cs`, delete this line (around line 43): +```csharp +services.AddSingleton(); +``` + +**Step 5: Verify the changes** + +```bash +ls src/JdeScoping.Api/Controllers/PipelineController.cs 2>/dev/null || echo "Controller deleted" +ls src/JdeScoping.Api/Mapping/*Pipeline* 2>/dev/null || echo "Mapper files deleted" +grep -n "PipelineMapper" src/JdeScoping.Api/DependencyInjection.cs || echo "DI registration removed" +``` + +Expected: All three checks confirm removal. + +--- + +## Task 5: Remove Core DTOs and Interface + +**Files:** +- Delete: `src/JdeScoping.Core/Models/Pipelines/` (entire folder) +- Delete: `src/JdeScoping.Core/ApiContracts/IPipelineApiClient.cs` + +**Step 1: Delete the Pipelines DTO folder** + +```bash +rm -rf src/JdeScoping.Core/Models/Pipelines/ +``` + +**Step 2: Delete IPipelineApiClient.cs** + +```bash +rm src/JdeScoping.Core/ApiContracts/IPipelineApiClient.cs +``` + +**Step 3: Verify deletions** + +```bash +ls src/JdeScoping.Core/Models/Pipelines/ 2>/dev/null || echo "Pipelines folder deleted" +ls src/JdeScoping.Core/ApiContracts/IPipelineApiClient.cs 2>/dev/null || echo "Interface deleted" +``` + +Expected: Both checks confirm deletion. + +--- + +## Task 6: Remove API Routes + +**Files:** +- Modify: `src/JdeScoping.Core/ApiContracts/ApiRoutes.cs:155-188` + +**Step 1: Remove the Pipelines class from ApiRoutes.cs** + +Delete the entire `Pipelines` static class (lines 155-188): +```csharp + /// + /// Routes for pipeline configuration API endpoints. + /// + public static class Pipelines + { + /// Base route for pipeline endpoints. + public const string Base = "api/pipelines"; + + /// Route template for getting a pipeline by name. + public const string ByName = "{name}"; + + /// Route template for getting pipeline status. + public const string Status = "{name}/status"; + + /// Route template for getting pipeline executions. + public const string Executions = "{name}/executions"; + + /// Builds the route to get a specific pipeline config. + /// The pipeline name to URL-encode. + /// The formatted route. + public static string GetByName(string name) => $"api/pipelines/{Uri.EscapeDataString(name)}"; + + /// Builds the route to get pipeline status. + /// The pipeline name to URL-encode. + /// The formatted route. + public static string GetStatus(string name) => $"api/pipelines/{Uri.EscapeDataString(name)}/status"; + + /// Builds the route to get pipeline executions. + /// The pipeline name to URL-encode. + /// The number of recent executions to retrieve. + /// The formatted route. + public static string GetExecutions(string name, int count = 10) => + $"api/pipelines/{Uri.EscapeDataString(name)}/executions?count={count}"; + } +``` + +**Step 2: Verify the change** + +```bash +grep -n "Pipelines" src/JdeScoping.Core/ApiContracts/ApiRoutes.cs || echo "Pipelines routes removed" +``` + +Expected: "Pipelines routes removed" + +--- + +## Task 7: Delete Plan Documents + +**Files:** +- Delete: `docs/plans/2026-01-07-pipeline-viewer-design.md` +- Delete: `docs/plans/2026-01-07-pipeline-viewer-implementation.md` + +**Step 1: Delete design document** + +```bash +rm docs/plans/2026-01-07-pipeline-viewer-design.md +``` + +**Step 2: Delete implementation plan** + +```bash +rm docs/plans/2026-01-07-pipeline-viewer-implementation.md +``` + +**Step 3: Verify deletions** + +```bash +ls docs/plans/*pipeline-viewer* 2>/dev/null || echo "Plan documents deleted" +``` + +Expected: "Plan documents deleted" + +--- + +## Task 8: Build and Verify + +**Step 1: Build the solution** + +```bash +cd /Users/dohertj2/Desktop/JdeScopingTool/NEW && dotnet build +``` + +Expected: Build succeeds with no errors. + +**Step 2: Search for any remaining pipeline viewer references** + +```bash +grep -r "PipelineViewer\|PipelineApiClient\|IPipelineApiClient\|PipelineController\|IPipelineMapper\|PipelineMapper\|pipeline-viewer" src/ --include="*.cs" --include="*.razor" || echo "No references remain" +``` + +Expected: "No references remain" + +**Step 3: Run tests** + +```bash +dotnet test +``` + +Expected: All tests pass. + +--- + +## Task 9: Commit + +**Step 1: Stage all changes** + +```bash +git add -A +``` + +**Step 2: Commit** + +```bash +git commit -m "$(cat <<'EOF' +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. +EOF +)" +``` + +--- + +## Summary + +| Task | Action | Files Affected | +|------|--------|----------------| +| 1 | Delete Blazor components | 3 files deleted | +| 2 | Remove nav link | 1 file modified | +| 3 | Remove client service | 1 deleted, 1 modified | +| 4 | Remove API layer | 3 deleted, 1 modified | +| 5 | Remove Core DTOs | 5 files deleted | +| 6 | Remove API routes | 1 file modified | +| 7 | Delete plan docs | 2 files deleted | +| 8 | Build and verify | - | +| 9 | Commit | - | + +**Total:** 14 files deleted, 4 files modified diff --git a/NEW/src/JdeScoping.Api/Controllers/PipelineController.cs b/NEW/src/JdeScoping.Api/Controllers/PipelineController.cs deleted file mode 100644 index 45a0986..0000000 --- a/NEW/src/JdeScoping.Api/Controllers/PipelineController.cs +++ /dev/null @@ -1,144 +0,0 @@ -using JdeScoping.Api.Mapping; -using JdeScoping.Core.ApiContracts; -using JdeScoping.Core.Models.Pipelines; -using JdeScoping.Core.Models.Enums; -using JdeScoping.DataSync.Contracts; -using JdeScoping.DataSync.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace JdeScoping.Api.Controllers; - -/// -/// API endpoints for pipeline configuration and status. -/// -[Route(ApiRoutes.Pipelines.Base)] -[Authorize] -public class PipelineController : ApiControllerBase -{ - private readonly IEtlPipelineFactory _pipelineFactory; - private readonly IDataUpdateRepository _dataUpdateRepository; - private readonly IPipelineMapper _mapper; - - /// - /// Initializes a new instance of the class. - /// - /// The ETL pipeline factory. - /// The data update repository. - /// The pipeline mapper. - public PipelineController( - IEtlPipelineFactory pipelineFactory, - IDataUpdateRepository dataUpdateRepository, - IPipelineMapper mapper) - { - _pipelineFactory = pipelineFactory; - _dataUpdateRepository = dataUpdateRepository; - _mapper = mapper; - } - - /// - /// Gets list of all available pipeline names. - /// - [HttpGet] - public ActionResult GetPipelineNames() - { - var names = _pipelineFactory.GetAvailableTables() - .OrderBy(n => n) - .ToList(); - return Ok(new PipelineListResponse(names)); - } - - /// - /// Gets configuration for a specific pipeline. - /// - /// The pipeline name. - [HttpGet(ApiRoutes.Pipelines.ByName)] - public ActionResult GetPipeline(string name) - { - var config = _pipelineFactory.GetPipelineConfig(name); - if (config is null) - return NotFound(); - - var defaults = _pipelineFactory.GetScheduleDefaults(); - var dto = _mapper.MapToDto(name, config, defaults); - return Ok(dto); - } - - /// - /// Gets schedule status for a pipeline. - /// - /// The pipeline name. - /// The cancellation token. - [HttpGet(ApiRoutes.Pipelines.Status)] - public async Task> GetStatus( - string name, - CancellationToken cancellationToken) - { - var config = _pipelineFactory.GetPipelineConfig(name); - if (config is null) - return NotFound(); - - var tableName = config.Destination.Table; - var lastRuns = await _dataUpdateRepository.GetLastRunsAsync(tableName, cancellationToken); - var lastSuccessful = await _dataUpdateRepository.GetLastDataUpdatesAsync(cancellationToken); - var defaults = _pipelineFactory.GetScheduleDefaults(); - - var statuses = new List(); - foreach (var updateType in new[] { UpdateTypes.Mass, UpdateTypes.Daily, UpdateTypes.Hourly }) - { - var scheduleConfig = _mapper.GetScheduleConfig(config, updateType); - var interval = _mapper.GetEffectiveInterval(scheduleConfig, defaults, updateType); - - lastRuns.TryGetValue(updateType, out var lastRun); - var successKey = $"{tableName}_{(int)updateType}"; - lastSuccessful.TryGetValue(successKey, out var lastSuccess); - - var nextRequired = lastSuccess?.EndDt?.AddMinutes(interval); - var isOverdue = DataUpdateRepository.IsOverdue( - lastSuccess?.EndDt, tableName, updateType, null); - - statuses.Add(new PipelineScheduleStatusDto( - updateType, - lastRun?.StartDt, - lastRun?.WasSuccessful ?? false, - lastSuccess?.EndDt, - nextRequired, - isOverdue, - interval)); - } - - return Ok(new PipelineStatusResponse(statuses)); - } - - /// - /// Gets recent execution history for a pipeline. - /// - /// The pipeline name. - /// The maximum number of recent executions to retrieve. - /// The cancellation token. - [HttpGet(ApiRoutes.Pipelines.Executions)] - public async Task> GetExecutions( - string name, - [FromQuery] int count = 30, - CancellationToken cancellationToken = default) - { - var config = _pipelineFactory.GetPipelineConfig(name); - if (config is null) - return NotFound(); - - var tableName = config.Destination.Table; - var updates = await _dataUpdateRepository.GetRecentUpdatesAsync( - tableName, null, count, cancellationToken); - - var executions = updates.Select(u => new PipelineExecutionDto( - u.UpdateType, - u.StartDt, - u.EndDt == default ? null : u.EndDt, - u.EndDt == default ? null : u.EndDt - u.StartDt, - u.NumberRecords, - u.WasSuccessful - )).ToList(); - - return Ok(new PipelineExecutionsResponse(executions)); - } -} diff --git a/NEW/src/JdeScoping.Api/DependencyInjection.cs b/NEW/src/JdeScoping.Api/DependencyInjection.cs index 886b7c1..d7be958 100644 --- a/NEW/src/JdeScoping.Api/DependencyInjection.cs +++ b/NEW/src/JdeScoping.Api/DependencyInjection.cs @@ -1,6 +1,5 @@ using System.Text.Json.Serialization; using JdeScoping.Api.Hubs; -using JdeScoping.Api.Mapping; using JdeScoping.Api.Options; using JdeScoping.Api.Services; using JdeScoping.Core.Interfaces; @@ -39,9 +38,6 @@ public static class ApiDependencyInjection // Register TimeProvider for testability (allows mocking DateTime.UtcNow) services.AddSingleton(TimeProvider.System); - // Register mappers - services.AddSingleton(); - // Configure SignalR services.AddSignalR(); diff --git a/NEW/src/JdeScoping.Api/Mapping/IPipelineMapper.cs b/NEW/src/JdeScoping.Api/Mapping/IPipelineMapper.cs deleted file mode 100644 index 202e017..0000000 --- a/NEW/src/JdeScoping.Api/Mapping/IPipelineMapper.cs +++ /dev/null @@ -1,34 +0,0 @@ -using JdeScoping.Core.Models.Enums; -using JdeScoping.Core.Models.Pipelines; -using JdeScoping.DataSync.Configuration; - -namespace JdeScoping.Api.Mapping; - -/// -/// Mapper for pipeline configuration to DTOs. -/// -public interface IPipelineMapper -{ - /// - /// Maps a pipeline configuration to its DTO representation. - /// - /// The pipeline name. - /// The pipeline configuration. - /// The default schedule settings. - PipelineConfigDto MapToDto(string name, PipelineConfig config, ScheduleDefaults defaults); - - /// - /// Gets the effective interval for a schedule, applying defaults if not specified. - /// - /// The schedule configuration, or null to use defaults. - /// The default schedule settings. - /// The type of update to get the interval for. - int GetEffectiveInterval(ScheduleConfig? config, ScheduleDefaults defaults, UpdateTypes updateType); - - /// - /// Gets the schedule configuration for a specific update type. - /// - /// The pipeline configuration. - /// The type of update to get the configuration for. - ScheduleConfig? GetScheduleConfig(PipelineConfig config, UpdateTypes updateType); -} diff --git a/NEW/src/JdeScoping.Api/Mapping/PipelineMapper.cs b/NEW/src/JdeScoping.Api/Mapping/PipelineMapper.cs deleted file mode 100644 index 657e3f0..0000000 --- a/NEW/src/JdeScoping.Api/Mapping/PipelineMapper.cs +++ /dev/null @@ -1,108 +0,0 @@ -using JdeScoping.Core.Models.Enums; -using JdeScoping.Core.Models.Pipelines; -using JdeScoping.DataSync.Configuration; - -namespace JdeScoping.Api.Mapping; - -/// -/// Maps pipeline configuration to DTOs. -/// -public class PipelineMapper : IPipelineMapper -{ - /// - public PipelineConfigDto MapToDto( - string name, - PipelineConfig config, - ScheduleDefaults defaults) - { - var source = new PipelineSourceDto( - config.Source.Connection, - Truncate(config.Source.Query), - Truncate(config.Source.MassQuery), - config.Source.Query, - config.Source.MassQuery, - config.Source.Parameters?.Select(p => new PipelineParameterDto( - p.Key, p.Value.Format, p.Value.Source)).ToList() ?? []); - - var matchCols = config.Destination.MatchColumns?.ToList(); - var destination = new PipelineDestinationDto( - config.Destination.Table, - matchCols?.Count > 0 ? "BulkMerge" : "BulkImport", - matchCols, - config.Destination.ExcludeFromUpdate?.ToList()); - - // Mass uses massQuery with no parameters; Daily/Hourly use query with parameters - var parameters = config.Source.Parameters?.Select(p => new PipelineParameterDto( - p.Key, p.Value.Format, p.Value.Source)).ToList() ?? []; - - var schedules = new PipelineSchedulesDto( - MapSchedule(config.Schedules?.Mass, defaults.Mass, config.Source.MassQuery, [], config.PreScripts, config.PostScripts), - MapSchedule(config.Schedules?.Daily, defaults.Daily, config.Source.Query, parameters, config.PreScripts, config.PostScripts), - MapSchedule(config.Schedules?.Hourly, defaults.Hourly, config.Source.Query, parameters, config.PreScripts, config.PostScripts)); - - return new PipelineConfigDto( - name, - source, - destination, - schedules, - config.PreScripts?.Count ?? 0, - config.PostScripts?.Count ?? 0, - config.PreScripts, - config.PostScripts); - } - - /// - public ScheduleConfig? GetScheduleConfig( - PipelineConfig config, - UpdateTypes updateType) => updateType switch - { - UpdateTypes.Mass => config.Schedules?.Mass, - UpdateTypes.Daily => config.Schedules?.Daily, - UpdateTypes.Hourly => config.Schedules?.Hourly, - _ => null - }; - - /// - public int GetEffectiveInterval( - ScheduleConfig? config, - ScheduleDefaults defaults, - UpdateTypes updateType) - { - if (config?.IntervalMinutes > 0) - return config.IntervalMinutes; - - return updateType switch - { - UpdateTypes.Mass => defaults.Mass.IntervalMinutes, - UpdateTypes.Daily => defaults.Daily.IntervalMinutes, - UpdateTypes.Hourly => defaults.Hourly.IntervalMinutes, - _ => 60 - }; - } - - private static PipelineScheduleDto MapSchedule( - ScheduleConfig? config, - ScheduleConfig defaults, - string? query, - List parameters, - List? preScripts, - List? postScripts) - { - return new PipelineScheduleDto( - config?.Enabled ?? defaults.Enabled, - config?.IntervalMinutes > 0 ? config.IntervalMinutes : defaults.IntervalMinutes, - config?.PrePurge ?? defaults.PrePurge, - config?.ReIndex ?? defaults.ReIndex, - config?.IntervalMinutes > 0 && config.IntervalMinutes != defaults.IntervalMinutes, - config?.PrePurge != null && config.PrePurge != defaults.PrePurge, - config?.ReIndex != null && config.ReIndex != defaults.ReIndex, - query, - parameters, - preScripts, - postScripts); - } - - private static string? Truncate(string? value, int maxLength = 100) => - value is null ? null : - value.Length <= maxLength ? value : value[..maxLength] + "..."; -} diff --git a/NEW/src/JdeScoping.Client/Components/Admin/PipelineScheduleSection.razor b/NEW/src/JdeScoping.Client/Components/Admin/PipelineScheduleSection.razor deleted file mode 100644 index f50ef28..0000000 --- a/NEW/src/JdeScoping.Client/Components/Admin/PipelineScheduleSection.razor +++ /dev/null @@ -1,116 +0,0 @@ -@namespace JdeScoping.Client.Components.Admin -@using JdeScoping.Core.Models.Pipelines -@using JdeScoping.Core.Models.Enums -@using JdeScoping.Client.Helpers - - - - - - @GetScheduleTypeName(ScheduleType) Refresh - - - - @if (Config?.Enabled == true) - { - - } - else - { - - } - - - - @if (Config is not null) - { - Schedule Settings - - - - - - - - - - - - - - - - - - - - - - - - - -
SettingValueSource
Interval@FormatInterval(Config.IntervalMinutes)@(Config.IntervalIsOverride ? "Override" : "Default")
Pre-Purge@(Config.PrePurge ? "Yes" : "No")@(Config.PrePurgeIsOverride ? "Override" : "Default")
Re-Index@(Config.ReIndex ? "Yes" : "No")@(Config.ReIndexIsOverride ? "Override" : "Default")
- - @if (Config.Parameters?.Count > 0) - { - Parameters -
    - @foreach (var param in Config.Parameters) - { -
  • @param.Name: @(param.Format ?? "default") (source: @param.Source)
  • - } -
- } - - @if (!string.IsNullOrWhiteSpace(Config.Query)) - { - Query -
@SqlFormatHelper.FormatSql(Config.Query)
- } - - @if (Config.PreScripts?.Count > 0) - { - Pre-Scripts (@Config.PreScripts.Count) - @for (int i = 0; i < Config.PreScripts.Count; i++) - { - var script = Config.PreScripts[i]; - Script @(i + 1): -
@SqlFormatHelper.FormatSql(script)
- } - } - - @if (Config.PostScripts?.Count > 0) - { - Post-Scripts (@Config.PostScripts.Count) - @for (int i = 0; i < Config.PostScripts.Count; i++) - { - var script = Config.PostScripts[i]; - Script @(i + 1): -
@SqlFormatHelper.FormatSql(script)
- } - } - } -
- -@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"; - } -} diff --git a/NEW/src/JdeScoping.Client/Components/Admin/SqlQueryModal.razor b/NEW/src/JdeScoping.Client/Components/Admin/SqlQueryModal.razor deleted file mode 100644 index 48d87ec..0000000 --- a/NEW/src/JdeScoping.Client/Components/Admin/SqlQueryModal.razor +++ /dev/null @@ -1,101 +0,0 @@ -@namespace JdeScoping.Client.Components.Admin -@using JdeScoping.Client.Helpers -@inject IJSRuntime JS - -@if (Visible) -{ -
-
-
- @Title - -
-
-
@FormattedSql
-
- -
-
-} - - - -@code { - [Parameter] public bool Visible { get; set; } - [Parameter] public EventCallback 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); - } -} diff --git a/NEW/src/JdeScoping.Client/Layout/MainLayout.razor b/NEW/src/JdeScoping.Client/Layout/MainLayout.razor index 5e76e26..c78b451 100644 --- a/NEW/src/JdeScoping.Client/Layout/MainLayout.razor +++ b/NEW/src/JdeScoping.Client/Layout/MainLayout.razor @@ -13,7 +13,6 @@ New Search Search Queue Refresh Status - Pipeline Viewer