Elsa 2.0/workflow editor (#541)

* Fix connection model porperty names

* Add workflow instance name to index

* Enable sample workflows

* Incremental work on activity displays

* Add custom activity display support

* Optimize workflow registry list endpoint model
This commit is contained in:
Sipke Schoorstra 2020-12-25 22:00:25 +01:00 committed by GitHub
parent 92e1a1c3bf
commit 1bf53fb59a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 564 additions and 107 deletions

View file

@ -185,6 +185,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.DistributedLocking.Sql
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.Timers", "src\activities\Elsa.Activities.Timers\Elsa.Activities.Timers.csproj", "{74E83393-50AF-41E3-A1C8-97202673CD85}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElsaDashboard.Abstractions", "src\dashboards\blazor\ElsaDashboard.Abstractions\ElsaDashboard.Abstractions.csproj", "{8FC11EB7-38FE-445C-BE58-4D96BB14AAB4}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.Rebus", "src\activities\Elsa.Activities.Rebus\Elsa.Activities.Rebus.csproj", "{A9843C3F-74F7-4282-A7B7-841F6B8EB12D}"
EndProject
Global
@ -445,6 +446,10 @@ Global
{74E83393-50AF-41E3-A1C8-97202673CD85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{74E83393-50AF-41E3-A1C8-97202673CD85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{74E83393-50AF-41E3-A1C8-97202673CD85}.Release|Any CPU.Build.0 = Release|Any CPU
{8FC11EB7-38FE-445C-BE58-4D96BB14AAB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8FC11EB7-38FE-445C-BE58-4D96BB14AAB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8FC11EB7-38FE-445C-BE58-4D96BB14AAB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8FC11EB7-38FE-445C-BE58-4D96BB14AAB4}.Release|Any CPU.Build.0 = Release|Any CPU
{A9843C3F-74F7-4282-A7B7-841F6B8EB12D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9843C3F-74F7-4282-A7B7-841F6B8EB12D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9843C3F-74F7-4282-A7B7-841F6B8EB12D}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -535,6 +540,7 @@ Global
{C90CD39C-8CDD-4BCB-AB6A-CF981D468DD2} = {7CD5C8D5-EC78-4A99-A514-F01CA7197AC8}
{78F2A9DC-3092-4371-ABFE-87C5DA4DE351} = {7CD5C8D5-EC78-4A99-A514-F01CA7197AC8}
{74E83393-50AF-41E3-A1C8-97202673CD85} = {37D2E628-5CC1-4C8E-8C02-3B0A29E2ACAA}
{8FC11EB7-38FE-445C-BE58-4D96BB14AAB4} = {E02BD53A-5B08-47A0-920E-DD537A6AFAAA}
{A9843C3F-74F7-4282-A7B7-841F6B8EB12D} = {B43B546E-23F3-46E8-ACB7-D04F05CDA180}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution

View file

@ -11,5 +11,6 @@ namespace Elsa.Client.Models
[DataMember(Order = 4)] public bool PersistWorkflow { get; set; }
[DataMember(Order = 5)] public bool LoadWorkflowContext { get; set; }
[DataMember(Order = 6)] public bool SaveWorkflowContext { get; set; }
[DataMember(Order = 7)] public Variables Properties { get; set; } = new();
}
}

View file

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.Serialization;
namespace Elsa.Client.Models
@ -20,11 +22,11 @@ namespace Elsa.Client.Models
Data = data;
}
[DataMember(Order = 1)]public IDictionary<string, object?> Data { get; set; }
[DataMember(Order = 1)] public IDictionary<string, object?> Data { get; set; }
public object? Get(string name) => Has(name) ? Data[name] : default;
public T Get<T>(string name)
public T? Get<T>(string name)
{
if (!Has(name))
return default!;
@ -34,7 +36,14 @@ namespace Elsa.Client.Models
if (value == null)
return default!;
return (T)value!;
if (value is T convertedValue)
return convertedValue;
if (value == default!)
return default!;
var converter = TypeDescriptor.GetConverter(typeof(T));
return converter.CanConvertFrom(value.GetType()) ? (T?) converter.ConvertFrom(value) : default;
}
public Variables Set(string name, object? value)

View file

@ -31,6 +31,14 @@ namespace Elsa
string propertyName,
ActivityExecutionContext activityExecutionContext,
CancellationToken cancellationToken)
=> (T) await workflowBlueprint.GetActivityPropertyValue(activityId, propertyName, activityExecutionContext, cancellationToken);
public static async ValueTask<object> GetActivityPropertyValue(
this IWorkflowBlueprint workflowBlueprint,
string activityId,
string propertyName,
ActivityExecutionContext activityExecutionContext,
CancellationToken cancellationToken)
{
var provider = workflowBlueprint.ActivityPropertyProviders.GetProvider(activityId, propertyName);
@ -38,7 +46,7 @@ namespace Elsa
return default!;
var value = await provider.GetValueAsync(activityExecutionContext, cancellationToken);
return (T) value!;
return value!;
}
public static T? GetActivityState<TActivity, T>(

View file

@ -19,12 +19,8 @@ namespace Elsa
return query;
}
public static IActivityBlueprintWrapper<TActivity>? FindActivity<TActivity>(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, Func<IActivityBlueprintWrapper<TActivity>, bool>? predicate = default)
where TActivity : IActivity =>
workflowBlueprintWrapper.Filter(predicate).FirstOrDefault();
public static IActivityBlueprintWrapper<TActivity> GetActivity<TActivity>(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, string id)
where TActivity : IActivity =>
workflowBlueprintWrapper.FindActivity<TActivity>(x => x.ActivityBlueprint.Id == id)!;
public static IActivityBlueprintWrapper<TActivity>? FindActivity<TActivity>(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, Func<IActivityBlueprintWrapper<TActivity>, bool>? predicate = default) where TActivity : IActivity => workflowBlueprintWrapper.Filter(predicate).FirstOrDefault();
public static IActivityBlueprintWrapper<TActivity>? GetActivity<TActivity>(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, string id) where TActivity : IActivity => workflowBlueprintWrapper.FindActivity<TActivity>(x => x.ActivityBlueprint.Id == id)!;
public static IActivityBlueprintWrapper? GetActivity(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, string id) => workflowBlueprintWrapper.Activities.FirstOrDefault(x => x.ActivityBlueprint.Id == id);
}
}

View file

@ -34,8 +34,7 @@ namespace Elsa.Services
properties[propertyName] = provider;
}
public IDictionary<string, IActivityPropertyValueProvider>? GetProviders(string activityId) =>
_providers.TryGetValue(activityId, out var properties) ? properties : null;
public IDictionary<string, IActivityPropertyValueProvider>? GetProviders(string activityId) => _providers.TryGetValue(activityId, out var properties) ? properties : null;
public IActivityPropertyValueProvider? GetProvider(string activityId, string propertyName) =>
_providers.TryGetValue(activityId, out var properties)

View file

@ -9,6 +9,7 @@ namespace Elsa.Services
{
void AddProvider(string activityId, string propertyName, IActivityPropertyValueProvider provider);
IActivityPropertyValueProvider? GetProvider(string activityId, string propertyName);
IDictionary<string, IActivityPropertyValueProvider>? GetProviders(string activityId);
ValueTask SetActivityPropertiesAsync(
IActivity activity,

View file

@ -17,6 +17,22 @@ namespace Elsa.Services.Models
public IActivityBlueprint ActivityBlueprint => ActivityExecutionContext.ActivityBlueprint;
public IActivityBlueprintWrapper<TActivity> As<TActivity>() where TActivity : IActivity => new ActivityBlueprintWrapper<TActivity>(ActivityExecutionContext);
public async ValueTask<object?> GetPropertyValueAsync(string propertyName, CancellationToken cancellationToken = default)
{
var workflowBlueprint = ActivityExecutionContext.WorkflowExecutionContext.WorkflowBlueprint;
var activityId = ActivityExecutionContext.ActivityBlueprint.Id;
// Computed property setters that depend on actual workflow state might fault, since we might be using a fake activity execution context.
try
{
return await workflowBlueprint.GetActivityPropertyValue(activityId, propertyName, ActivityExecutionContext, cancellationToken);
}
catch (Exception)
{
return default;
}
}
}
public class ActivityBlueprintWrapper<TActivity> : ActivityBlueprintWrapper, IActivityBlueprintWrapper<TActivity> where TActivity : IActivity

View file

@ -9,6 +9,7 @@ namespace Elsa.Services.Models
{
IActivityBlueprint ActivityBlueprint { get; }
IActivityBlueprintWrapper<TActivity> As<TActivity>() where TActivity : IActivity;
ValueTask<object?> GetPropertyValueAsync(string propertyName, CancellationToken cancellationToken = default);
}
public interface IActivityBlueprintWrapper<TActivity> : IActivityBlueprintWrapper where TActivity:IActivity

View file

@ -0,0 +1,10 @@
using ElsaDashboard.Models;
using Microsoft.AspNetCore.Components;
namespace ElsaDashboard.Components
{
public abstract class ActivityDisplayComponent : ComponentBase
{
[Parameter] public ActivityDisplayContext DisplayContext { protected get; set; } = default!;
}
}

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>ElsaDashboard</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components" Version="5.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\clients\Elsa.Client\Elsa.Client.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,20 @@
using System;
using ElsaDashboard.Components;
using ElsaDashboard.Models;
using Microsoft.AspNetCore.Components.Rendering;
namespace ElsaDashboard.Extensions
{
public static class RenderTreeBuilderExtensions
{
public static RenderTreeBuilder AddActivityDisplayComponent<T>(this RenderTreeBuilder builder, ActivityDisplayContext context) where T : ActivityDisplayComponent => AddActivityDisplayComponent(builder, typeof(T), context);
public static RenderTreeBuilder AddActivityDisplayComponent(this RenderTreeBuilder builder, Type type, ActivityDisplayContext context)
{
builder.OpenComponent(0, type);
builder.AddAttribute(1, "DisplayContext", context);
builder.CloseComponent();
return builder;
}
}
}

View file

@ -0,0 +1,13 @@
using ElsaDashboard.Services;
using Microsoft.Extensions.DependencyInjection;
namespace ElsaDashboard.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddActivityDisplayProvider<T>(this IServiceCollection services) where T : class, IActivityDisplayProvider
{
return services.AddTransient<IActivityDisplayProvider, T>();
}
}
}

View file

@ -0,0 +1,6 @@
using Elsa.Client.Models;
namespace ElsaDashboard.Models
{
public record ActivityDisplayContext(string ActivityType, Variables Properties);
}

View file

@ -0,0 +1,12 @@
using System;
using Microsoft.AspNetCore.Components;
namespace ElsaDashboard.Models
{
public class ActivityDisplayDescriptor
{
public string ActivityType { get; init; } = default!;
public Func<ActivityDisplayContext, RenderFragment>? RenderBody { get; init; }
public Func<ActivityDisplayContext, RenderFragment>? RenderIcon { get; init; }
}
}

View file

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ElsaDashboard.Models;
namespace ElsaDashboard.Services
{
public abstract class ActivityDisplayProvider : IActivityDisplayProvider
{
public virtual ValueTask<IEnumerable<ActivityDisplayDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
{
var descriptors = GetDescriptors();
return ValueTask.FromResult(descriptors);
}
protected virtual IEnumerable<ActivityDisplayDescriptor> GetDescriptors()
{
yield break;
}
}
}

View file

@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ElsaDashboard.Models;
namespace ElsaDashboard.Services
{
public interface IActivityDisplayProvider
{
ValueTask<IEnumerable<ActivityDisplayDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default);
}
}

View file

@ -0,0 +1,11 @@
using System.Threading;
using System.Threading.Tasks;
using ElsaDashboard.Models;
namespace ElsaDashboard.Services
{
public interface IActivityDisplayService
{
ValueTask<ActivityDisplayDescriptor?> GetDisplayDescriptorAsync(string activityType, CancellationToken cancellationToken = default);
}
}

View file

@ -0,0 +1,4 @@
@using NodaTime
@inherits ActivityDisplayComponent
<p>Every <a href="#" class="font-medium border-b border-solid border-gray-200 text-black">@(DisplayContext.Properties.Get<Duration>("Timeout"))</a></p>

View file

@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ElsaDashboard.Application.Activities;
using ElsaDashboard.Extensions;
using ElsaDashboard.Models;
using ElsaDashboard.Services;
namespace ElsaDashboard.Application.Display
{
public class TimerDisplayProvider : ActivityDisplayProvider
{
protected override IEnumerable<ActivityDisplayDescriptor> GetDescriptors()
{
yield return new ActivityDisplayDescriptor
{
ActivityType = "Timer",
RenderBody = context => builder => builder.AddActivityDisplayComponent<TimerActivity>(context)
};
}
}
}

View file

@ -28,6 +28,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ElsaDashboard.Abstractions\ElsaDashboard.Abstractions.csproj" />
<ProjectReference Include="..\ElsaDashboard.Shared\ElsaDashboard.Shared.csproj" />
</ItemGroup>

View file

@ -1,5 +1,8 @@
using Blazored.Modal;
using ElsaDashboard.Application.Display;
using ElsaDashboard.Application.Services;
using ElsaDashboard.Extensions;
using ElsaDashboard.Services;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
@ -11,7 +14,9 @@ namespace Microsoft.Extensions.DependencyInjection
return services
.AddBlazoredModal()
.AddScoped<IConfirmDialogService, ConfirmDialogService>()
.AddScoped<IFlyoutPanelService, FlyoutPanelService>();
.AddScoped<IFlyoutPanelService, FlyoutPanelService>()
.AddSingleton<IActivityDisplayService, ActivityDisplayService>()
.AddActivityDisplayProvider<TimerDisplayProvider>();
}
}
}

View file

@ -1,14 +1,19 @@
using System.Collections.Generic;
using System.Linq;
using Elsa.Client.Models;
using ElsaDashboard.Models;
namespace ElsaDashboard.Application.Models
{
public record ActivityModel
{
public ActivityModel(string activityId, string type) => (ActivityId, Type) = (activityId, type);
public ActivityModel(string activityId, string type, IEnumerable<string> outcomes) => (ActivityId, Type, Outcomes) = (activityId, type, outcomes.ToList());
public string ActivityId { get; init; }
public string Type { get; init; }
public string? Name { get; init; }
public string? DisplayName { get; init; }
public string? Description { get; set; }
public ICollection<string> Outcomes { get; init; } = new List<string>();
public Variables Properties { get; set; } = new();
public ActivityDisplayDescriptor? DisplayDescriptor { get; set; }
}
}

View file

@ -18,26 +18,6 @@ namespace ElsaDashboard.Application.Models
public IImmutableList<ConnectionModel> Connections { get; init; } = Array.Empty<ConnectionModel>().ToImmutableList();
public static WorkflowModel Blank() => new("New Workflow");
public static WorkflowModel Demo() =>
new WorkflowModel
{
Activities = new[]
{
new ActivityModel("activity-1", "ReadLine", new[] { "Done" }),
new ActivityModel("activity-2", "IfElse", new[] { "True", "False" }),
new ActivityModel("activity-3", "WriteLine", new[] { "Done" }),
new ActivityModel("activity-4", "WriteLine", new[] { "Done" })
}.ToImmutableList(),
Connections = new[]
{
new ConnectionModel("activity-1", "activity-2", "Done"),
new ConnectionModel("activity-2", "activity-3", "True"),
new ConnectionModel("activity-2", "activity-4", "False")
}.ToImmutableList()
};
public WorkflowModel AddActivity(ActivityModel activity) => this with { Activities = Activities.Add(activity)};
public WorkflowModel RemoveActivity(ActivityModel activity) => this with { Activities = Activities.Where(x => x != activity).ToImmutableList()};
public WorkflowModel AddConnection(ConnectionModel connection) => this with { Connections = Connections.Add(connection)};

View file

@ -1,4 +1,5 @@
using System;
using Elsa.Client.Models;
namespace ElsaDashboard.Application.Models
{

View file

@ -69,7 +69,7 @@ namespace ElsaDashboard.Application.Pages.WorkflowDefinitions
private ActivityDefinition CreateActivityDefinition(ActivityModel activityModel)
{
return new ActivityDefinition
return new()
{
ActivityId = activityModel.ActivityId,
Type = activityModel.Type
@ -78,7 +78,7 @@ namespace ElsaDashboard.Application.Pages.WorkflowDefinitions
private WorkflowModel CreateWorkflowModel(WorkflowDefinition workflowDefinition)
{
return new WorkflowModel
return new()
{
Name = workflowDefinition.Name,
Activities = workflowDefinition.Activities.Select(CreateActivityModel).ToImmutableList(),
@ -94,7 +94,14 @@ namespace ElsaDashboard.Application.Pages.WorkflowDefinitions
private ActivityModel CreateActivityModel(ActivityDefinition activityDefinition)
{
var descriptor = ActivityDescriptors[activityDefinition.Type];
return new ActivityModel(activityDefinition.ActivityId, activityDefinition.Type, descriptor.Outcomes);
return new ActivityModel
{
Name = activityDefinition.Name,
ActivityId = activityDefinition.ActivityId,
Type = activityDefinition.Type,
DisplayName = descriptor.DisplayName,
Outcomes = descriptor.Outcomes
};
}
private void StartBackgroundWorker()

View file

@ -32,9 +32,9 @@
<div class="flex-1">
<h3>Filters</h3>
</div>
<ButtonDropdown Text="@SelectedWorkflowText" Items="@WorkflowFilterItems" IconType="@typeof(Icons.Squares)"/>
<ButtonDropdown Text="@SelectedWorkflowStatusText" Items="@WorkflowStatusFilterItems" IconType="@typeof(Icons.PlayCircle)"/>
<ButtonDropdown Text="@SelectedOrderByText" Items="@OrderByItems" IconType="@typeof(Icons.SortDescending)"/>
<ButtonDropdown Text="@SelectedWorkflowText" Items="@WorkflowFilterItems" IconType="@typeof(SquaresIcon)"/>
<ButtonDropdown Text="@SelectedWorkflowStatusText" Items="@WorkflowStatusFilterItems" IconType="@typeof(PlayCircleIcon)"/>
<ButtonDropdown Text="@SelectedOrderByText" Items="@OrderByItems" IconType="@typeof(SortDescendingIcon)"/>
</div>
<!-- Workflow instances (only on smallest breakpoint) -->

View file

@ -101,14 +101,18 @@
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-100">
@foreach (var workflowBlueprint in WorkflowBlueprints.Items)
@foreach (var grouping in VersionedWorkflowBlueprints)
{
var displayName = workflowBlueprint.DisplayName ?? workflowBlueprint.Name ?? "Unnamed";
var workflowBlueprintId = grouping.Key;
var workflowBlueprints = grouping.OrderByDescending(x => x.Version).ToList();
var latestVersion = workflowBlueprints.First();
var publishedVersion = workflowBlueprints.FirstOrDefault(x => x.IsPublished);
var displayName = latestVersion.DisplayName ?? latestVersion.Name ?? "Unnamed";
<tr>
<td class="px-6 py-3 whitespace-no-wrap text-sm leading-5 font-medium text-gray-900">
<div class="flex items-center space-x-3 lg:pl-2">
<div class="flex-shrink-0 w-2.5 h-2.5 rounded-full bg-pink-600"></div>
<a href="@($"workflow-registry/{workflowBlueprint.Id}/viewer")" class="truncate hover:text-gray-600">
<a href="@($"workflow-registry/{workflowBlueprintId}/viewer")" class="truncate hover:text-gray-600">
<span>@displayName</span>
</a>
</div>
@ -125,10 +129,10 @@
</div>
</td>
<td class="hidden md:table-cell px-6 py-3 whitespace-no-wrap text-sm leading-5 text-gray-500 text-right">
1
@latestVersion.Version
</td>
<td class="hidden md:table-cell px-6 py-3 whitespace-no-wrap text-sm leading-5 text-gray-500 text-right">
1
@(publishedVersion?.Version.ToString() ?? "-")
</td>
<td class="pr-6">
<div x-data="{ open: false }" x-on:keydown.escape="open = false" x-on:click.away="open = false" class="relative flex justify-end items-center">
@ -147,7 +151,7 @@
class="z-10 mx-3 origin-top-right absolute right-7 top-0 w-48 mt-1 rounded-md shadow-lg">
<div class="rounded-md bg-white shadow-xs" role="menu" aria-orientation="vertical" aria-labelledby="project-options-menu-0">
<div class="py-1">
<a href="@($"workflow-registry/{workflowBlueprint.Id}/viewer")" class="group flex items-center px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" role="menuitem">
<a href="@($"workflow-registry/{workflowBlueprintId}/viewer")" class="group flex items-center px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" role="menuitem">
<!-- Heroicon name: pencil-alt -->
<svg class="mr-3 h-5 w-5 text-gray-400 group-hover:text-gray-500 group-focus:text-gray-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"/>

View file

@ -1,4 +1,6 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Elsa.Client.Models;
using ElsaDashboard.Shared.Rpc;
using Microsoft.AspNetCore.Components;
@ -9,6 +11,7 @@ namespace ElsaDashboard.Application.Pages.WorkflowRegistry
{
[Inject] private IWorkflowRegistryService WorkflowRegistryService { get; set; } = default!;
private PagedList<WorkflowBlueprint> WorkflowBlueprints { get; set; } = new();
private IEnumerable<IGrouping<string, WorkflowBlueprint>> VersionedWorkflowBlueprints => WorkflowBlueprints.Items.GroupBy(x => x.Id);
protected override async Task OnInitializedAsync()
{

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using Elsa.Client.Models;
using ElsaDashboard.Application.Models;
using ElsaDashboard.Services;
using ElsaDashboard.Shared.Rpc;
using Microsoft.AspNetCore.Components;
@ -14,6 +15,7 @@ namespace ElsaDashboard.Application.Pages.WorkflowRegistry
[Parameter] public string WorkflowDefinitionId { get; set; } = default!;
[Inject] private IWorkflowRegistryService WorkflowRegistryService { get; set; } = default!;
[Inject] private IActivityService ActivityService { get; set; } = default!;
[Inject] private IActivityDisplayService ActivityDisplayService { get; set; } = default!;
private IDictionary<string, ActivityInfo> ActivityDescriptors { get; set; } = default!;
private WorkflowBlueprint WorkflowBlueprint { get; set; } = default!;
private WorkflowModel WorkflowModel { get; set; } = WorkflowModel.Blank();
@ -26,15 +28,17 @@ namespace ElsaDashboard.Application.Pages.WorkflowRegistry
protected override async Task OnParametersSetAsync()
{
WorkflowBlueprint = (await WorkflowRegistryService.GetById(WorkflowDefinitionId, VersionOptions.Latest))!;
WorkflowModel = CreateWorkflowModel(WorkflowBlueprint);
WorkflowModel = await CreateWorkflowModelAsync(WorkflowBlueprint);
}
private WorkflowModel CreateWorkflowModel(WorkflowBlueprint workflowBlueprint)
private async ValueTask<WorkflowModel> CreateWorkflowModelAsync(WorkflowBlueprint workflowBlueprint)
{
var activities = await Task.WhenAll(workflowBlueprint.Activities.Select(async x => await CreateActivityModelAsync(x)));
return new()
{
Name = workflowBlueprint.Name,
Activities = workflowBlueprint.Activities.Select(CreateActivityModel).ToImmutableList(),
Activities = activities.ToImmutableList(),
Connections = workflowBlueprint.Connections.Select(CreateConnectionModel).ToImmutableList()
};
}
@ -44,10 +48,22 @@ namespace ElsaDashboard.Application.Pages.WorkflowRegistry
return new(connectionDefinition.SourceActivityId, connectionDefinition.TargetActivityId, connectionDefinition.Outcome);
}
private ActivityModel CreateActivityModel(ActivityBlueprint activityBlueprint)
private async ValueTask<ActivityModel> CreateActivityModelAsync(ActivityBlueprint activityBlueprint)
{
var descriptor = ActivityDescriptors[activityBlueprint.Type];
return new ActivityModel(activityBlueprint.Id, activityBlueprint.Type, descriptor.Outcomes);
var displayDescriptor = await ActivityDisplayService.GetDisplayDescriptorAsync(activityBlueprint.Type);
return new ActivityModel
{
Name = activityBlueprint.Name,
Type = activityBlueprint.Type,
DisplayName = descriptor.DisplayName,
Outcomes = descriptor.Outcomes,
ActivityId = activityBlueprint.Id,
Description = descriptor.Description,
Properties = activityBlueprint.Properties,
DisplayDescriptor = displayDescriptor
};
}
}
}

View file

@ -0,0 +1,33 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ElsaDashboard.Models;
using ElsaDashboard.Services;
namespace ElsaDashboard.Application.Services
{
public class ActivityDisplayService : IActivityDisplayService
{
private readonly IEnumerable<IActivityDisplayProvider> _providers;
public ActivityDisplayService(IEnumerable<IActivityDisplayProvider> providers)
{
_providers = providers;
}
public async ValueTask<ActivityDisplayDescriptor?> GetDisplayDescriptorAsync(string activityType, CancellationToken cancellationToken = default)
{
foreach (var provider in _providers)
{
var descriptors = await provider.GetDescriptorsAsync(cancellationToken);
var descriptor = descriptors.FirstOrDefault(x => x.ActivityType == activityType);
if (descriptor != null)
return descriptor;
}
return null;
}
}
}

View file

@ -7,14 +7,10 @@
<div class="p-5 border-b border-b-solid">
<div class="flex justify-between space-x-8">
<div class="flex-shrink-0">
<svg class="h-10 w-10 text-blue-500" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="12" r="9"/>
<polyline points="12 7 12 12 15 15"/>
</svg>
@Icon
</div>
<div class="flex-1 font-medium leading-8">
<p>@Model.Type</p>
<p>@DisplayName</p>
</div>
<div class="flex-shrink-0">
<button x-on:click="open = !open" aria-haspopup="true" class="w-8 h-8 inline-flex items-center justify-center text-gray-400 rounded-full bg-transparent hover:text-gray-500 focus:outline-none focus:text-gray-500 focus:bg-gray-100 transition ease-in-out duration-150">
@ -39,10 +35,7 @@
</div>
<div class="border-t border-gray-100"></div>
<div class="py-1">
<a href="#" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" role="menuitem">
Removed
from pinned
</a>
<a href="#" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" role="menuitem">Delete</a>
</div>
</div>
</div>
@ -51,6 +44,6 @@
</div>
<div class="p-6 text-gray-400 text-sm">
Every <a href="#" class="font-medium border-b border-solid border-gray-200 text-black">5 minutes</a>
@Body
</div>
</div>

View file

@ -1,4 +1,6 @@
using ElsaDashboard.Application.Models;
using Elsa.Client.Models;
using ElsaDashboard.Application.Icons;
using ElsaDashboard.Application.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
@ -6,7 +8,37 @@ namespace ElsaDashboard.Application.Shared
{
partial class Activity
{
[Parameter] public ActivityModel Model { get; set; } = new ActivityModel("", "");
public const string DefaultIconClass = "h-10 w-10 text-blue-500";
[Parameter] public ActivityModel Model { get; set; } = new();
[Parameter] public EventCallback<MouseEventArgs> OnClick { get; set; }
[Parameter] public RenderFragment Body { get; set; } = default!;
[Parameter] public RenderFragment Icon { get; set; } = default!;
protected override void OnInitialized()
{
if (Body == null!)
Body = GetDefaultBody();
if (Icon == null!)
Icon = GetDefaultIcon();
}
private RenderFragment GetDefaultBody() =>
builder =>
{
builder.OpenElement(0, "p");
builder.AddContent(1, Model.Description);
builder.CloseElement();
};
private RenderFragment GetDefaultIcon() =>
builder =>
{
builder.OpenComponent<ActivityIcon>(0);
builder.AddAttribute(1, "CssClass", DefaultIconClass);
builder.CloseComponent();
};
private string DisplayName => string.IsNullOrWhiteSpace(Model.DisplayName) ? Model.Type : Model.DisplayName;
}
}

View file

@ -12,11 +12,11 @@ namespace ElsaDashboard.Application.Shared
{
MenuItems = new[]
{
new MenuItem("Home", "", GetIcon<Home>(), NavLinkMatch.All),
new MenuItem("Workflow Registry", "workflow-registry", GetIcon<Squares>(), NavLinkMatch.Prefix),
new MenuItem("Workflow Instances", "workflow-instances", GetIcon<PlayCircle>(), NavLinkMatch.Prefix),
new MenuItem("Workflow Definitions", "workflow-definitions", GetIcon<Database>(), NavLinkMatch.Prefix),
new MenuItem("Composite Activities", "composite-activities", GetIcon<Icons.Activity>(), NavLinkMatch.Prefix),
new MenuItem("Home", "", GetIcon<HomeIcon>(), NavLinkMatch.All),
new MenuItem("Workflow Registry", "workflow-registry", GetIcon<SquaresIcon>(), NavLinkMatch.Prefix),
new MenuItem("Workflow Instances", "workflow-instances", GetIcon<PlayCircleIcon>(), NavLinkMatch.Prefix),
new MenuItem("Workflow Definitions", "workflow-definitions", GetIcon<DatabaseIcon>(), NavLinkMatch.Prefix),
new MenuItem("Composite Activities", "composite-activities", GetIcon<ActivityIcon>(), NavLinkMatch.Prefix),
};
}

View file

@ -1,4 +1,5 @@
@using ElsaDashboard.Application.Extensions
@using ElsaDashboard.Models
@{
var renderedActivities = new HashSet<string>();
@ -86,7 +87,7 @@
@RenderOutcomeButton($"start-button-plus-{activityId}", "", () => onAddActivityClickHandler(null, activityId, null))
}
<Activity Model="@activity"/>
@RenderActivity(activity)
@RenderOutcomeButtons(activity, onAddActivityClickHandler)
</div>
@if (children.Any())

View file

@ -9,6 +9,8 @@ using Elsa.Client.Models;
using ElsaDashboard.Application.Extensions;
using ElsaDashboard.Application.Models;
using ElsaDashboard.Application.Services;
using ElsaDashboard.Models;
using ElsaDashboard.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
@ -19,7 +21,7 @@ namespace ElsaDashboard.Application.Shared
partial class WorkflowDesigner : IAsyncDisposable
{
private static Func<ConnectionModel, Task> _connectionCreatedAction = default!;
[Parameter] public WorkflowModel Model { private get; set; } = WorkflowModel.Blank();
[Parameter] public EventCallback<WorkflowModelChangedEventArgs> WorkflowChanged { get; set; }
[Inject] private IJSRuntime JS { get; set; } = default!;
@ -28,6 +30,15 @@ namespace ElsaDashboard.Application.Shared
private bool _connectionsChanged = true;
private EventCallbackFactory EventCallbackFactory { get; } = new();
private readonly ActivityInfo _unknownActivityDescriptor =
new()
{
Type = "Unknown",
Category = "Unknown",
Description = "Activity type not found",
DisplayName = "Unknown"
};
[JSInvokableAttribute("InvokeConnectionCreated")]
public static Task InvokeConnectionCreated(ConnectionModel connection) => _connectionCreatedAction(connection);
@ -59,7 +70,7 @@ namespace ElsaDashboard.Application.Shared
{
Model = model;
ConnectionsHasChanged();
await WorkflowChanged.InvokeAsync(new WorkflowModelChangedEventArgs(Model));
}
@ -164,7 +175,7 @@ namespace ElsaDashboard.Application.Shared
{
await FlyoutPanelService.ShowAsync<ActivityPicker>(
"Activities",
new { ActivitySelected = EventCallbackFactory.Create<ActivityDescriptorSelectedEventArgs>(this, e => OnActivityPickedAsync(e, parentActivityId, targetActivityId, outcome)) },
new {ActivitySelected = EventCallbackFactory.Create<ActivityDescriptorSelectedEventArgs>(this, e => OnActivityPickedAsync(e, parentActivityId, targetActivityId, outcome))},
ButtonDescriptor.Create("Cancel", _ => ClosePanelAsync()));
}
@ -172,7 +183,14 @@ namespace ElsaDashboard.Application.Shared
{
outcome ??= "Done";
var activity = new ActivityModel(Guid.NewGuid().ToString("N"), activityInfo.Type, activityInfo.Outcomes);
var activity = new ActivityModel
{
ActivityId = Guid.NewGuid().ToString("N"),
Type = activityInfo.Type,
Outcomes = activityInfo.Outcomes,
DisplayName = activityInfo.DisplayName
};
var model = Model.AddActivity(activity);
if (targetActivityId != null)
@ -210,10 +228,35 @@ namespace ElsaDashboard.Application.Shared
model = model.AddConnection(connection);
}
}
await UpdateModelAsync(model);
await FlyoutPanelService.HideAsync();
}
private RenderFragment RenderActivity(ActivityModel activityModel)
{
return builder =>
{
var index = 0;
builder.OpenComponent<Activity>(index++);
builder.AddAttribute(index++, "Model", activityModel);
var displayDescriptor = activityModel.DisplayDescriptor;
if (displayDescriptor != null)
{
var context = new ActivityDisplayContext(activityModel.Type, activityModel.Properties);
if(displayDescriptor.RenderBody != null)
builder.AddAttribute(index++, "Body", displayDescriptor.RenderBody(context));
if(displayDescriptor.RenderIcon != null)
builder.AddAttribute(index, "Icon", displayDescriptor.RenderIcon(context));
}
builder.CloseComponent();
};
}
private void ConnectionsHasChanged()
{
@ -229,7 +272,7 @@ namespace ElsaDashboard.Application.Shared
await FlyoutPanelService.ShowAsync<ActivityEditor>(
activityInfo.DisplayName,
new { ActivityInfo = activityInfo },
new {ActivityInfo = activityInfo},
ButtonDescriptor.Create("Cancel", _ => ShowActivityPickerAsync(sourceActivityId, targetActivityId, outcome)),
ButtonDescriptor.Create("OK", _ => AddActivityAsync(activityInfo, sourceActivityId, targetActivityId, outcome), true));
}

View file

@ -3,10 +3,12 @@
@using Blazored.Modal
@using Blazored.Modal.Services
@using ElsaDashboard.Application
@using ElsaDashboard.Application.Icons
@using ElsaDashboard.Application.Models
@using ElsaDashboard.Application.Shared
@using ElsaDashboard.Components
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Microsoft.JSInterop

View file

@ -1,5 +1,7 @@
using Elsa.Client.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using NodaTime;
using NodaTime.Serialization.JsonNet;
using ProtoBuf;
@ -16,7 +18,15 @@ namespace ElsaDashboard.Shared.Surrogates
SerializerSettings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto
}.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
};
SerializerSettings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
SerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.All;
SerializerSettings.TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple;
SerializerSettings.TypeNameHandling = TypeNameHandling.Auto;
SerializerSettings.Converters.Add(new StringEnumConverter(new DefaultNamingStrategy()));
}
public WorkflowBlueprintSurrogate(WorkflowBlueprint value)

View file

@ -28,8 +28,9 @@ namespace Elsa.Persistence.MongoDb.Services
var tenantKeysDefinition = Builders<WorkflowInstance>.IndexKeys.Ascending(x => x.TenantId);
var versionKeysDefinition = Builders<WorkflowInstance>.IndexKeys.Ascending(x => x.Version);
var workflowStatusKeysDefinition = Builders<WorkflowInstance>.IndexKeys.Ascending(x => x.WorkflowStatus);
var workflowNameKeysDefinition = Builders<WorkflowInstance>.IndexKeys.Ascending(x => x.Name);
await CreateIndexesAsync(_mongoClient.WorkflowInstances, cancellationToken, tenantKeysDefinition, versionKeysDefinition, workflowStatusKeysDefinition);
await CreateIndexesAsync(_mongoClient.WorkflowInstances, cancellationToken, tenantKeysDefinition, versionKeysDefinition, workflowStatusKeysDefinition, workflowNameKeysDefinition);
}
private async Task CreateWorkflowDefinitionsIndexes(CancellationToken cancellationToken)
@ -41,7 +42,7 @@ namespace Elsa.Persistence.MongoDb.Services
await CreateIndexesAsync(_mongoClient.WorkflowDefinitions, cancellationToken, tenantKeysDefinition, definitionVersionIdKeysDefinition, versionKeysDefinition, nameKeysDefinition);
}
private async Task CreateWorkflowExecutionLogIndexes(CancellationToken cancellationToken)
{
var tenantKeysDefinition = Builders<WorkflowExecutionLogRecord>.IndexKeys.Ascending(x => x.TenantId);

View file

@ -22,8 +22,8 @@ namespace Elsa.Samples.Timers
.AddTimerActivities(options => options.UseQuartzProvider())
.AddWorkflow<RecurringTaskWorkflow>()
.AddWorkflow<CancelTimerWorkflow>()
//.AddWorkflow<CronTaskWorkflow>()
//.AddWorkflow(new OneOffWorkflow(SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromSeconds(5))))
.AddWorkflow<CronTaskWorkflow>()
.AddWorkflow(new OneOffWorkflow(SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromSeconds(5))))
;
});
}

View file

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using AutoMapper;
using Elsa.Models;
using Elsa.Server.Api.Models;
using Elsa.Server.Api.Services;
using Elsa.Server.Api.Swagger.Examples;
using Elsa.Services;
using Microsoft.AspNetCore.Http;
@ -19,12 +20,12 @@ namespace Elsa.Server.Api.Endpoints.WorkflowRegistry
public class Get : Controller
{
private readonly IWorkflowRegistry _workflowRegistry;
private readonly IMapper _mapper;
private readonly IWorkflowBlueprintMapper _workflowBlueprintMapper;
public Get(IWorkflowRegistry workflowRegistry, IMapper mapper)
public Get(IWorkflowRegistry workflowRegistry, IWorkflowBlueprintMapper workflowBlueprintMapper)
{
_workflowRegistry = workflowRegistry;
_mapper = mapper;
_workflowBlueprintMapper = workflowBlueprintMapper;
}
[HttpGet]
@ -44,7 +45,7 @@ namespace Elsa.Server.Api.Endpoints.WorkflowRegistry
if (workflowBlueprint == null)
return NotFound();
return _mapper.Map<WorkflowBlueprintModel>(workflowBlueprint);
return await _workflowBlueprintMapper.MapAsync(workflowBlueprint, cancellationToken);
}
}
}

View file

@ -1,4 +1,7 @@
using System.Linq;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
@ -6,8 +9,10 @@ using Elsa.Models;
using Elsa.Server.Api.Models;
using Elsa.Server.Api.Swagger.Examples;
using Elsa.Services;
using Elsa.Services.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Swashbuckle.AspNetCore.Annotations;
using Swashbuckle.AspNetCore.Filters;
@ -21,23 +26,24 @@ namespace Elsa.Server.Api.Endpoints.WorkflowRegistry
{
private readonly IWorkflowRegistry _workflowRegistry;
private readonly IMapper _mapper;
private readonly IServiceProvider _serviceProvider;
public List(IWorkflowRegistry workflowRegistry, IMapper mapper)
public List(IWorkflowRegistry workflowRegistry, IWorkflowBlueprintReflector workflowBlueprintReflector, IMapper mapper, IServiceProvider serviceProvider)
{
_workflowRegistry = workflowRegistry;
_mapper = mapper;
_serviceProvider = serviceProvider;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PagedList<WorkflowBlueprintModel>))]
[SwaggerResponseExample(StatusCodes.Status200OK, typeof(WorkflowBlueprintModelPagedListExample))]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PagedList<WorkflowBlueprintSummaryModel>))]
[SwaggerOperation(
Summary = "Returns a list of workflow blueprints.",
Description = "Returns paginated a list of workflow blueprints. When no version options are specified, the latest version is returned.",
OperationId = "WorkflowBlueprints.List",
Tags = new[] { "WorkflowBlueprints" })
Tags = new[] {"WorkflowBlueprints"})
]
public async Task<ActionResult<PagedList<WorkflowBlueprintModel>>> Handle(int? page = default, int? pageSize = default, VersionOptions? version = default, CancellationToken cancellationToken = default)
public async Task<ActionResult<PagedList<WorkflowBlueprintSummaryModel>>> Handle(int? page = default, int? pageSize = default, VersionOptions? version = default, CancellationToken cancellationToken = default)
{
version ??= VersionOptions.Latest;
var workflowBlueprints = await _workflowRegistry.GetWorkflowsAsync(cancellationToken).Where(x => x.WithVersion(version.Value)).ToListAsync(cancellationToken);
@ -45,16 +51,16 @@ namespace Elsa.Server.Api.Endpoints.WorkflowRegistry
var skip = page * pageSize;
var items = workflowBlueprints.AsEnumerable();
if (skip != null)
if (skip != null)
items = items.Skip(skip.Value);
if (pageSize != null)
items = items.Take(pageSize.Value);
var mappedItems = items.Select(x => _mapper.Map<WorkflowBlueprintModel>(x)).ToList();
var pagedList = new PagedList<WorkflowBlueprintModel>(mappedItems, page, pageSize, totalCount);
return pagedList;
using var scope = _serviceProvider.CreateScope();
var mappedItems = _mapper.Map<IEnumerable<WorkflowBlueprintSummaryModel>>(items).ToList();
return new PagedList<WorkflowBlueprintSummaryModel>(mappedItems, page, pageSize, totalCount);
}
}
}

View file

@ -28,8 +28,8 @@ namespace Elsa.Server.Api.Endpoints.WorkflowRegistry
public class ConnectionModel
{
public string SourceId { get; set; } = default!;
public string TargetId { get; set; } = default!;
public string SourceActivityId { get; set; } = default!;
public string TargetActivityId { get; set; } = default!;
public string Outcome { get; set; } = default!;
}
@ -41,5 +41,19 @@ namespace Elsa.Server.Api.Endpoints.WorkflowRegistry
public bool PersistWorkflow { get; set; }
public bool LoadWorkflowContext { get; set; }
public bool SaveWorkflowContext { get; set; }
public Variables Properties { get; set; } = new();
}
public class WorkflowBlueprintSummaryModel
{
public string Id { get; set; } = default!;
public int Version { get; set; }
public string? TenantId { get; set; }
public bool IsSingleton { get; set; }
public bool IsEnabled { get; set; }
public string? Description { get; set; }
public bool IsPublished { get; set; }
public bool IsLatest { get; set; }
public string? DisplayName { get; set; }
}
}

View file

@ -3,6 +3,7 @@ using Elsa;
using Elsa.Models;
using Elsa.Server.Api.Extensions;
using Elsa.Server.Api.Mapping;
using Elsa.Server.Api.Services;
using Elsa.Server.Api.Swagger.Examples;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Any;
@ -35,6 +36,8 @@ namespace Microsoft.Extensions.DependencyInjection
});
services.AddSingleton<ConnectionConverter>();
services.AddSingleton<ActivityBlueprintConverter>();
services.AddSingleton<IWorkflowBlueprintMapper, WorkflowBlueprintMapper>();
services.AddAutoMapperProfile<AutoMapperProfile>();
return services;
}

View file

@ -0,0 +1,36 @@
using System.Collections.Generic;
using AutoMapper;
using Elsa.Models;
using Elsa.Server.Api.Endpoints.WorkflowRegistry;
using Elsa.Services.Models;
namespace Elsa.Server.Api.Mapping
{
public class ActivityBlueprintConverter : ITypeConverter<IActivityBlueprint, ActivityBlueprintModel>
{
public const string ActivityPropertiesKey = "ActivityProperties";
public ActivityBlueprintModel Convert(IActivityBlueprint source, ActivityBlueprintModel destination, ResolutionContext context)
{
return new()
{
Id = source.Id,
Name = source.Name,
Type = source.Type,
PersistWorkflow = source.PersistWorkflow,
LoadWorkflowContext = source.LoadWorkflowContext,
SaveWorkflowContext = source.SaveWorkflowContext,
Properties = GetProperties(source, context)
};
}
private Variables GetProperties(IActivityBlueprint activityBlueprint, ResolutionContext context)
{
if (!context.Items.ContainsKey(ActivityPropertiesKey))
return new Variables();
var dictionary = (IDictionary<string, Variables>) context.Items[ActivityPropertiesKey];
return dictionary[activityBlueprint.Id];
}
}
}

View file

@ -9,7 +9,8 @@ namespace Elsa.Server.Api.Mapping
public AutoMapperProfile()
{
CreateMap<IWorkflowBlueprint, WorkflowBlueprintModel>();
CreateMap<IActivityBlueprint, ActivityBlueprintModel>();
CreateMap<IWorkflowBlueprint, WorkflowBlueprintSummaryModel>();
CreateMap<IActivityBlueprint, ActivityBlueprintModel>().ConvertUsing<ActivityBlueprintConverter>();
CreateMap<ICompositeActivityBlueprint, CompositeActivityBlueprintModel>();
CreateMap<IConnection, ConnectionModel>().ConvertUsing<ConnectionConverter>();
}

View file

@ -9,8 +9,8 @@ namespace Elsa.Server.Api.Mapping
public ConnectionModel Convert(IConnection source, ConnectionModel destination, ResolutionContext context) =>
new()
{
SourceId = source.Source.Activity.Id,
TargetId = source.Target.Activity.Id,
SourceActivityId = source.Source.Activity.Id,
TargetActivityId = source.Target.Activity.Id,
Outcome = source.Source.Outcome
};
}

View file

@ -0,0 +1,12 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Server.Api.Endpoints.WorkflowRegistry;
using Elsa.Services.Models;
namespace Elsa.Server.Api.Services
{
public interface IWorkflowBlueprintMapper
{
ValueTask<WorkflowBlueprintModel> MapAsync(IWorkflowBlueprint workflowBlueprint, CancellationToken cancellationToken = default);
}
}

View file

@ -0,0 +1,59 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using Elsa.Models;
using Elsa.Server.Api.Endpoints.WorkflowRegistry;
using Elsa.Server.Api.Mapping;
using Elsa.Services;
using Elsa.Services.Models;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Server.Api.Services
{
public class WorkflowBlueprintMapper : IWorkflowBlueprintMapper
{
private readonly IWorkflowBlueprintReflector _workflowBlueprintReflector;
private readonly IMapper _mapper;
private readonly IServiceProvider _serviceProvider;
public WorkflowBlueprintMapper(IWorkflowBlueprintReflector workflowBlueprintReflector, IMapper mapper, IServiceProvider serviceProvider)
{
_workflowBlueprintReflector = workflowBlueprintReflector;
_mapper = mapper;
_serviceProvider = serviceProvider;
}
public async ValueTask<WorkflowBlueprintModel> MapAsync(IWorkflowBlueprint workflowBlueprint, CancellationToken cancellationToken = default)
{
using var scope = _serviceProvider.CreateScope();
var wrapper = await _workflowBlueprintReflector.ReflectAsync(scope, workflowBlueprint, cancellationToken);
var activityProperties = await Task.WhenAll(wrapper.Activities.Select(async x => (x.ActivityBlueprint.Id, await GetActivityPropertiesAsync(wrapper, x, cancellationToken))));
var activityPropertyDictionary = activityProperties.ToDictionary(x => x.Id, x => x.Item2);
return _mapper.Map<WorkflowBlueprintModel>(workflowBlueprint, options => options.Items[ActivityBlueprintConverter.ActivityPropertiesKey] = activityPropertyDictionary);
}
private async ValueTask<Variables> GetActivityPropertiesAsync(IWorkflowBlueprintWrapper workflowBlueprintWrapper, IActivityBlueprintWrapper activityBlueprintWrapper, CancellationToken cancellationToken)
{
var workflowBlueprint = workflowBlueprintWrapper.WorkflowBlueprint;
var activityBlueprint = activityBlueprintWrapper.ActivityBlueprint;
var activityId = activityBlueprint.Id;
var activityPropertyValueProviders = workflowBlueprint.ActivityPropertyProviders.GetProviders(activityId);
var activityWrapper = workflowBlueprintWrapper.GetActivity(activityId)!;
var properties = new Variables();
if (activityPropertyValueProviders == null)
return properties;
foreach (var valueProvider in activityPropertyValueProviders)
{
var value = await activityWrapper.GetPropertyValueAsync(valueProvider.Key, cancellationToken);
properties.Set(valueProvider.Key, value);
}
return properties;
}
}
}

View file

@ -36,7 +36,7 @@ namespace Elsa.Server.Api.Swagger.Examples
Name = "Activity2",
}
},
Connections = new[] { new ConnectionModel { SourceId = "activity-1", TargetId = "activity-2", Outcome = OutcomeNames.Done } }
Connections = new[] { new ConnectionModel { SourceActivityId = "activity-1", TargetActivityId = "activity-2", Outcome = OutcomeNames.Done } }
};
}
}

View file

@ -1,7 +1,9 @@
using Elsa.Activities.Console;
using Elsa.Activities.ControlFlow;
using Elsa.Activities.Primitives;
using Elsa.Activities.Timers;
using Elsa.Builders;
using NodaTime;
namespace Elsa.Server.Host.Workflows
{
@ -13,6 +15,7 @@ namespace Elsa.Server.Host.Workflows
.WithDisplayName("Conditions")
.WriteLine("What is your age?")
.ReadLine()
.Timer(Duration.FromMinutes(5))
.SetVariable("Age", context => int.Parse(context.GetInput<string>()))
.IfElse(context => context.GetVariable<int>("Age") < 18, ifElse =>
{