diff --git a/src/Trax.Dashboard/Components/Pages/Data/DeadLettersPage.razor b/src/Trax.Dashboard/Components/Pages/Data/DeadLettersPage.razor index 213a566..e42c11e 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/DeadLettersPage.razor +++ b/src/Trax.Dashboard/Components/Pages/Data/DeadLettersPage.razor @@ -18,14 +18,14 @@ ButtonStyle="ButtonStyle.Primary" Variant="Variant.Outlined" Size="ButtonSize.Small" - Disabled="@(_batchOperating)" + Disabled="@(BatchOperating)" Click="@RequeueAll" /> @if (_selectedDeadLetters.Count > 0) { @@ -33,13 +33,13 @@ Icon="replay" ButtonStyle="ButtonStyle.Primary" Size="ButtonSize.Small" - Disabled="@(_batchOperating)" + Disabled="@(BatchOperating)" Click="@RequeueSelected" /> } @@ -54,7 +54,7 @@ } -@if (_batchError is not null) +@if (BatchError is not null) { - @_batchError + @BatchError } ? _grid; private IList _selectedDeadLetters = new List(); - private bool _batchOperating; private bool _showBatchAcknowledgeInput; private bool _acknowledgeSelectedOnly; private string _batchAcknowledgeNote = ""; - private string? _batchError; protected override async Task LoadDataAsync(CancellationToken cancellationToken) { @@ -161,112 +159,45 @@ await _grid.ReloadAsync(); } - private async Task> LoadPageAsync( - LoadDataArgs args, - CancellationToken cancellationToken) - { - using var context = await DataContextFactory.CreateDbContextAsync(cancellationToken); - IQueryable query = context.DeadLetters.AsNoTracking(); - - if (!string.IsNullOrEmpty(args.Filter)) - query = query.Where(args.Filter); - - if (!string.IsNullOrEmpty(args.OrderBy)) - query = query.OrderBy(args.OrderBy); - else - query = query.OrderByDescending(d => d.Id); + private Task> LoadPageAsync(LoadDataArgs args, CancellationToken ct) + => DataGridQueryHelper.LoadPageAsync( + DataContextFactory, + db => db.DeadLetters.AsNoTracking().OrderByDescending(d => d.Id), + args, + ct + ); - var count = await query.CountAsync(cancellationToken); - - if (args.Skip.HasValue) - query = query.Skip(args.Skip.Value); - if (args.Top.HasValue) - query = query.Take(args.Top.Value); - - var items = await query.ToListAsync(cancellationToken); - return new ServerDataResult(items, count); - } - - private async Task RequeueAll() + private Task RequeueAll() => RunBatchOperationAsync(async () => { - _batchError = null; - _batchOperating = true; - - try - { - var result = await Scheduler.RequeueAllDeadLettersAsync(); + var result = await Scheduler.RequeueAllDeadLettersAsync(); + NotificationService.Notify(NotificationSeverity.Success, "Batch Requeue", result.Message, duration: 4000); + }, () => _selectedDeadLetters.Clear()); - NotificationService.Notify( - NotificationSeverity.Success, - "Batch Requeue", - result.Message, - duration: 4000); - - _selectedDeadLetters.Clear(); - PausePolling = false; - await LoadDataAsync(DisposalToken); - } - catch (Exception ex) { _batchError = ex.Message; } - finally { _batchOperating = false; } - } + private Task RequeueSelected() => RunBatchOperationAsync(async () => + { + var ids = _selectedDeadLetters.Select(d => d.Id).ToArray(); + var result = await Scheduler.RequeueDeadLettersAsync(ids); + NotificationService.Notify(NotificationSeverity.Success, "Batch Requeue", result.Message, duration: 4000); + }, () => _selectedDeadLetters.Clear()); - private async Task RequeueSelected() + private Task AcknowledgeBatch() => RunBatchOperationAsync(async () => { - _batchError = null; - _batchOperating = true; + BatchDeadLetterResult result; - try + if (_acknowledgeSelectedOnly) { var ids = _selectedDeadLetters.Select(d => d.Id).ToArray(); - var result = await Scheduler.RequeueDeadLettersAsync(ids); - - NotificationService.Notify( - NotificationSeverity.Success, - "Batch Requeue", - result.Message, - duration: 4000); - - _selectedDeadLetters.Clear(); - PausePolling = false; - await LoadDataAsync(DisposalToken); + result = await Scheduler.AcknowledgeDeadLettersAsync(ids, _batchAcknowledgeNote); } - catch (Exception ex) { _batchError = ex.Message; } - finally { _batchOperating = false; } - } - - private async Task AcknowledgeBatch() - { - _batchError = null; - _batchOperating = true; - - try + else { - BatchDeadLetterResult result; - - if (_acknowledgeSelectedOnly) - { - var ids = _selectedDeadLetters.Select(d => d.Id).ToArray(); - result = await Scheduler.AcknowledgeDeadLettersAsync(ids, _batchAcknowledgeNote); - } - else - { - result = await Scheduler.AcknowledgeAllDeadLettersAsync(_batchAcknowledgeNote); - } + result = await Scheduler.AcknowledgeAllDeadLettersAsync(_batchAcknowledgeNote); + } - NotificationService.Notify( - NotificationSeverity.Success, - "Batch Acknowledge", - result.Message, - duration: 4000); + NotificationService.Notify(NotificationSeverity.Success, "Batch Acknowledge", result.Message, duration: 4000); - _showBatchAcknowledgeInput = false; - _acknowledgeSelectedOnly = false; - _batchAcknowledgeNote = ""; - _selectedDeadLetters.Clear(); - PausePolling = false; - await LoadDataAsync(DisposalToken); - } - catch (Exception ex) { _batchError = ex.Message; } - finally { _batchOperating = false; } - } + _showBatchAcknowledgeInput = false; + _acknowledgeSelectedOnly = false; + _batchAcknowledgeNote = ""; + }, () => _selectedDeadLetters.Clear()); } diff --git a/src/Trax.Dashboard/Components/Pages/Data/LogsPage.razor b/src/Trax.Dashboard/Components/Pages/Data/LogsPage.razor index 7698a57..186cdf9 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/LogsPage.razor +++ b/src/Trax.Dashboard/Components/Pages/Data/LogsPage.razor @@ -49,29 +49,11 @@ await _grid.ReloadAsync(); } - private async Task> LoadPageAsync( - LoadDataArgs args, - CancellationToken cancellationToken) - { - using var context = await DataContextFactory.CreateDbContextAsync(cancellationToken); - IQueryable query = context.Logs.AsNoTracking(); - - if (!string.IsNullOrEmpty(args.Filter)) - query = query.Where(args.Filter); - - if (!string.IsNullOrEmpty(args.OrderBy)) - query = query.OrderBy(args.OrderBy); - else - query = query.OrderByDescending(l => l.Id); - - var count = await query.CountAsync(cancellationToken); - - if (args.Skip.HasValue) - query = query.Skip(args.Skip.Value); - if (args.Top.HasValue) - query = query.Take(args.Top.Value); - - var items = await query.ToListAsync(cancellationToken); - return new ServerDataResult(items, count); - } + private Task> LoadPageAsync(LoadDataArgs args, CancellationToken ct) + => DataGridQueryHelper.LoadPageAsync( + DataContextFactory, + db => db.Logs.AsNoTracking().OrderByDescending(l => l.Id), + args, + ct + ); } diff --git a/src/Trax.Dashboard/Components/Pages/Data/ManifestGroupDetailPage.razor.cs b/src/Trax.Dashboard/Components/Pages/Data/ManifestGroupDetailPage.razor.cs index b055096..e2db49c 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/ManifestGroupDetailPage.razor.cs +++ b/src/Trax.Dashboard/Components/Pages/Data/ManifestGroupDetailPage.razor.cs @@ -11,7 +11,6 @@ using Trax.Effect.Models.Manifest; using Trax.Effect.Models.ManifestGroup; using Trax.Effect.Models.Metadata; -using Trax.Scheduler.Services.CancellationRegistry; using Trax.Scheduler.Services.TraxScheduler; using static Trax.Dashboard.Utilities.DashboardFormatters; @@ -412,7 +411,6 @@ private async Task CancelAllRunning() .Manifests.Where(m => m.ManifestGroupId == ManifestGroupId) .Select(m => m.Id); - // Get IDs of in-progress metadata for this group var inProgressIds = await context .Metadatas.AsNoTracking() .Where(m => @@ -434,26 +432,17 @@ private async Task CancelAllRunning() return; } - // Batch set cancel_requested = true - await context - .Metadatas.Where(m => inProgressIds.Contains(m.Id)) - .ExecuteUpdateAsync( - s => s.SetProperty(m => m.CancellationRequested, true), - DisposalToken - ); - - // Same-server instant cancel bonus - var registry = ServiceProvider.GetService(); - if (registry is not null) - { - foreach (var id in inProgressIds) - registry.TryCancel(id); - } + var count = await CancellationHelper.CancelTrainsAsync( + DataContextFactory, + ServiceProvider, + inProgressIds, + DisposalToken + ); NotificationService.Notify( NotificationSeverity.Success, "Cancellation Requested", - $"Cancel signal sent for {inProgressIds.Count} train(s).", + $"Cancel signal sent for {count} train(s).", duration: 4000 ); } diff --git a/src/Trax.Dashboard/Components/Pages/Data/ManifestGroupsPage.razor b/src/Trax.Dashboard/Components/Pages/Data/ManifestGroupsPage.razor index ce03d59..a164438 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/ManifestGroupsPage.razor +++ b/src/Trax.Dashboard/Components/Pages/Data/ManifestGroupsPage.razor @@ -1,6 +1,5 @@ @page "/trax/data/manifest-groups" @using Trax.Scheduler.Services.TraxScheduler -@using Trax.Scheduler.Services.CancellationRegistry @using Microsoft.Extensions.DependencyInjection @inherits PollingComponentBase @inject IDataContextProviderFactory DataContextFactory @@ -42,35 +41,35 @@ Icon="play_arrow" ButtonStyle="ButtonStyle.Primary" Size="ButtonSize.Small" - Disabled="@(_operating)" + Disabled="@(BatchOperating)" Click="@TriggerSelected" /> } -@if (_batchError is not null) +@if (BatchError is not null) { - @_batchError + @BatchError } @if (_dagLayout is not null && _dagLayout.Nodes.Count > 0) @@ -155,7 +154,6 @@ private HashSet _selectedGroups = []; private DagLayout? _dagLayout; private bool _operating; - private string? _batchError; protected override async Task LoadDataAsync(CancellationToken cancellationToken) { @@ -256,89 +254,55 @@ } } - private async Task SetSelectedEnabled(bool enabled) + private Task SetSelectedEnabled(bool enabled) => RunBatchOperationAsync(async () => { - _batchError = null; - _operating = true; - - try - { - var ids = _selectedGroups.Select(g => g.Id).ToList(); - - using var context = await DataContextFactory.CreateDbContextAsync(DisposalToken); - var count = await context.ManifestGroups - .Where(g => ids.Contains(g.Id)) - .ExecuteUpdateAsync( - s => s - .SetProperty(g => g.IsEnabled, enabled) - .SetProperty(g => g.UpdatedAt, DateTime.UtcNow), - DisposalToken); + var ids = _selectedGroups.Select(g => g.Id).ToList(); - NotificationService.Notify( - NotificationSeverity.Success, - enabled ? "Groups Enabled" : "Groups Disabled", - $"{count} group(s) {(enabled ? "enabled" : "disabled")}.", - duration: 4000); + using var context = await DataContextFactory.CreateDbContextAsync(DisposalToken); + var count = await context.ManifestGroups + .Where(g => ids.Contains(g.Id)) + .ExecuteUpdateAsync( + s => s + .SetProperty(g => g.IsEnabled, enabled) + .SetProperty(g => g.UpdatedAt, DateTime.UtcNow), + DisposalToken); - _selectedGroups.Clear(); - PausePolling = false; - await RefreshNowAsync(); - } - catch (Exception ex) { _batchError = ex.Message; } - finally { _operating = false; } - } + NotificationService.Notify( + NotificationSeverity.Success, + enabled ? "Groups Enabled" : "Groups Disabled", + $"{count} group(s) {(enabled ? "enabled" : "disabled")}.", + duration: 4000); + }, () => _selectedGroups.Clear()); - private async Task TriggerSelected() + private Task TriggerSelected() => RunBatchOperationAsync(async () => { - _batchError = null; - _operating = true; - - try + var totalQueued = 0; + foreach (var group in _selectedGroups) { - var totalQueued = 0; - foreach (var group in _selectedGroups) - { - totalQueued += await Scheduler.TriggerGroupAsync(group.Id); - } - - NotificationService.Notify( - NotificationSeverity.Success, - "Groups Triggered", - $"{totalQueued} manifest(s) queued across {_selectedGroups.Count} group(s).", - duration: 4000); - - _selectedGroups.Clear(); - PausePolling = false; + totalQueued += await Scheduler.TriggerGroupAsync(group.Id); } - catch (Exception ex) { _batchError = ex.Message; } - finally { _operating = false; } - } - private async Task CancelSelectedRunning() - { - _batchError = null; - _operating = true; + NotificationService.Notify( + NotificationSeverity.Success, + "Groups Triggered", + $"{totalQueued} manifest(s) queued across {_selectedGroups.Count} group(s).", + duration: 4000); + }, () => _selectedGroups.Clear()); - try + private Task CancelSelectedRunning() => RunBatchOperationAsync(async () => + { + var totalCancelled = 0; + foreach (var group in _selectedGroups) { - var totalCancelled = 0; - foreach (var group in _selectedGroups) - { - totalCancelled += await Scheduler.CancelGroupAsync(group.Id); - } - - NotificationService.Notify( - NotificationSeverity.Success, - "Cancellation Requested", - $"Cancel signal sent for {totalCancelled} execution(s) across {_selectedGroups.Count} group(s).", - duration: 4000); - - _selectedGroups.Clear(); - PausePolling = false; + totalCancelled += await Scheduler.CancelGroupAsync(group.Id); } - catch (Exception ex) { _batchError = ex.Message; } - finally { _operating = false; } - } + + NotificationService.Notify( + NotificationSeverity.Success, + "Cancellation Requested", + $"Cancel signal sent for {totalCancelled} execution(s) across {_selectedGroups.Count} group(s).", + duration: 4000); + }, () => _selectedGroups.Clear()); private void OnDagNodeClick(long groupId) { diff --git a/src/Trax.Dashboard/Components/Pages/Data/ManifestsPage.razor b/src/Trax.Dashboard/Components/Pages/Data/ManifestsPage.razor index 028b8f8..42ba16a 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/ManifestsPage.razor +++ b/src/Trax.Dashboard/Components/Pages/Data/ManifestsPage.razor @@ -18,28 +18,28 @@ Icon="play_arrow" ButtonStyle="ButtonStyle.Primary" Size="ButtonSize.Small" - Disabled="@(_batchOperating)" + Disabled="@(BatchOperating)" Click="@TriggerSelected" /> } -@if (_batchError is not null) +@if (BatchError is not null) { - @_batchError + @BatchError } _items = []; private HashSet _selectedManifests = []; - private bool _batchOperating; - private string? _batchError; protected override async Task LoadDataAsync(CancellationToken cancellationToken) { @@ -144,60 +142,37 @@ _items = await query.ToListAsync(cancellationToken); } - private async Task TriggerSelected() + private Task TriggerSelected() => RunBatchOperationAsync(async () => { - _batchError = null; - _batchOperating = true; - - try + var count = 0; + foreach (var manifest in _selectedManifests) { - var count = 0; - foreach (var manifest in _selectedManifests) - { - await Scheduler.TriggerAsync(manifest.ExternalId); - count++; - } - - NotificationService.Notify( - NotificationSeverity.Success, - "Batch Trigger", - $"{count} manifest(s) triggered.", - duration: 4000); - - _selectedManifests.Clear(); - PausePolling = false; + await Scheduler.TriggerAsync(manifest.ExternalId); + count++; } - catch (Exception ex) { _batchError = ex.Message; } - finally { _batchOperating = false; } - } - - private async Task SetSelectedEnabled(bool enabled) - { - _batchError = null; - _batchOperating = true; - try - { - var ids = _selectedManifests.Select(m => m.Id).ToList(); - - using var context = await DataContextFactory.CreateDbContextAsync(DisposalToken); - var count = await context.Manifests - .Where(m => ids.Contains(m.Id)) - .ExecuteUpdateAsync( - s => s.SetProperty(m => m.IsEnabled, enabled), - DisposalToken); - - NotificationService.Notify( - NotificationSeverity.Success, - enabled ? "Manifests Enabled" : "Manifests Disabled", - $"{count} manifest(s) {(enabled ? "enabled" : "disabled")}.", - duration: 4000); + NotificationService.Notify( + NotificationSeverity.Success, + "Batch Trigger", + $"{count} manifest(s) triggered.", + duration: 4000); + }, () => _selectedManifests.Clear()); - _selectedManifests.Clear(); - PausePolling = false; - await LoadDataAsync(DisposalToken); - } - catch (Exception ex) { _batchError = ex.Message; } - finally { _batchOperating = false; } - } + private Task SetSelectedEnabled(bool enabled) => RunBatchOperationAsync(async () => + { + var ids = _selectedManifests.Select(m => m.Id).ToList(); + + using var context = await DataContextFactory.CreateDbContextAsync(DisposalToken); + var count = await context.Manifests + .Where(m => ids.Contains(m.Id)) + .ExecuteUpdateAsync( + s => s.SetProperty(m => m.IsEnabled, enabled), + DisposalToken); + + NotificationService.Notify( + NotificationSeverity.Success, + enabled ? "Manifests Enabled" : "Manifests Disabled", + $"{count} manifest(s) {(enabled ? "enabled" : "disabled")}.", + duration: 4000); + }, () => _selectedManifests.Clear()); } diff --git a/src/Trax.Dashboard/Components/Pages/Data/MetadataDetailPage.razor.cs b/src/Trax.Dashboard/Components/Pages/Data/MetadataDetailPage.razor.cs index 9727695..3e119c4 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/MetadataDetailPage.razor.cs +++ b/src/Trax.Dashboard/Components/Pages/Data/MetadataDetailPage.razor.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Radzen; +using Trax.Dashboard.Utilities; using Trax.Effect.Data.Services.IDataContextFactory; using Trax.Effect.Enums; using Trax.Effect.Models.Log; @@ -11,7 +12,6 @@ using Trax.Effect.Models.WorkQueue.DTOs; using Trax.Effect.Utils; using Trax.Mediator.Services.TrainDiscovery; -using Trax.Scheduler.Services.CancellationRegistry; using static Trax.Dashboard.Utilities.DashboardFormatters; namespace Trax.Dashboard.Components.Pages.Data; @@ -72,17 +72,12 @@ private async Task CancelTrain() try { - // Always set DB flag (works cross-server) - using var context = await DataContextFactory.CreateDbContextAsync(DisposalToken); - await context - .Metadatas.Where(m => m.Id == MetadataId) - .ExecuteUpdateAsync( - s => s.SetProperty(m => m.CancellationRequested, true), - DisposalToken - ); - - // Same-server instant cancel bonus - ServiceProvider.GetService()?.TryCancel(MetadataId); + await CancellationHelper.CancelTrainsAsync( + DataContextFactory, + ServiceProvider, + [MetadataId], + DisposalToken + ); NotificationService.Notify( NotificationSeverity.Success, diff --git a/src/Trax.Dashboard/Components/Pages/Data/MetadataPage.razor b/src/Trax.Dashboard/Components/Pages/Data/MetadataPage.razor index c39f452..39d763b 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/MetadataPage.razor +++ b/src/Trax.Dashboard/Components/Pages/Data/MetadataPage.razor @@ -1,6 +1,5 @@ @page "/trax/data/metadata" @using System.Linq.Dynamic.Core -@using Trax.Scheduler.Services.CancellationRegistry @using Microsoft.Extensions.DependencyInjection @inherits PollingComponentBase @inject IDataContextProviderFactory DataContextFactory @@ -20,14 +19,14 @@ Icon="cancel" ButtonStyle="ButtonStyle.Warning" Size="ButtonSize.Small" - Disabled="@(_batchOperating)" + Disabled="@(BatchOperating)" Click="@CancelSelected" /> } -@if (_batchError is not null) +@if (BatchError is not null) { - @_batchError + @BatchError } ? _grid; private HashSet _selectedMetadata = []; - private bool _batchOperating; - private string? _batchError; private async Task CancelTrain(long metadataId, string trainName) { try { - using var context = await DataContextFactory.CreateDbContextAsync(DisposalToken); - await context - .Metadatas.Where(m => m.Id == metadataId) - .ExecuteUpdateAsync( - s => s.SetProperty(m => m.CancellationRequested, true), - DisposalToken - ); - - ServiceProvider.GetService()?.TryCancel(metadataId); + var count = await CancellationHelper.CancelTrainsAsync( + DataContextFactory, ServiceProvider, [metadataId], DisposalToken); NotificationService.Notify( NotificationSeverity.Success, @@ -175,51 +165,28 @@ } } - private async Task CancelSelected() + private Task CancelSelected() => RunBatchOperationAsync(async () => { - _batchError = null; - _batchOperating = true; + var inProgressIds = _selectedMetadata + .Where(m => m.TrainState == TrainState.InProgress) + .Select(m => m.Id) + .ToList(); - try + if (inProgressIds.Count == 0) { - var inProgressIds = _selectedMetadata - .Where(m => m.TrainState == TrainState.InProgress) - .Select(m => m.Id) - .ToList(); - - if (inProgressIds.Count == 0) - { - _batchError = "No in-progress executions selected. Only in-progress executions can be cancelled."; - return; - } - - using var context = await DataContextFactory.CreateDbContextAsync(DisposalToken); - var count = await context.Metadatas - .Where(m => inProgressIds.Contains(m.Id) && m.TrainState == TrainState.InProgress) - .ExecuteUpdateAsync( - s => s.SetProperty(m => m.CancellationRequested, true), - DisposalToken); + BatchError = "No in-progress executions selected. Only in-progress executions can be cancelled."; + return; + } - var registry = ServiceProvider.GetService(); - if (registry is not null) - { - foreach (var id in inProgressIds) - registry.TryCancel(id); - } + var count = await CancellationHelper.CancelTrainsAsync( + DataContextFactory, ServiceProvider, inProgressIds, DisposalToken); - NotificationService.Notify( - NotificationSeverity.Success, - "Batch Cancellation", - $"Cancel signal sent for {count} execution(s).", - duration: 4000); - - _selectedMetadata.Clear(); - PausePolling = false; - await LoadDataAsync(DisposalToken); - } - catch (Exception ex) { _batchError = ex.Message; } - finally { _batchOperating = false; } - } + NotificationService.Notify( + NotificationSeverity.Success, + "Batch Cancellation", + $"Cancel signal sent for {count} execution(s).", + duration: 4000); + }, () => _selectedMetadata.Clear()); protected override async Task LoadDataAsync(CancellationToken cancellationToken) { @@ -227,32 +194,19 @@ await _grid.ReloadAsync(); } - private async Task> LoadPageAsync( - LoadDataArgs args, - CancellationToken cancellationToken) - { - using var context = await DataContextFactory.CreateDbContextAsync(cancellationToken); - IQueryable query = context.Metadatas.AsNoTracking(); - - if (DashboardSettings.HideAdminTrains) - query = query.ExcludeAdmin(DashboardSettings.AdminTrainNames); - - if (!string.IsNullOrEmpty(args.Filter)) - query = query.Where(args.Filter); - - if (!string.IsNullOrEmpty(args.OrderBy)) - query = query.OrderBy(args.OrderBy); - else - query = query.OrderByDescending(m => m.Id); - - var count = await query.CountAsync(cancellationToken); + private Task> LoadPageAsync(LoadDataArgs args, CancellationToken ct) + => DataGridQueryHelper.LoadPageAsync( + DataContextFactory, + db => + { + IQueryable query = db.Metadatas.AsNoTracking(); - if (args.Skip.HasValue) - query = query.Skip(args.Skip.Value); - if (args.Top.HasValue) - query = query.Take(args.Top.Value); + if (DashboardSettings.HideAdminTrains) + query = query.ExcludeAdmin(DashboardSettings.AdminTrainNames); - var items = await query.ToListAsync(cancellationToken); - return new ServerDataResult(items, count); - } + return query.OrderByDescending(m => m.Id); + }, + args, + ct + ); } diff --git a/src/Trax.Dashboard/Components/Pages/Data/WorkQueueDetailPage.razor b/src/Trax.Dashboard/Components/Pages/Data/WorkQueueDetailPage.razor index 5c2f463..6e42c57 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/WorkQueueDetailPage.razor +++ b/src/Trax.Dashboard/Components/Pages/Data/WorkQueueDetailPage.razor @@ -59,7 +59,7 @@ - diff --git a/src/Trax.Dashboard/Components/Pages/Data/WorkQueueDetailPage.razor.cs b/src/Trax.Dashboard/Components/Pages/Data/WorkQueueDetailPage.razor.cs index 7816fb4..edbb83d 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/WorkQueueDetailPage.razor.cs +++ b/src/Trax.Dashboard/Components/Pages/Data/WorkQueueDetailPage.razor.cs @@ -83,13 +83,4 @@ private async Task CancelEntry() _cancelling = false; } } - - private static BadgeStyle GetStatusBadgeStyle(WorkQueueStatus status) => - status switch - { - WorkQueueStatus.Queued => BadgeStyle.Info, - WorkQueueStatus.Dispatched => BadgeStyle.Success, - WorkQueueStatus.Cancelled => BadgeStyle.Warning, - _ => BadgeStyle.Light, - }; } diff --git a/src/Trax.Dashboard/Components/Pages/Data/WorkQueuePage.razor b/src/Trax.Dashboard/Components/Pages/Data/WorkQueuePage.razor index 0b6a357..fab551c 100644 --- a/src/Trax.Dashboard/Components/Pages/Data/WorkQueuePage.razor +++ b/src/Trax.Dashboard/Components/Pages/Data/WorkQueuePage.razor @@ -17,14 +17,14 @@ Icon="cancel" ButtonStyle="ButtonStyle.Warning" Size="ButtonSize.Small" - Disabled="@(_batchOperating)" + Disabled="@(BatchOperating)" Click="@CancelSelected" /> } -@if (_batchError is not null) +@if (BatchError is not null) { - @_batchError + @BatchError } @@ -104,8 +104,6 @@ @code { private TraxDataGrid? _grid; private HashSet _selectedEntries = []; - private bool _batchOperating; - private string? _batchError; protected override async Task LoadDataAsync(CancellationToken cancellationToken) { @@ -113,77 +111,39 @@ await _grid.ReloadAsync(); } - private async Task CancelSelected() + private Task CancelSelected() => RunBatchOperationAsync(async () => { - _batchError = null; - _batchOperating = true; + var queuedIds = _selectedEntries + .Where(e => e.Status == WorkQueueStatus.Queued) + .Select(e => e.Id) + .ToList(); - try + if (queuedIds.Count == 0) { - var queuedIds = _selectedEntries - .Where(e => e.Status == WorkQueueStatus.Queued) - .Select(e => e.Id) - .ToList(); - - if (queuedIds.Count == 0) - { - _batchError = "No queued entries selected. Only queued entries can be cancelled."; - return; - } - - using var context = await DataContextFactory.CreateDbContextAsync(DisposalToken); - var count = await context.WorkQueues - .Where(wq => queuedIds.Contains(wq.Id) && wq.Status == WorkQueueStatus.Queued) - .ExecuteUpdateAsync( - s => s.SetProperty(wq => wq.Status, WorkQueueStatus.Cancelled), - DisposalToken); - - NotificationService.Notify( - NotificationSeverity.Success, - "Entries Cancelled", - $"{count} work queue entry(s) cancelled.", - duration: 4000); - - _selectedEntries.Clear(); - PausePolling = false; - await LoadDataAsync(DisposalToken); + BatchError = "No queued entries selected. Only queued entries can be cancelled."; + return; } - catch (Exception ex) { _batchError = ex.Message; } - finally { _batchOperating = false; } - } - - private async Task> LoadPageAsync( - LoadDataArgs args, - CancellationToken cancellationToken) - { - using var context = await DataContextFactory.CreateDbContextAsync(cancellationToken); - IQueryable query = context.WorkQueues.AsNoTracking(); - if (!string.IsNullOrEmpty(args.Filter)) - query = query.Where(args.Filter); + using var context = await DataContextFactory.CreateDbContextAsync(DisposalToken); + var count = await context.WorkQueues + .Where(wq => queuedIds.Contains(wq.Id) && wq.Status == WorkQueueStatus.Queued) + .ExecuteUpdateAsync( + s => s.SetProperty(wq => wq.Status, WorkQueueStatus.Cancelled), + DisposalToken); + + NotificationService.Notify( + NotificationSeverity.Success, + "Entries Cancelled", + $"{count} work queue entry(s) cancelled.", + duration: 4000); + }, () => _selectedEntries.Clear()); + + private Task> LoadPageAsync(LoadDataArgs args, CancellationToken ct) + => DataGridQueryHelper.LoadPageAsync( + DataContextFactory, + db => db.WorkQueues.AsNoTracking().OrderByDescending(q => q.Id), + args, + ct + ); - if (!string.IsNullOrEmpty(args.OrderBy)) - query = query.OrderBy(args.OrderBy); - else - query = query.OrderByDescending(q => q.Id); - - var count = await query.CountAsync(cancellationToken); - - if (args.Skip.HasValue) - query = query.Skip(args.Skip.Value); - if (args.Top.HasValue) - query = query.Take(args.Top.Value); - - var items = await query.ToListAsync(cancellationToken); - return new ServerDataResult(items, count); - } - - private static BadgeStyle GetStatusBadgeStyle(WorkQueueStatus status) => - status switch - { - WorkQueueStatus.Queued => BadgeStyle.Info, - WorkQueueStatus.Dispatched => BadgeStyle.Success, - WorkQueueStatus.Cancelled => BadgeStyle.Warning, - _ => BadgeStyle.Light, - }; } diff --git a/src/Trax.Dashboard/Components/Shared/PollingComponentBase.cs b/src/Trax.Dashboard/Components/Shared/PollingComponentBase.cs index f456edf..e9e3e0e 100644 --- a/src/Trax.Dashboard/Components/Shared/PollingComponentBase.cs +++ b/src/Trax.Dashboard/Components/Shared/PollingComponentBase.cs @@ -42,6 +42,46 @@ public abstract class PollingComponentBase : ComponentBase, IAsyncDisposable /// protected bool PausePolling { get; set; } + /// + /// Error message from the most recent batch operation, displayed as an alert. + /// Cleared at the start of each batch operation. + /// + protected string? BatchError { get; set; } + + /// + /// True while a batch operation is in progress. Used to disable action buttons. + /// + protected bool BatchOperating { get; set; } + + /// + /// Runs a batch operation with standardized error handling, loading state, and polling control. + /// Clears , sets during execution, + /// invokes on success, unpauses polling, and reloads data. + /// + /// The async operation to execute. + /// Optional callback invoked after the operation succeeds (e.g. clear selection). + protected async Task RunBatchOperationAsync(Func operation, Action? onSuccess = null) + { + BatchError = null; + BatchOperating = true; + + try + { + await operation(); + onSuccess?.Invoke(); + PausePolling = false; + await LoadDataAsync(DisposalToken); + } + catch (Exception ex) + { + BatchError = ex.Message; + } + finally + { + BatchOperating = false; + } + } + /// /// A CancellationToken that is cancelled when the component is disposed. /// Event handlers can pass this to async operations so they abort when the user navigates away. diff --git a/src/Trax.Dashboard/Utilities/CancellationHelper.cs b/src/Trax.Dashboard/Utilities/CancellationHelper.cs new file mode 100644 index 0000000..56af9fc --- /dev/null +++ b/src/Trax.Dashboard/Utilities/CancellationHelper.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Trax.Effect.Data.Services.DataContext; +using Trax.Effect.Data.Services.IDataContextFactory; +using Trax.Effect.Enums; +using Trax.Scheduler.Services.CancellationRegistry; + +namespace Trax.Dashboard.Utilities; + +/// +/// Centralizes the "set CancellationRequested + notify CancellationRegistry" pattern +/// used by MetadataPage, MetadataDetailPage, and ManifestGroupDetailPage. +/// +public static class CancellationHelper +{ + /// + /// Requests cancellation for the specified metadata IDs by setting the database flag + /// and attempting same-server instant cancellation via . + /// + /// The number of metadata records that had cancellation requested. + public static async Task CancelTrainsAsync( + IDataContextProviderFactory factory, + IServiceProvider serviceProvider, + IEnumerable metadataIds, + CancellationToken ct + ) + { + var ids = metadataIds.ToList(); + if (ids.Count == 0) + return 0; + + using var context = await factory.CreateDbContextAsync(ct); + var count = await context + .Metadatas.Where(m => ids.Contains(m.Id) && m.TrainState == TrainState.InProgress) + .ExecuteUpdateAsync(s => s.SetProperty(m => m.CancellationRequested, true), ct); + + var registry = serviceProvider.GetService(); + if (registry is not null) + { + foreach (var id in ids) + registry.TryCancel(id); + } + + return count; + } +} diff --git a/src/Trax.Dashboard/Utilities/DashboardFormatters.cs b/src/Trax.Dashboard/Utilities/DashboardFormatters.cs index 789c07e..6ed26ef 100644 --- a/src/Trax.Dashboard/Utilities/DashboardFormatters.cs +++ b/src/Trax.Dashboard/Utilities/DashboardFormatters.cs @@ -97,6 +97,15 @@ public static string FormatUptime(TimeSpan uptime) return $"{(int)uptime.TotalMinutes}m {uptime.Seconds}s"; } + public static BadgeStyle GetWorkQueueStatusBadgeStyle(WorkQueueStatus status) => + status switch + { + WorkQueueStatus.Queued => BadgeStyle.Info, + WorkQueueStatus.Dispatched => BadgeStyle.Success, + WorkQueueStatus.Cancelled => BadgeStyle.Warning, + _ => BadgeStyle.Light, + }; + public static BadgeStyle GetLogLevelBadgeStyle(LogLevel level) => level switch { diff --git a/src/Trax.Dashboard/Utilities/DataGridQueryHelper.cs b/src/Trax.Dashboard/Utilities/DataGridQueryHelper.cs new file mode 100644 index 0000000..86c210f --- /dev/null +++ b/src/Trax.Dashboard/Utilities/DataGridQueryHelper.cs @@ -0,0 +1,53 @@ +using System.Linq.Dynamic.Core; +using Microsoft.EntityFrameworkCore; +using Trax.Dashboard.Models; +using Trax.Effect.Data.Services.DataContext; +using Trax.Effect.Data.Services.IDataContextFactory; + +namespace Trax.Dashboard.Utilities; + +/// +/// Eliminates duplicated server-side pagination boilerplate across DataGrid list pages. +/// Each page provides a query factory that returns the base IQueryable with default ordering; +/// this helper applies dynamic filtering, sorting, pagination, and returns the result. +/// +public static class DataGridQueryHelper +{ + /// + /// Loads a page of data for a server-side TraxDataGrid. + /// + /// The data context factory to create a DbContext. + /// + /// A function that receives the IDataContext and returns the base IQueryable + /// with default ordering applied (e.g. db.WorkQueues.AsNoTracking().OrderByDescending(q => q.Id)). + /// + /// The Radzen LoadDataArgs containing filter, sort, skip, and take values. + /// Cancellation token. + public static async Task> LoadPageAsync( + IDataContextProviderFactory factory, + Func> queryFactory, + Radzen.LoadDataArgs args, + CancellationToken ct + ) + where T : class + { + using var context = await factory.CreateDbContextAsync(ct); + var query = queryFactory(context); + + if (!string.IsNullOrEmpty(args.Filter)) + query = query.Where(args.Filter); + + if (!string.IsNullOrEmpty(args.OrderBy)) + query = query.OrderBy(args.OrderBy); + + var count = await query.CountAsync(ct); + + if (args.Skip.HasValue) + query = query.Skip(args.Skip.Value); + if (args.Top.HasValue) + query = query.Take(args.Top.Value); + + var items = await query.ToListAsync(ct); + return new ServerDataResult(items, count); + } +} diff --git a/tests/Trax.Dashboard.Tests.Integration/UnitTests/DashboardFormattersTests.cs b/tests/Trax.Dashboard.Tests.Integration/UnitTests/DashboardFormattersTests.cs index d327d62..2491265 100644 --- a/tests/Trax.Dashboard.Tests.Integration/UnitTests/DashboardFormattersTests.cs +++ b/tests/Trax.Dashboard.Tests.Integration/UnitTests/DashboardFormattersTests.cs @@ -210,4 +210,36 @@ public void GetStateBadgeStyle_AllDefinedStates_ReturnExpectedStyles() } #endregion + + #region GetWorkQueueStatusBadgeStyle + + [Test] + public void GetWorkQueueStatusBadgeStyle_Queued_ReturnsInfo() + { + var result = DashboardFormatters.GetWorkQueueStatusBadgeStyle(WorkQueueStatus.Queued); + result.Should().Be(BadgeStyle.Info); + } + + [Test] + public void GetWorkQueueStatusBadgeStyle_Dispatched_ReturnsSuccess() + { + var result = DashboardFormatters.GetWorkQueueStatusBadgeStyle(WorkQueueStatus.Dispatched); + result.Should().Be(BadgeStyle.Success); + } + + [Test] + public void GetWorkQueueStatusBadgeStyle_Cancelled_ReturnsWarning() + { + var result = DashboardFormatters.GetWorkQueueStatusBadgeStyle(WorkQueueStatus.Cancelled); + result.Should().Be(BadgeStyle.Warning); + } + + [Test] + public void GetWorkQueueStatusBadgeStyle_UnknownValue_ReturnsLight() + { + var result = DashboardFormatters.GetWorkQueueStatusBadgeStyle((WorkQueueStatus)99); + result.Should().Be(BadgeStyle.Light); + } + + #endregion }