Initial views and controllers displaying workflow instances

This commit is contained in:
Sipke Schoorstra 2019-01-06 16:52:04 +01:00
parent e619f2079e
commit 02d8cd071b
44 changed files with 478 additions and 185 deletions

View file

@ -61,6 +61,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Web.BootstrapTheme", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleDashboard.Web", "src\samples\SampleDashboard.Web\SampleDashboard.Web.csproj", "{27BD3AE7-8D3B-4D19-A8E3-4DC6922C2213}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.InMemory", "src\core\Elsa.Persistence.InMemory\Elsa.Persistence.InMemory.csproj", "{089E23E7-0B8D-4BB6-949D-EB669C116D9E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -143,6 +145,10 @@ Global
{27BD3AE7-8D3B-4D19-A8E3-4DC6922C2213}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27BD3AE7-8D3B-4D19-A8E3-4DC6922C2213}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27BD3AE7-8D3B-4D19-A8E3-4DC6922C2213}.Release|Any CPU.Build.0 = Release|Any CPU
{089E23E7-0B8D-4BB6-949D-EB669C116D9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{089E23E7-0B8D-4BB6-949D-EB669C116D9E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{089E23E7-0B8D-4BB6-949D-EB669C116D9E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{089E23E7-0B8D-4BB6-949D-EB669C116D9E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -171,6 +177,7 @@ Global
{E04F98D6-F6B3-475F-BCAA-F0D586EE3965} = {B33F1927-FF0A-4826-80C6-F480E9C43892}
{B5E36E84-1978-43C7-8859-F5184C9A657C} = {B33F1927-FF0A-4826-80C6-F480E9C43892}
{27BD3AE7-8D3B-4D19-A8E3-4DC6922C2213} = {C55090FE-8506-4FDA-928F-5ACBB9CBE019}
{089E23E7-0B8D-4BB6-949D-EB669C116D9E} = {35F44BE9-13D0-417C-A01A-F0787BEE6DC3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8B0975FD-7050-48B0-88C5-48C33378E158}

View file

@ -6,18 +6,21 @@ using Elsa.Handlers;
using Elsa.Models;
using Elsa.Results;
using Microsoft.Extensions.Localization;
using NodaTime;
namespace Elsa.Activities.Console.Drivers
{
public class ReadLineDriver : ActivityDriver<ReadLine>
{
private readonly IClock clock;
private readonly TextReader input;
public ReadLineDriver()
public ReadLineDriver(IClock clock)
{
this.clock = clock;
}
public ReadLineDriver(TextReader input) : this()
public ReadLineDriver(IClock clock, TextReader input) : this(clock)
{
this.input = input;
}
@ -25,7 +28,7 @@ namespace Elsa.Activities.Console.Drivers
protected override async Task<ActivityExecutionResult> OnExecuteAsync(ReadLine activity, WorkflowExecutionContext workflowContext, CancellationToken cancellationToken)
{
if (input == null)
return Halt();
return Halt(clock.GetCurrentInstant());
var value = await input.ReadLineAsync();
workflowContext.SetLastResult(value);

View file

@ -16,8 +16,4 @@
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Handlers" />
</ItemGroup>
</Project>

View file

@ -18,6 +18,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="1.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="NodaTime" Version="2.4.4" />
</ItemGroup>
</Project>

View file

@ -9,10 +9,20 @@ namespace Elsa.Extensions
public static bool IsDefinition(this Workflow workflow) => workflow.Metadata.ParentId == null;
public static bool IsInstance(this Workflow workflow) => !workflow.IsDefinition();
public static bool IsHalted(this Workflow workflow) =>
workflow.Status == WorkflowStatus.Halted;
public static bool IsFaulted(this Workflow workflow) =>
workflow.Status == WorkflowStatus.Aborted ||
workflow.Status == WorkflowStatus.Faulted;
public static bool IsFinished(this Workflow workflow) =>
workflow.Status == WorkflowStatus.Finished;
public static IEnumerable<IActivity> GetStartActivities(this Workflow workflow)
{
var query =
from activity in workflow.Activities
from activity in workflow.Activities
where !workflow.Connections.Select(x => x.Target.Activity).Contains(activity)
select activity;

View file

@ -2,6 +2,7 @@
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using NodaTime;
namespace Elsa.Models
{
@ -25,6 +26,11 @@ namespace Elsa.Models
[JsonConverter(typeof(StringEnumConverter))]
public WorkflowStatus Status { get; set; }
public Instant CreatedAt { get; set; }
public Instant? StartedAt { get; set; }
public Instant? HaltedAt { get; set; }
public Instant? FinishedAt { get; set; }
public IList<IActivity> Activities { get; set; } = new List<IActivity>();
public IList<Connection> Connections { get; set; } = new List<Connection>();
public Stack<WorkflowExecutionScope> Scopes { get; set; }

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NodaTime;
namespace Elsa.Models
{
@ -56,10 +57,11 @@ namespace Elsa.Models
CurrentScope.LastResult = value;
}
public void Fault(Exception exception, IActivity activity) => Fault(exception.Message, activity);
public void Fault(Exception exception, IActivity activity, Instant instant) => Fault(exception.Message, activity, instant);
public void Fault(string errorMessage, IActivity activity)
public void Fault(string errorMessage, IActivity activity, Instant instant)
{
Workflow.FinishedAt = instant;
Workflow.Fault = new WorkflowFault
{
Message = errorMessage,
@ -67,7 +69,7 @@ namespace Elsa.Models
};
}
public void Halt()
public void Halt(Instant instant)
{
var activity = CurrentActivity;
if (!Workflow.BlockingActivities.Contains(activity))
@ -75,11 +77,13 @@ namespace Elsa.Models
Workflow.BlockingActivities.Add(activity);
}
Workflow.HaltedAt = instant;
Workflow.Status = WorkflowStatus.Halted;
}
public void Finish()
public void Finish(Instant instant)
{
Workflow.FinishedAt = instant;
Workflow.Status = WorkflowStatus.Finished;
}

View file

@ -22,6 +22,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.1.1" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.1.0" />
<PackageReference Include="YamlDotNet.NetStandard" Version="4.0.0" />
</ItemGroup>

View file

@ -5,6 +5,7 @@ using Elsa.Serialization.Formatters;
using Elsa.Serialization.Tokenizers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NodaTime;
namespace Elsa.Extensions
{
@ -13,6 +14,7 @@ namespace Elsa.Extensions
public static IServiceCollection AddWorkflowsCore(this IServiceCollection services)
{
services.TryAddSingleton<IIdGenerator, DefaultIdGenerator>();
services.TryAddSingleton<IClock>(SystemClock.Instance);
services.TryAddScoped<IWorkflowSerializer, WorkflowSerializer>();
services.TryAddScoped<IWorkflowTokenizer, WorkflowTokenizer>();
services.AddScoped<IActivityHarvester, TypedActivityHarvester>();

View file

@ -1,18 +1,19 @@
using System;
using System.Collections.Generic;
using Elsa.Results;
using NodaTime;
namespace Elsa.Handlers
{
public abstract class ActivityDriver<T> : ActivityDriverBase<T> where T : IActivity
{
protected HaltResult Halt() => new HaltResult();
protected HaltResult Halt(Instant instant) => new HaltResult(instant);
protected TriggerEndpointsResult TriggerEndpoints(IEnumerable<string> names) => new TriggerEndpointsResult(names);
protected TriggerEndpointsResult TriggerEndpoint(string name) => TriggerEndpoints(new[] { name });
protected ScheduleActivityResult ScheduleActivity(IActivity activity) => new ScheduleActivityResult(activity);
protected ReturnValueResult SetReturnValue(object value) => new ReturnValueResult(value);
protected FinishWorkflowResult Finish() => new FinishWorkflowResult();
protected FaultWorkflowResult Fault(string errorMessage) => new FaultWorkflowResult(errorMessage);
protected FaultWorkflowResult Fault(Exception exception) => new FaultWorkflowResult(exception);
protected FinishWorkflowResult Finish(Instant instant) => new FinishWorkflowResult(instant);
protected FaultWorkflowResult Fault(string errorMessage, Instant instant) => new FaultWorkflowResult(errorMessage, instant);
protected FaultWorkflowResult Fault(Exception exception, Instant instant) => new FaultWorkflowResult(exception, instant);
}
}

View file

@ -1,14 +1,22 @@
using Elsa.Activities;
using Elsa.Models;
using Elsa.Results;
using NodaTime;
namespace Elsa.Handlers
{
public class UnknownActivityDriver : ActivityDriver<UnknownActivity>
{
private readonly IClock clock;
public UnknownActivityDriver(IClock clock)
{
this.clock = clock;
}
protected override ActivityExecutionResult OnExecute(UnknownActivity activity, WorkflowExecutionContext workflowContext)
{
return Fault($"Unknown activity: {activity.Name}, ID: {activity.Id}");
return Fault($"Unknown activity: {activity.Name}, ID: {activity.Id}", clock.GetCurrentInstant());
}
}
}

View file

@ -1,25 +1,28 @@
using System;
using Elsa.Models;
using NodaTime;
namespace Elsa.Results
{
public class FaultWorkflowResult : ActivityExecutionResult
{
private readonly string errorMessage;
private readonly Instant instant;
public FaultWorkflowResult(Exception exception) : this(exception.Message)
public FaultWorkflowResult(Exception exception, Instant instant) : this(exception.Message, instant)
{
}
public FaultWorkflowResult(string errorMessage)
public FaultWorkflowResult(string errorMessage, Instant instant)
{
this.errorMessage = errorMessage;
this.instant = instant;
}
protected override void Execute(IWorkflowInvoker invoker, WorkflowExecutionContext workflowContext)
{
var activity = workflowContext.CurrentActivity;
workflowContext.Fault(errorMessage, activity);
workflowContext.Fault(errorMessage, activity, instant);
}
}
}

View file

@ -1,12 +1,20 @@
using Elsa.Models;
using NodaTime;
namespace Elsa.Results
{
public class FinishWorkflowResult : ActivityExecutionResult
{
private readonly Instant instant;
public FinishWorkflowResult(Instant instant)
{
this.instant = instant;
}
protected override void Execute(IWorkflowInvoker invoker, WorkflowExecutionContext workflowContext)
{
workflowContext.Finish();
workflowContext.Finish(instant);
}
}
}

View file

@ -1,6 +1,7 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Models;
using NodaTime;
namespace Elsa.Results
{
@ -9,6 +10,13 @@ namespace Elsa.Results
/// </summary>
public class HaltResult : ActivityExecutionResult
{
private readonly Instant instant;
public HaltResult(Instant instant)
{
this.instant = instant;
}
public override async Task ExecuteAsync(IWorkflowInvoker invoker, WorkflowExecutionContext workflowContext, CancellationToken cancellationToken)
{
if (workflowContext.IsFirstPass)
@ -21,7 +29,7 @@ namespace Elsa.Results
}
else
{
workflowContext.Halt();
workflowContext.Halt(instant);
}
}
}

View file

@ -9,6 +9,8 @@ using Elsa.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using NodaTime;
using NodaTime.Serialization.JsonNet;
namespace Elsa.Serialization.Tokenizers
{
@ -22,7 +24,13 @@ namespace Elsa.Serialization.Tokenizers
{
this.tokenizerInvoker = tokenizerInvoker;
this.activityLibrary = activityLibrary;
jsonSerializer = JsonSerializer.Create(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });
jsonSerializer = JsonSerializer.Create(
new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
}
.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb));
}
public async Task<JToken> TokenizeWorkflowAsync(Workflow value, CancellationToken cancellationToken)
@ -89,13 +97,13 @@ namespace Elsa.Serialization.Tokenizers
var activityModel = JObject.FromObject(value, jsonSerializer);
return Task.FromResult<JToken>(activityModel);
}
public async Task<IActivity> DetokenizeActivityAsync(JToken token, CancellationToken cancellationToken)
{
var name = token["name"].Value<string>();
var descriptor = await GetActivityDescriptorAsync(name, cancellationToken);
var activityType = descriptor?.ActivityType ?? typeof(UnknownActivity);
var activity = (IActivity)token.ToObject(activityType);
var activity = (IActivity) token.ToObject(activityType);
activity.Descriptor = descriptor;
return activity;

View file

@ -6,16 +6,22 @@ using System.Threading.Tasks;
using Elsa.Models;
using Elsa.Results;
using Microsoft.Extensions.Logging;
using NodaTime;
namespace Elsa
{
public class WorkflowInvoker : IWorkflowInvoker
{
private readonly IClock clock;
private readonly ILogger logger;
public WorkflowInvoker(IActivityInvoker activityInvoker, ILogger<WorkflowInvoker> logger)
public WorkflowInvoker(
IActivityInvoker activityInvoker,
IClock clock,
ILogger<WorkflowInvoker> logger)
{
ActivityInvoker = activityInvoker;
this.clock = clock;
this.logger = logger;
}
@ -31,6 +37,9 @@ namespace Elsa
workflow.BlockingActivities.Remove(startActivity);
else
startActivity = workflow.Activities.First();
if (!isResuming)
workflow.StartedAt = clock.GetCurrentInstant();
workflowExecutionContext.Workflow.Status = WorkflowStatus.Executing;
workflowExecutionContext.ScheduleActivity(startActivity);
@ -50,7 +59,7 @@ namespace Elsa
}
if(workflowExecutionContext.Workflow.Status != WorkflowStatus.Halted)
workflowExecutionContext.Finish();
workflowExecutionContext.Finish(clock.GetCurrentInstant());
return workflowExecutionContext;
}
@ -88,7 +97,7 @@ namespace Elsa
ex,
"An unhandled error occurred while executing an activity. Putting the workflow in the faulted state."
);
workflowContext.Fault(ex, activity);
workflowContext.Fault(ex, activity, clock.GetCurrentInstant());
}
private async Task<ActivityExecutionResult> ExecuteOrResumeActivityAsync(WorkflowExecutionContext workflowContext, IActivity activity, bool isResuming, CancellationToken cancellationToken)

View file

@ -14,7 +14,11 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Abstractions\Elsa.Abstractions.csproj"/>
<ProjectReference Include="..\Elsa.Abstractions\Elsa.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NodaTime" Version="2.4.4" />
</ItemGroup>
</Project>

View file

@ -10,5 +10,9 @@ namespace Elsa.Persistence
void Visit(WorkflowIsBlockedOnActivity specification);
void Visit(WorkflowIsDefinition specification);
void Visit(WorkflowIsInstance specification);
void Visit(WorkflowIsInstanceOf specification);
void Visit(WorkflowIsFinished specification);
void Visit(WorkflowIsHalted specification);
void Visit(WorkflowIsFaulted specification);
}
}

View file

@ -14,4 +14,4 @@
public void Accept(TVisitor visitor) => visitor.Visit(this);
public bool IsSatisfiedBy(T obj) => Left.IsSatisfiedBy(obj) && Right.IsSatisfiedBy(obj);
}
}
}

View file

@ -14,4 +14,4 @@
public void Accept(TVisitor visitor) => visitor.Visit(this);
public bool IsSatisfiedBy(T obj) => Left.IsSatisfiedBy(obj) || Right.IsSatisfiedBy(obj);
}
}
}

View file

@ -1,4 +1,5 @@
using System.Linq;
using Elsa.Extensions;
using Elsa.Models;
namespace Elsa.Persistence.Specifications
@ -16,7 +17,7 @@ namespace Elsa.Persistence.Specifications
{
var query =
from activity in value.BlockingActivities
where activity.Name == ActivityName
where value.IsInstance() && activity.Name == ActivityName
select activity;
return query.Any();

View file

@ -0,0 +1,22 @@
using Elsa.Extensions;
using Elsa.Models;
namespace Elsa.Persistence.Specifications
{
public class WorkflowIsFaulted : ISpecification<Workflow, IWorkflowSpecificationVisitor>
{
private readonly string parentId;
public WorkflowIsFaulted(string parentId)
{
this.parentId = parentId;
}
public bool IsSatisfiedBy(Workflow item) => item.Metadata.ParentId == parentId && item.IsFaulted();
public void Accept(IWorkflowSpecificationVisitor visitor)
{
visitor.Visit(this);
}
}
}

View file

@ -0,0 +1,22 @@
using Elsa.Extensions;
using Elsa.Models;
namespace Elsa.Persistence.Specifications
{
public class WorkflowIsFinished : ISpecification<Workflow, IWorkflowSpecificationVisitor>
{
private readonly string parentId;
public WorkflowIsFinished(string parentId)
{
this.parentId = parentId;
}
public bool IsSatisfiedBy(Workflow item) => item.Metadata.ParentId == parentId && item.IsFinished();
public void Accept(IWorkflowSpecificationVisitor visitor)
{
visitor.Visit(this);
}
}
}

View file

@ -0,0 +1,22 @@
using Elsa.Extensions;
using Elsa.Models;
namespace Elsa.Persistence.Specifications
{
public class WorkflowIsHalted : ISpecification<Workflow, IWorkflowSpecificationVisitor>
{
private readonly string parentId;
public WorkflowIsHalted(string parentId)
{
this.parentId = parentId;
}
public bool IsSatisfiedBy(Workflow item) => item.Metadata.ParentId == parentId && item.IsHalted();
public void Accept(IWorkflowSpecificationVisitor visitor)
{
visitor.Visit(this);
}
}
}

View file

@ -0,0 +1,21 @@
using Elsa.Models;
namespace Elsa.Persistence.Specifications
{
public class WorkflowIsInstanceOf : ISpecification<Workflow, IWorkflowSpecificationVisitor>
{
private readonly string parentId;
public WorkflowIsInstanceOf(string parentId)
{
this.parentId = parentId;
}
public bool IsSatisfiedBy(Workflow item) => item.Metadata.ParentId == parentId;
public void Accept(IWorkflowSpecificationVisitor visitor)
{
visitor.Visit(this);
}
}
}

View file

@ -1,4 +1,5 @@
using System.Linq;
using Elsa.Extensions;
using Elsa.Models;
namespace Elsa.Persistence.Specifications
@ -15,9 +16,10 @@ namespace Elsa.Persistence.Specifications
public bool IsSatisfiedBy(Workflow value)
{
var query =
from activity in value.Activities
where activity.Name == ActivityName
where !value.Connections.Select(x => x.Target.Activity).Contains(activity)
from activity in value.Activities
where value.IsDefinition()
&& activity.Name == ActivityName
&& !value.Connections.Select(x => x.Target.Activity).Contains(activity)
select activity;
return query.Any();
@ -28,4 +30,4 @@ namespace Elsa.Persistence.Specifications
visitor.Visit(this);
}
}
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageVersion>1.0.0</PackageVersion>
<Authors>Sipke Schoorstra</Authors>
<Description>Elsa is a set of workflowing libraries and tools to enable super-fast workflowing capabilities in any .NET Core application.</Description>

View file

@ -7,6 +7,7 @@ using Elsa.Models;
using Elsa.Persistence.FileSystem.Options;
using Elsa.Serialization;
using Microsoft.Extensions.Options;
using NodaTime;
namespace Elsa.Persistence.FileSystem
{
@ -15,14 +16,21 @@ namespace Elsa.Persistence.FileSystem
private readonly IFileSystem fileSystem;
private readonly IIdGenerator idGenerator;
private readonly IWorkflowSerializer workflowSerializer;
private readonly IClock clock;
private readonly string rootDirectory;
private readonly string format;
public FileSystemWorkflowStore(IOptions<FileSystemStoreOptions> options, IFileSystem fileSystem, IIdGenerator idGenerator, IWorkflowSerializer workflowSerializer)
public FileSystemWorkflowStore(
IOptions<FileSystemStoreOptions> options,
IFileSystem fileSystem,
IIdGenerator idGenerator,
IWorkflowSerializer workflowSerializer,
IClock clock)
{
this.fileSystem = fileSystem;
this.idGenerator = idGenerator;
this.workflowSerializer = workflowSerializer;
this.clock = clock;
rootDirectory = options.Value.RootDirectory;
format = options.Value.Format;
}
@ -47,6 +55,7 @@ namespace Elsa.Persistence.FileSystem
var id = $"{idGenerator.Generate()}{fileExtension}";
value.Metadata.Id = id;
value.CreatedAt = clock.GetCurrentInstant();
await UpdateAsync(value, cancellationToken);
}

View file

@ -3,13 +3,19 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Models;
using NodaTime;
namespace Elsa.Persistence.InMemory
{
public class InMemoryWorkflowStore : IWorkflowStore
{
public InMemoryWorkflowStore()
private readonly IIdGenerator idGenerator;
private readonly IClock clock;
public InMemoryWorkflowStore(IIdGenerator idGenerator, IClock clock)
{
this.idGenerator = idGenerator;
this.clock = clock;
Workflows = new List<Workflow>();
}
@ -31,6 +37,9 @@ namespace Elsa.Persistence.InMemory
public Task AddAsync(Workflow value, CancellationToken cancellationToken)
{
value.Metadata.Id = idGenerator.Generate();
value.CreatedAt = clock.GetCurrentInstant();
Workflows.Add(value);
return Task.CompletedTask;
}

View file

@ -1,12 +1,15 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Extensions;
using Elsa.Models;
using Elsa.Persistence;
using Elsa.Persistence.Specifications;
using Elsa.Serialization;
using Elsa.Serialization.Extensions;
using Elsa.Web.Management.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json.Linq;
@ -34,14 +37,47 @@ namespace Elsa.Web.Management.Controllers
}
[HttpGet]
public async Task<IActionResult> Index(CancellationToken cancellationToken)
public async Task<IActionResult> Definitions(CancellationToken cancellationToken)
{
var workflows = await workflowStore.GetManyAsync(new AllWorkflows(), cancellationToken);
return View(workflows.ToList());
var workflows = await workflowStore.GetManyAsync(new WorkflowIsDefinition(), cancellationToken).ToListAsync();
var viewModels = await Task.WhenAll(workflows.Select(async x => await CreateWorkflowSummaryViewModelAsync(x, cancellationToken)).ToList());
return View(viewModels);
}
[HttpGet("{parentId}/finished")]
public Task<IActionResult> Finished(string parentId, CancellationToken cancellationToken)
{
return Instances(parentId, new WorkflowIsFinished(parentId), cancellationToken);
}
[HttpGet("{parentId}/halted")]
public Task<IActionResult> Halted(string parentId, CancellationToken cancellationToken)
{
return Instances(parentId, new WorkflowIsHalted(parentId), cancellationToken);
}
[HttpGet("{parentId}/faulted")]
public Task<IActionResult> Faulted(string parentId, CancellationToken cancellationToken)
{
return Instances(parentId, new WorkflowIsFaulted(parentId), cancellationToken);
}
private async Task<IActionResult> Instances(string parentId, ISpecification<Workflow, IWorkflowSpecificationVisitor> specification, CancellationToken cancellationToken)
{
var definition = await workflowStore.GetAsync(parentId, cancellationToken);
var workflows = await workflowStore.GetManyAsync(specification, cancellationToken).ToListAsync();
var viewModel = new WorkflowInstancesViewModel
{
Definition = definition,
Instances = workflows
};
return View("Instances", viewModel);
}
[HttpGet("new")]
public IActionResult New(CancellationToken cancellationToken)
public IActionResult New()
{
var workflow = new Workflow
{
@ -65,14 +101,14 @@ namespace Elsa.Web.Management.Controllers
});
}
[HttpGet("{id}")]
[HttpGet("{id}/edit")]
public async Task<IActionResult> Edit(string id, CancellationToken cancellationToken)
{
var workflow = await workflowStore.GetAsync(id, cancellationToken);
return View(workflow);
}
[HttpPut("{id}")]
[HttpPut("{id}/update")]
public async Task<IActionResult> Update(string id, [FromBody] JToken workflowData, CancellationToken cancellationToken)
{
var workflow = await workflowSerializer.DeserializeAsync(workflowData, cancellationToken);
@ -91,5 +127,25 @@ namespace Elsa.Web.Management.Controllers
var contentType = formatter.ContentType;
return File(bytes, contentType, fileName);
}
[HttpGet("{id}/details")]
public async Task<IActionResult> Details(string id, CancellationToken cancellationToken)
{
var workflow = await workflowStore.GetAsync(id, cancellationToken);
return View(workflow);
}
private async Task<WorkflowSummaryViewModel> CreateWorkflowSummaryViewModelAsync(Workflow workflow, CancellationToken cancellationToken)
{
var instances = await workflowStore.GetManyAsync(new WorkflowIsInstanceOf(workflow.Metadata.Id), cancellationToken).ToListAsync();
return new WorkflowSummaryViewModel
{
Workflow = workflow,
NumHalted = instances.Count(x => x.IsHalted()),
NumFaulted = instances.Count(x => x.IsFaulted()),
NumFinished = instances.Count(x => x.IsFinished())
};
}
}
}

View file

@ -13,18 +13,18 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OrchardCore" Version="1.0.0-beta3-70362"/>
<PackageReference Include="OrchardCore.FileStorage.FileSystem" Version="1.0.0-beta3-70362"/>
<PackageReference Include="OrchardCore.Module.Targets" Version="1.0.0-beta3-70362"/>
<PackageReference Include="OrchardCore.Mvc.Core" Version="1.0.0-beta3-70362"/>
<PackageReference Include="OrchardCore.DisplayManagement" Version="1.0.0-beta3-70362"/>
<PackageReference Include="OrchardCore.ResourceManagement" Version="1.0.0-beta3-70362"/>
<PackageReference Include="OrchardCore.Infrastructure" Version="1.0.0-beta3-70362"/>
<PackageReference Include="OrchardCore.Settings" Version="1.0.0-beta3-70362"/>
<PackageReference Include="OrchardCore" Version="1.0.0-beta3-70362" />
<PackageReference Include="OrchardCore.FileStorage.FileSystem" Version="1.0.0-beta3-70362" />
<PackageReference Include="OrchardCore.Module.Targets" Version="1.0.0-beta3-70362" />
<PackageReference Include="OrchardCore.Mvc.Core" Version="1.0.0-beta3-70362" />
<PackageReference Include="OrchardCore.DisplayManagement" Version="1.0.0-beta3-70362" />
<PackageReference Include="OrchardCore.ResourceManagement" Version="1.0.0-beta3-70362" />
<PackageReference Include="OrchardCore.Infrastructure" Version="1.0.0-beta3-70362" />
<PackageReference Include="OrchardCore.Settings" Version="1.0.0-beta3-70362" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Web.Components\Elsa.Web.Components.csproj"/>
<ProjectReference Include="..\Elsa.Web.Components\Elsa.Web.Components.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,11 @@
using System.Collections.Generic;
using Elsa.Models;
namespace Elsa.Web.Management.ViewModels
{
public class WorkflowInstancesViewModel
{
public Workflow Definition { get; set; }
public ICollection<Workflow> Instances { get; set; }
}
}

View file

@ -0,0 +1,12 @@
using Elsa.Models;
namespace Elsa.Web.Management.ViewModels
{
public class WorkflowSummaryViewModel
{
public Workflow Workflow { get; set; }
public int NumHalted { get; set; }
public int NumFaulted { get; set; }
public int NumFinished { get; set; }
}
}

View file

@ -0,0 +1,73 @@
@model ICollection<WorkflowSummaryViewModel>
<div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">@T["Workflows"]</h1>
<div class="btn-toolbar mb-2 mb-md-1">
<nav class="admin-toolbar">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="btn btn-sm btn-primary" asp-action="New">@T["Create Workflow"]</a>
</li>
</ul>
</nav>
</div>
</div>
<table id="dataTable" class="table table-striped table-bordered dataTable" width="100%" role="grid" aria-describedby="dataTable_info" style="width: 100%;" data-turbolinks="false">
<thead>
<tr role="row">
<th class="sorting" tabindex="0" aria-controls="dataTable" aria-label="Id: activate to sort column ascending" style="width: 214px;">ID</th>
<th class="sorting_asc" tabindex="0" aria-controls="dataTable" aria-sort="ascending" aria-label="Name: activate to sort column descending" style="width: 281px;">Name</th>
<th class="sorting" tabindex="0" aria-controls="dataTable" aria-label="Description: activate to sort column ascending" style="width: 313px;">Description</th>
<th tabindex="0" aria-controls="dataTable" style="width: 314px;">Execution</th>
<th tabindex="0" aria-controls="dataTable" style="width: 214px;">Actions</th>
</tr>
</thead>
<tfoot>
<tr>
<th rowspan="1" colspan="1">ID</th>
<th rowspan="1" colspan="1">Name</th>
<th rowspan="1" colspan="1">Description</th>
<th rowspan="1" colspan="1">Execution</th>
<th rowspan="1" colspan="1">Actions</th>
</tr>
</tfoot>
<tbody>
@{
var index = 0;
}
@foreach (var workflowSummary in Model)
{
var workflowDefinition = workflowSummary.Workflow;
var workflowId = workflowDefinition.Metadata.Id;
var rowClass = index % 2 == 0 ? "even" : "odd";
index++;
<tr role="row" class="@rowClass">
<td>
<a asp-action="Edit" asp-route-id="@workflowId">@workflowId</a>
</td>
<td class="sorting_1">@workflowDefinition.Metadata.Name</td>
<td>@workflowDefinition.Metadata.Description</td>
<td>
@if (workflowSummary.NumHalted > 0)
{
<a asp-action="Halted" asp-route-parentId="@workflowId" class="btn btn-sm btn-info">@T["Halted"] <span class="badge badge-light">@workflowSummary.NumHalted</span></a>
}
@if (workflowSummary.NumFaulted > 0)
{
<a asp-action="Faulted" asp-route-parentId="@workflowId" class="btn btn-sm btn-danger">@T["Faulted"] <span class="badge badge-light">@workflowSummary.NumFaulted</span></a>
}
@if (workflowSummary.NumFinished > 0)
{
<a asp-action="Finished" asp-route-parentId="@workflowId" class="btn btn-sm btn-success">@T["Finished"] <span class="badge badge-light">@workflowSummary.NumFinished</span></a>
}
</td>
<td><a asp-action="Edit" asp-route-id="@workflowDefinition.Metadata.Id">@T["edit"]</a> | delete | enable</td>
</tr>
}
</tbody>
</table>
</div>

View file

@ -0,0 +1,2 @@
@model Workflow
@Html.Partial("WorkflowEditor")

View file

@ -1,2 +1,2 @@
@model Elsa.Models.Workflow
@model Workflow
@Html.Partial("WorkflowEditor")

View file

@ -1,48 +0,0 @@
@model ICollection<Elsa.Models.Workflow>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="bgc-white bd bdrs-3 p-20 mB-20">
<h4 class="c-grey-900 mB-20">Workflow Definitions</h4>
<table id="dataTable" class="table table-striped table-bordered dataTable" cellspacing="0" width="100%" role="grid" aria-describedby="dataTable_info" style="width: 100%;" data-turbolinks="false">
<thead>
<tr role="row">
<th class="sorting" tabindex="0" aria-controls="dataTable" rowspan="1" colspan="1" aria-label="Id: activate to sort column ascending" style="width: 214px;">ID</th>
<th class="sorting_asc" tabindex="0" aria-controls="dataTable" rowspan="1" colspan="1" aria-sort="ascending" aria-label="Name: activate to sort column descending" style="width: 281px;">Name</th>
<th class="sorting" tabindex="0" aria-controls="dataTable" rowspan="1" colspan="1" aria-label="Position: activate to sort column ascending" style="width: 413px;">Description</th>
<th class="sorting" tabindex="0" aria-controls="dataTable" rowspan="1" colspan="1" aria-label="Office: activate to sort column ascending" style="width: 214px;">Actions</th>
</tr>
</thead>
<tfoot>
<tr>
<th rowspan="1" colspan="1">ID</th>
<th rowspan="1" colspan="1">Name</th>
<th rowspan="1" colspan="1">Description</th>
<th rowspan="1" colspan="1">Actions</th>
</tr>
</tfoot>
<tbody>
@{
var index = 0;
}
@foreach (var workflowDefinition in Model)
{
var rowClass = index % 2 == 0 ? "even" : "odd";
index++;
<tr role="row" class="@rowClass">
<td>
<a asp-action="Edit" asp-route-id="@workflowDefinition.Metadata.Id">@workflowDefinition.Metadata.Id</a>
</td>
<td class="sorting_1">@workflowDefinition.Metadata.Name</td>
<td>@workflowDefinition.Metadata.Description</td>
<td><a asp-action="Edit" asp-route-id="@workflowDefinition.Metadata.Id">edit</a> | delete | enable</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,50 @@
@model WorkflowInstancesViewModel
<div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
@Model.Definition.Metadata.Name
<small class="text-muted">@T["Finished Workflows"]</small>
</h1>
</div>
<table id="dataTable" class="table table-striped table-bordered dataTable" width="100%" role="grid" aria-describedby="dataTable_info" style="width: 100%;" data-turbolinks="false">
<thead>
<tr role="row">
<th class="sorting" tabindex="0" aria-controls="dataTable" aria-label="Id: activate to sort column ascending" style="width: 214px;">ID</th>
<th class="sorting_asc" tabindex="0" aria-controls="dataTable" aria-sort="ascending" aria-label="Name: activate to sort column descending" style="width: 281px;">Started</th>
<th class="sorting" tabindex="0" aria-controls="dataTable" aria-label="Description: activate to sort column ascending" style="width: 313px;">Finished</th>
<th tabindex="0" aria-controls="dataTable" style="width: 214px;">Actions</th>
</tr>
</thead>
<tfoot>
<tr>
<th rowspan="1" colspan="1">ID</th>
<th rowspan="1" colspan="1">Started</th>
<th rowspan="1" colspan="1">Finished</th>
<th rowspan="1" colspan="1">Actions</th>
</tr>
</tfoot>
<tbody>
@{
var index = 0;
}
@foreach (var workflow in Model.Instances)
{
var workflowId = workflow.Metadata.Id;
var rowClass = index % 2 == 0 ? "even" : "odd";
index++;
<tr role="row" class="@rowClass">
<td>
<a asp-action="Details" asp-route-id="@workflowId">@workflowId</a>
</td>
<td class="sorting_1">@workflow.StartedAt</td>
<td class="sorting_1">@workflow.FinishedAt</td>
<td><a asp-action="Details" asp-route-id="@workflowId">@T["details"]</a> | @T["delete"]</td>
</tr>
}
</tbody>
</table>
</div>

View file

@ -1,3 +1,5 @@
@using Elsa.Models
@using Elsa.Web.Management.ViewModels
@inherits OrchardCore.DisplayManagement.Razor.RazorPage<TModel>
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Elsa.Web.Components

View file

@ -8,23 +8,29 @@ using Elsa.Serialization;
using Elsa.Serialization.Extensions;
using Elsa.Serialization.Formatters;
using Elsa.Web.Persistence.FileSystem.Extensions;
using NodaTime;
using OrchardCore.FileStorage;
namespace Elsa.Web.Persistence.FileSystem.Services
{
public class FileSystemWorkflowStore : IWorkflowStore
{
private const string Format = YamlTokenFormatter.FormatName;
private readonly IWorkflowsFileStore fileStore;
private readonly IWorkflowSerializer workflowSerializer;
private readonly IIdGenerator idGenerator;
private const string Format = YamlTokenFormatter.FormatName;
private const string RootDirectory = "workflows";
private readonly IClock clock;
public FileSystemWorkflowStore(IWorkflowsFileStore fileStore, IWorkflowSerializer workflowSerializer, IIdGenerator idGenerator)
public FileSystemWorkflowStore(
IWorkflowsFileStore fileStore,
IWorkflowSerializer workflowSerializer,
IIdGenerator idGenerator,
IClock clock)
{
this.fileStore = fileStore;
this.workflowSerializer = workflowSerializer;
this.idGenerator = idGenerator;
this.clock = clock;
}
public async Task<IEnumerable<Workflow>> GetManyAsync(ISpecification<Workflow, IWorkflowSpecificationVisitor> specification, CancellationToken cancellationToken)
@ -45,7 +51,8 @@ namespace Elsa.Web.Persistence.FileSystem.Services
{
var fileExtension = $".{Format.ToLower()}";
var id = $"{idGenerator.Generate()}{fileExtension}";
value.CreatedAt = clock.GetCurrentInstant();
value.Metadata.Id = id;
await UpdateAsync(value, cancellationToken);
@ -53,10 +60,9 @@ namespace Elsa.Web.Persistence.FileSystem.Services
public async Task UpdateAsync(Workflow value, CancellationToken cancellationToken)
{
await EnsureWorkflowsDirectoryAsync();
var data = await workflowSerializer.SerializeAsync(value, Format, cancellationToken);
var fileName = value.Metadata.Id;
var path = fileStore.Combine(RootDirectory, fileName);
var path = fileName;
using (var stream = data.ToStream())
{
@ -66,8 +72,7 @@ namespace Elsa.Web.Persistence.FileSystem.Services
private async Task<IEnumerable<Workflow>> ListAsync(CancellationToken cancellationToken)
{
await EnsureWorkflowsDirectoryAsync();
var files = await fileStore.GetDirectoryContentAsync(RootDirectory);
var files = await fileStore.GetDirectoryContentAsync();
var loadTasks = files.Select(x => LoadWorkflowDefinitionAsync(x, cancellationToken));
return await Task.WhenAll(loadTasks);
}
@ -79,10 +84,5 @@ namespace Elsa.Web.Persistence.FileSystem.Services
workflow.Metadata.Id = file.Name;
return workflow;
}
private async Task EnsureWorkflowsDirectoryAsync()
{
await fileStore.TryCreateDirectoryAsync(RootDirectory);
}
}
}

View file

@ -20,7 +20,7 @@ namespace Elsa.Web.Persistence.FileSystem
{
var shellOptions = serviceProvider.GetRequiredService<IOptions<ShellOptions>>().Value;
var shellSettings = serviceProvider.GetRequiredService<ShellSettings>();
var mediaPath = Path.Combine(shellOptions.ShellsApplicationDataPath, shellOptions.ShellsContainerName, shellSettings.Name, "elsa");
var mediaPath = Path.Combine(shellOptions.ShellsApplicationDataPath, shellOptions.ShellsContainerName, shellSettings.Name, "workflows");
var fileStore = new FileSystemStore(mediaPath);
return new WorkflowsFileStore(fileStore);

View file

@ -13,27 +13,26 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OrchardCore.DisplayManagement" Version="1.0.0-beta3-70362"/>
<PackageReference Include="OrchardCore.ResourceManagement" Version="1.0.0-beta3-70362"/>
<PackageReference Include="OrchardCore.Theme.Targets" Version="1.0.0-beta3-70362"/>
<PackageReference Include="OrchardCore.DisplayManagement" Version="1.0.0-beta3-70362" />
<PackageReference Include="OrchardCore.ResourceManagement" Version="1.0.0-beta3-70362" />
<PackageReference Include="OrchardCore.Theme.Targets" Version="1.0.0-beta3-70362" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="Assets\node_modules\**"/>
<Compile Remove="Assets\node_modules\**"/>
<Folder Remove="Assets\node_modules\**"/>
<EmbeddedResource Remove="Assets\node_modules\**" />
<Compile Remove="Assets\node_modules\**" />
<Folder Remove="Assets\node_modules\**" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\scripts\dashboard.ts"/>
<Content Include="Assets\styles\dashboard.scss"/>
<Content Include="Views\Elsa.Web.Management\Workflows\New.cshtml"/>
<Content Include="Views\Elsa.Web.Management\Workflows\Edit.cshtml"/>
<Content Include="Views\Elsa.Web.Management\Workflows\Index.cshtml"/>
<Content Include="Assets\scripts\dashboard.ts" />
<Content Include="Assets\styles\dashboard.scss" />
<Content Include="Views\Elsa.Web.Management\Workflows\New.cshtml" />
<Content Include="Views\Elsa.Web.Management\Workflows\Edit.cshtml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\modules\Elsa.Web.Management\Elsa.Web.Management.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Web.Management\Elsa.Web.Management.csproj" />
</ItemGroup>
</Project>

View file

@ -1,55 +0,0 @@
@model ICollection<Elsa.Models.Workflow>
<div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">@T["Workflows"]</h1>
<div class="btn-toolbar mb-2 mb-md-1">
<nav class="admin-toolbar">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="btn btn-sm btn-primary" asp-action="New">@T["Create Workflow"]</a>
</li>
</ul>
</nav>
</div>
</div>
<table id="dataTable" class="table table-striped table-bordered dataTable" width="100%" role="grid" aria-describedby="dataTable_info" style="width: 100%;" data-turbolinks="false">
<thead>
<tr role="row">
<th class="sorting" tabindex="0" aria-controls="dataTable" rowspan="1" colspan="1" aria-label="Id: activate to sort column ascending" style="width: 214px;">ID</th>
<th class="sorting_asc" tabindex="0" aria-controls="dataTable" rowspan="1" colspan="1" aria-sort="ascending" aria-label="Name: activate to sort column descending" style="width: 281px;">Name</th>
<th class="sorting" tabindex="0" aria-controls="dataTable" rowspan="1" colspan="1" aria-label="Position: activate to sort column ascending" style="width: 413px;">Description</th>
<th class="sorting" tabindex="0" aria-controls="dataTable" rowspan="1" colspan="1" aria-label="Office: activate to sort column ascending" style="width: 214px;">Actions</th>
</tr>
</thead>
<tfoot>
<tr>
<th rowspan="1" colspan="1">ID</th>
<th rowspan="1" colspan="1">Name</th>
<th rowspan="1" colspan="1">Description</th>
<th rowspan="1" colspan="1">Actions</th>
</tr>
</tfoot>
<tbody>
@{
var index = 0;
}
@foreach (var workflowDefinition in Model)
{
var rowClass = index % 2 == 0 ? "even" : "odd";
index++;
<tr role="row" class="@rowClass">
<td>
<a asp-area="Elsa.Web.Management" asp-controller="Workflows" asp-action="Edit" asp-route-id="@workflowDefinition.Metadata.Id">@workflowDefinition.Metadata.Id</a>
</td>
<td class="sorting_1">@workflowDefinition.Metadata.Name</td>
<td>@workflowDefinition.Metadata.Description</td>
<td><a asp-area="Elsa.Web.Management" asp-controller="Workflows" asp-action="Edit" asp-route-id="@workflowDefinition.Metadata.Id">edit</a> | delete | enable</td>
</tr>
}
</tbody>
</table>
</div>

View file

@ -8,7 +8,7 @@
</a>
</li>
<li class="nav-item">
<a class="nav-link" asp-action="Index" asp-controller="Workflows" asp-area="Elsa.Web.Management">
<a class="nav-link" asp-action="Definitions" asp-controller="Workflows" asp-area="Elsa.Web.Management">
<span data-feather="file"></span>
@T["Workflows"]
</a>