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
}