diff --git a/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/ActivityDetailsTab.razor.cs b/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/ActivityDetailsTab.razor.cs index 4f86a6de..97a0c29f 100644 --- a/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/ActivityDetailsTab.razor.cs +++ b/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/ActivityDetailsTab.razor.cs @@ -43,6 +43,7 @@ public record ActivityExecutionRecordTableRow(int Number, ActivityExecutionRecor public void Refresh() { CreateDataModels(); + StateHasChanged(); } /// diff --git a/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/ActivityExecutionsTab.razor.cs b/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/ActivityExecutionsTab.razor.cs index 33995f05..9531b34b 100644 --- a/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/ActivityExecutionsTab.razor.cs +++ b/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/ActivityExecutionsTab.razor.cs @@ -2,13 +2,14 @@ using Elsa.Api.Client.Resources.ActivityExecutions.Models; using Elsa.Studio.Models; using Elsa.Studio.Workflows.Domain.Contracts; +using Elsa.Studio.Workflows.Extensions; using Microsoft.AspNetCore.Components; using MudBlazor; namespace Elsa.Studio.Workflows.Components.WorkflowInstanceViewer.Components; /// Displays the details of an activity. -public partial class ActivityExecutionsTab +public partial class ActivityExecutionsTab : IAsyncDisposable { /// Represents a row in the table of activity executions. /// The number of executions. @@ -31,10 +32,12 @@ public record ActivityExecutionRecordTableRow(int Number, ActivityExecutionRecor private IDictionary SelectedActivityState { get; set; } = new Dictionary(); private IDictionary SelectedOutcomesData { get; set; } = new Dictionary(); private IDictionary SelectedOutputData { get; set; } = new Dictionary(); + private Timer? _refreshTimer; /// Refreshes the component. - public void Refresh() + public async Task RefreshAsync() { + await StopRefreshTimerAsync(); SelectedItem = null; SelectedActivityState = new Dictionary(); SelectedOutcomesData = new Dictionary(); @@ -70,11 +73,51 @@ private void CreateSelectedItemDataModels(ActivityExecutionRecord? record) SelectedOutputData = outputData; } - private async Task OnActivityExecutionClicked(TableRowClickEventArgs arg) + private async Task RefreshSelectedItemAsync(string id) { var id = arg.Item.ActivityExecutionSummary.Id; SelectedItem = await ActivityExecutionService.GetAsync(id); CreateSelectedItemDataModels(SelectedItem); - StateHasChanged(); + await InvokeAsync(StateHasChanged); + } + + private async Task OnActivityExecutionClicked(TableRowClickEventArgs arg) + { + await StopRefreshTimerAsync(); + var id = arg.Item.ActivityExecution.Id; + await RefreshSelectedItemAsync(id); + + if (SelectedItem == null) + return; + + if (SelectedItem.IsFused()) + return; + + RefreshSelectedItemPeriodically(id); + } + + private void RefreshSelectedItemPeriodically(string id) + { + async void Callback(object? _) + { + await RefreshSelectedItemAsync(id); + + if (SelectedItem == null || SelectedItem.IsFused()) + await StopRefreshTimerAsync(); + } + + _refreshTimer = new Timer(Callback, null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); + } + + private async Task StopRefreshTimerAsync() + { + if (_refreshTimer == null) return; + await _refreshTimer.DisposeAsync(); + _refreshTimer = null; + } + + async ValueTask IAsyncDisposable.DisposeAsync() + { + if (_refreshTimer != null) await _refreshTimer.DisposeAsync(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/WorkflowInstanceDesigner.razor.cs b/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/WorkflowInstanceDesigner.razor.cs index c5fdeb93..955f4851 100644 --- a/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/WorkflowInstanceDesigner.razor.cs +++ b/src/modules/Elsa.Studio.Workflows/Components/WorkflowInstanceViewer/Components/WorkflowInstanceDesigner.razor.cs @@ -10,6 +10,7 @@ using Elsa.Studio.DomInterop.Contracts; using Elsa.Studio.Workflows.Contracts; using Elsa.Studio.Workflows.Domain.Contracts; +using Elsa.Studio.Workflows.Extensions; using Elsa.Studio.Workflows.Models; using Elsa.Studio.Workflows.Pages.WorkflowInstances.View.Models; using Elsa.Studio.Workflows.Shared.Args; @@ -72,6 +73,7 @@ public partial class WorkflowInstanceDesigner : IAsyncDisposable private IWorkflowInstanceObserver? WorkflowInstanceObserver { get; set; } = default!; private ICollection SelectedActivityExecutions { get; set; } = new List(); private ActivityExecutionRecord? LastActivityExecution { get; set; } + private Timer? _refreshTimer; private RadzenSplitterPane ActivityPropertiesPane { @@ -190,7 +192,7 @@ private async Task DisposeObserverAsync() private async Task OnActivityExecutionLogUpdated(ActivityExecutionLogUpdatedMessage message) { if (_designer == null) return; - + foreach (var stats in message.Stats) { var activityNodeId = stats.ActivityNodeId; @@ -228,6 +230,7 @@ private void StopElapsedTimer() private async Task HandleActivitySelectedAsync(JsonObject activity) { + await StopRefreshActivityStatePeriodically(); await InvokeAsync(async () => { var activityNodeId = activity.GetNodeId(); @@ -236,8 +239,19 @@ await InvokeAsync(async () => SelectedActivityExecutions = await GetActivityExecutionRecordsAsync(activityNodeId); StateHasChanged(); _activityDetailsTab?.Refresh(); - _activityExecutionsTab?.Refresh(); + + if (_activityExecutionsTab != null) + await _activityExecutionsTab.RefreshAsync(); }); + + if (SelectedActivityExecutions.Any()) + { + var lastRecord = SelectedActivityExecutions.Last(); + LastActivityExecution = await InvokeWithBlazorServiceContext(() => ActivityExecutionService.GetAsync(lastRecord.Id)); + + if (LastActivityExecution != null) + RefreshActivityStatePeriodically(LastActivityExecution.Id); + } } private async Task> GetActivityExecutionRecordsAsync(string activityNodeId) @@ -264,6 +278,39 @@ private async Task UpdatePropertiesPaneHeightAsync() _propertiesPaneHeight = (int)visibleHeight - 50; } + private async Task RefreshSelectedItemAsync(string id) + { + var record = await InvokeWithBlazorServiceContext(() => ActivityExecutionService.GetAsync(id)); + LastActivityExecution = record; + await InvokeAsync(() => + { + StateHasChanged(); + _activityDetailsTab?.Refresh(); + }); + } + + private void RefreshActivityStatePeriodically(string id) + { + async void Callback(object? _) + { + await RefreshSelectedItemAsync(id); + + if (LastActivityExecution == null || LastActivityExecution.IsFused()) + await StopRefreshActivityStatePeriodically(); + } + + _refreshTimer = new Timer(Callback, null, TimeSpan.Zero, TimeSpan.FromSeconds(1)); + } + + private async Task StopRefreshActivityStatePeriodically() + { + if (_refreshTimer != null) + { + await _refreshTimer.DisposeAsync(); + _refreshTimer = null; + } + } + private static ActivityStats Map(ActivityExecutionStats source) { return new ActivityStats @@ -322,5 +369,8 @@ async ValueTask IAsyncDisposable.DisposeAsync() { StopElapsedTimer(); await DisposeObserverAsync(); + + if (_refreshTimer != null) + await _refreshTimer.DisposeAsync(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Studio.Workflows/Extensions/ActivityExecutionRecordExtensions.cs b/src/modules/Elsa.Studio.Workflows/Extensions/ActivityExecutionRecordExtensions.cs new file mode 100644 index 00000000..79ee9d7f --- /dev/null +++ b/src/modules/Elsa.Studio.Workflows/Extensions/ActivityExecutionRecordExtensions.cs @@ -0,0 +1,18 @@ +using Elsa.Api.Client.Resources.ActivityExecutions.Models; + +namespace Elsa.Studio.Workflows.Extensions; + +/// +/// Provides extension methods for . +/// +public static class ActivityExecutionRecordExtensions +{ + /// + /// Determines if the specified activity execution record is fused. + /// + public static bool IsFused(this ActivityExecutionRecord record) + { + // TODO: with blueberry, consider introducing a new property to ActivityExecutionRecord to indicate whether the record has all details or not. + return record.ActivityState != null || record.Outputs != null || record.Exception != null || record.Payload != null || record.Properties.Count > 0; + } +} \ No newline at end of file