diff --git a/README.md b/README.md index 9a32f4d5b..f36bb53be 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ +![Web-based workflow designer](/doc/elsa-cover.png) + ## Elsa Workflows Elsa Workflows is a set of lean & mean workflow components that you can use in your .NET Core applications. With Elsa, you can invoke and trigger workflows from your own application. Workflows can be expressed as JSON, YAML, XML or in code. + You can manually handcraft workflows or use the web-based workflow designer. -![Web-based workflow designer](/doc/workflow-sample-1.png) +![Web-based workflow designer](/doc/workflow-sample-1.png) ## Why Elsa Workflows? @@ -112,3 +115,4 @@ In order to run Elsa on your local machine, follow these steps: - Implement more activities (fork, join, script, HTTP request, loops, etc.) - Implement integration with Orchard Core (separate repo) - Detailed documentation + diff --git a/doc/elsa-cover.png b/doc/elsa-cover.png new file mode 100644 index 000000000..62b084b01 Binary files /dev/null and b/doc/elsa-cover.png differ diff --git a/src/activities/Elsa.Activities.Http/Activities/HttpRequestAction.cs b/src/activities/Elsa.Activities.Http/Activities/HttpRequestAction.cs index b0faf6295..46617b76d 100644 --- a/src/activities/Elsa.Activities.Http/Activities/HttpRequestAction.cs +++ b/src/activities/Elsa.Activities.Http/Activities/HttpRequestAction.cs @@ -19,13 +19,18 @@ namespace Elsa.Activities.Http.Activities public string Method { get; set; } /// - /// The body to send along with the request + /// The body to send along with the request. /// public WorkflowExpression Body { get; set; } /// - /// The body to send along with the request + /// The headers to send along with the request. /// public IDictionary RequestHeaders { get; set; } + + /// + /// A list of HTTP status codes this activity cam handle. + /// + public ICollection SupportedStatusCodes { get; set; } } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Activities/HttpResponseAction.cs b/src/activities/Elsa.Activities.Http/Activities/HttpResponseAction.cs new file mode 100644 index 000000000..8f4c26263 --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Activities/HttpResponseAction.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Net; +using Elsa.Expressions; +using Elsa.Models; + +namespace Elsa.Activities.Http.Activities +{ + public class HttpResponseAction : Activity + { + /// + /// The HTTP status code to return. + /// + public HttpStatusCode StatusCode { get; set; } + + /// + /// The body to send along with the response + /// + public WorkflowExpression Body { get; set; } + + /// + /// The Content-Type header to send along with the response. + /// + public WorkflowExpression ContentType { get; set; } + + /// + /// The headers to send along with the response, one header: value pair per line. + /// + public WorkflowExpression ResponseHeaders { get; set; } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/ActivityDescriptors.cs b/src/activities/Elsa.Activities.Http/ActivityDescriptors.cs index 7799378e1..da1547108 100644 --- a/src/activities/Elsa.Activities.Http/ActivityDescriptors.cs +++ b/src/activities/Elsa.Activities.Http/ActivityDescriptors.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; +using System.Linq; using Elsa.Activities.Http.Activities; using Elsa.Models; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; namespace Elsa.Activities.Http @@ -23,10 +25,18 @@ namespace Elsa.Activities.Http T["Triggers when an incoming HTTP request is received."], T["Done"]); - yield return ActivityDescriptor.ForAction( + yield return ActivityDescriptor.For( Category, T["HTTP Request"], T["Execute a HTTP request."], + true, + true, + a => a.SupportedStatusCodes.Select(x => T[x.ToString()])); + + yield return ActivityDescriptor.ForAction( + Category, + T["HTTP Response"], + T["Write a HTTP response."], T["Done"]); } } diff --git a/src/activities/Elsa.Activities.Http/Drivers/HttpResponseActionDriver.cs b/src/activities/Elsa.Activities.Http/Drivers/HttpResponseActionDriver.cs new file mode 100644 index 000000000..1921f45c6 --- /dev/null +++ b/src/activities/Elsa.Activities.Http/Drivers/HttpResponseActionDriver.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Activities.Http.Activities; +using Elsa.Expressions; +using Elsa.Handlers; +using Elsa.Models; +using Elsa.Results; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Activities.Http.Drivers +{ + public class HttpResponseActionDriver : ActivityDriver + { + private readonly IWorkflowExpressionEvaluator expressionEvaluator; + private readonly IHttpContextAccessor httpContextAccessor; + + public HttpResponseActionDriver(IWorkflowExpressionEvaluator expressionEvaluator, IHttpContextAccessor httpContextAccessor) + { + this.expressionEvaluator = expressionEvaluator; + this.httpContextAccessor = httpContextAccessor; + } + + protected override async Task OnExecuteAsync(HttpResponseAction activity, WorkflowExecutionContext workflowContext, CancellationToken cancellationToken) + { + var response = httpContextAccessor.HttpContext.Response; + + response.StatusCode = (int) activity.StatusCode; + response.ContentType = await expressionEvaluator.EvaluateAsync(activity.ContentType, workflowContext, cancellationToken); + + var headersText = await expressionEvaluator.EvaluateAsync(activity.ResponseHeaders, workflowContext, cancellationToken); + + if (headersText != null) + { + var headersQuery = + from line in Regex.Split(headersText, "\\n", RegexOptions.Multiline) + let pair = line.Split(':', '=') + select new KeyValuePair(pair[0], pair[1]); + + foreach (var header in headersQuery) + { + var headerValueExpression = new WorkflowExpression(activity.ResponseHeaders.Syntax, header.Value); + response.Headers[header.Key] = await expressionEvaluator.EvaluateAsync(headerValueExpression, workflowContext, cancellationToken); + } + } + + var bodyText = await expressionEvaluator.EvaluateAsync(activity.Body, workflowContext, cancellationToken); + + if (!string.IsNullOrWhiteSpace(bodyText)) + { + await response.WriteAsync(bodyText, cancellationToken); + } + + return TriggerEndpoint("Done"); + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs b/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs index 4433f86a0..1980627b1 100644 --- a/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs +++ b/src/activities/Elsa.Activities.Http/Extensions/ServiceCollectionExtensions.cs @@ -20,12 +20,15 @@ namespace Elsa.Activities.Http.Extensions public static IServiceCollection AddHttpWorkflowDrivers(this IServiceCollection services) { services.AddHttpWorkflowDescriptors(); - services.TryAddSingleton(); services.AddAsyncInitialization(); + services.TryAddSingleton(); services.TryAddTransient(); - services.AddActivityDriver(); services.TryAddSingleton(); + services + .AddActivityDriver() + .AddActivityDriver(); + return services; } } diff --git a/src/core/Elsa.Abstractions/Models/ActivityDescriptor.cs b/src/core/Elsa.Abstractions/Models/ActivityDescriptor.cs index 719ed5499..d02c022ed 100644 --- a/src/core/Elsa.Abstractions/Models/ActivityDescriptor.cs +++ b/src/core/Elsa.Abstractions/Models/ActivityDescriptor.cs @@ -34,6 +34,15 @@ namespace Elsa.Models params LocalizedString[] endpoints) => new ActivityDescriptor(typeof(T), category, displayText, description, isTrigger, isBrowsable, endpoints); + public static ActivityDescriptor For( + LocalizedString category, + LocalizedString displayText, + LocalizedString description, + bool isTrigger, + bool isBrowsable, + Func> getEndpoints) where T : IActivity => + new ActivityDescriptor(typeof(T), category, displayText, description, isTrigger, isBrowsable, a => getEndpoints((T) a)); + public ActivityDescriptor() { } @@ -46,15 +55,34 @@ namespace Elsa.Models bool isTrigger, bool isBrowsable, params LocalizedString[] endpoints) + : this( + activityType, + category, + displayText, + description, + isTrigger, + isBrowsable, + a => endpoints) + { + } + + public ActivityDescriptor( + Type activityType, + LocalizedString category, + LocalizedString displayText, + LocalizedString description, + bool isTrigger, + bool isBrowsable, + Func> getEndpoints) { ActivityType = activityType; Name = activityType.Name; Category = category; DisplayText = displayText; Description = description; - GetEndpoints = a => endpoints; IsTrigger = isTrigger; IsBrowsable = isBrowsable; + GetEndpoints = getEndpoints; } public bool IsBrowsable { get; set; } = true; diff --git a/src/core/Elsa.Core/Elsa.Core.csproj b/src/core/Elsa.Core/Elsa.Core.csproj index 129106231..422ec79b9 100644 --- a/src/core/Elsa.Core/Elsa.Core.csproj +++ b/src/core/Elsa.Core/Elsa.Core.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/web/modules/Elsa.Web.Activities.Console/Views/ReadLine.Card.cshtml b/src/web/modules/Elsa.Web.Activities.Console/Views/ReadLine.Card.cshtml index c3d7d87e7..6adb272c7 100644 --- a/src/web/modules/Elsa.Web.Activities.Console/Views/ReadLine.Card.cshtml +++ b/src/web/modules/Elsa.Web.Activities.Console/Views/ReadLine.Card.cshtml @@ -1,4 +1,4 @@ -@model Elsa.Web.ViewModels.ActivityShapeModel +@model ActivityShapeModel @{ var activityDescriptor = Model.Activity.Descriptor; } diff --git a/src/web/modules/Elsa.Web.Activities.Console/Views/ReadLine.Design.cshtml b/src/web/modules/Elsa.Web.Activities.Console/Views/ReadLine.Design.cshtml index 4ba2a2c73..6b6605c3a 100644 --- a/src/web/modules/Elsa.Web.Activities.Console/Views/ReadLine.Design.cshtml +++ b/src/web/modules/Elsa.Web.Activities.Console/Views/ReadLine.Design.cshtml @@ -1,4 +1,4 @@ -@model Elsa.Web.ViewModels.ActivityShapeModel +@model ActivityShapeModel

@Model.DisplayText

diff --git a/src/web/modules/Elsa.Web.Activities.Console/Views/WriteLine.Card.cshtml b/src/web/modules/Elsa.Web.Activities.Console/Views/WriteLine.Card.cshtml index 91ba0ec8d..6faca4b18 100644 --- a/src/web/modules/Elsa.Web.Activities.Console/Views/WriteLine.Card.cshtml +++ b/src/web/modules/Elsa.Web.Activities.Console/Views/WriteLine.Card.cshtml @@ -1,4 +1,4 @@ -@model Elsa.Web.ViewModels.ActivityShapeModel +@model ActivityShapeModel @{ var activityDescriptor = Model.Activity.Descriptor; } diff --git a/src/web/modules/Elsa.Web.Activities.Console/Views/WriteLine.Design.cshtml b/src/web/modules/Elsa.Web.Activities.Console/Views/WriteLine.Design.cshtml index ae80d368e..11b3e3057 100644 --- a/src/web/modules/Elsa.Web.Activities.Console/Views/WriteLine.Design.cshtml +++ b/src/web/modules/Elsa.Web.Activities.Console/Views/WriteLine.Design.cshtml @@ -1,4 +1,4 @@ -@model Elsa.Web.ViewModels.ActivityShapeModel +@model ActivityShapeModel

@Model.DisplayText

diff --git a/src/web/modules/Elsa.Web.Activities.Console/Views/WriteLine.Edit.cshtml b/src/web/modules/Elsa.Web.Activities.Console/Views/WriteLine.Edit.cshtml index faa1fee4a..63bb5dfa0 100644 --- a/src/web/modules/Elsa.Web.Activities.Console/Views/WriteLine.Edit.cshtml +++ b/src/web/modules/Elsa.Web.Activities.Console/Views/WriteLine.Edit.cshtml @@ -1,4 +1,4 @@ -@model Elsa.Web.Activities.Console.ViewModels.WriteLineViewModel +@model WriteLineViewModel
diff --git a/src/web/modules/Elsa.Web.Activities.Console/Views/_ViewImports.cshtml b/src/web/modules/Elsa.Web.Activities.Console/Views/_ViewImports.cshtml index db890e877..0c6e6d44a 100644 --- a/src/web/modules/Elsa.Web.Activities.Console/Views/_ViewImports.cshtml +++ b/src/web/modules/Elsa.Web.Activities.Console/Views/_ViewImports.cshtml @@ -1,2 +1,5 @@ +@using Elsa.Activities.Console.Activities +@using Elsa.Web.Activities.Console.ViewModels +@using Elsa.Web.ViewModels @inherits OrchardCore.DisplayManagement.Razor.RazorPage @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers \ No newline at end of file diff --git a/src/web/modules/Elsa.Web.Activities.Http/Display/HttpResponseActionDisplay.cs b/src/web/modules/Elsa.Web.Activities.Http/Display/HttpResponseActionDisplay.cs new file mode 100644 index 000000000..2fa033d5e --- /dev/null +++ b/src/web/modules/Elsa.Web.Activities.Http/Display/HttpResponseActionDisplay.cs @@ -0,0 +1,27 @@ +using System.Net; +using Elsa.Activities.Http.Activities; +using Elsa.Web.Activities.Http.ViewModels; +using Elsa.Web.Components.ViewModels; +using Elsa.Web.Drivers; + +namespace Elsa.Web.Activities.Http.Display +{ + public class HttpResponseActionDisplay : ActivityDisplayDriver + { + protected override void EditActivity(HttpResponseAction activity, HttpResponseActionViewModel model) + { + model.StatusCode = (int)activity.StatusCode; + model.Body = new ExpressionViewModel(activity.Body); + model.ResponseHeaders = new ExpressionViewModel(activity.ResponseHeaders); + model.ContentType = new ExpressionViewModel(activity.ContentType); + } + + protected override void UpdateActivity(HttpResponseActionViewModel model, HttpResponseAction activity) + { + activity.StatusCode = (HttpStatusCode)model.StatusCode; + activity.Body = model.Body.ToWorkflowExpression(); + activity.ResponseHeaders = model.ResponseHeaders.ToWorkflowExpression(); + activity.ContentType = model.ContentType.ToWorkflowExpression(); + } + } +} \ No newline at end of file diff --git a/src/web/modules/Elsa.Web.Activities.Http/Startup.cs b/src/web/modules/Elsa.Web.Activities.Http/Startup.cs index e7966d38f..d69046956 100644 --- a/src/web/modules/Elsa.Web.Activities.Http/Startup.cs +++ b/src/web/modules/Elsa.Web.Activities.Http/Startup.cs @@ -13,7 +13,8 @@ namespace Elsa.Web.Activities.Http services .AddHttpWorkflowDescriptors() .AddActivityDisplay() - .AddActivityDisplay(); + .AddActivityDisplay() + .AddActivityDisplay(); } } } \ No newline at end of file diff --git a/src/web/modules/Elsa.Web.Activities.Http/ViewModels/HttpResponseActionViewModel.cs b/src/web/modules/Elsa.Web.Activities.Http/ViewModels/HttpResponseActionViewModel.cs new file mode 100644 index 000000000..c30c3aeda --- /dev/null +++ b/src/web/modules/Elsa.Web.Activities.Http/ViewModels/HttpResponseActionViewModel.cs @@ -0,0 +1,19 @@ +using Elsa.Expressions; +using Elsa.Web.Components.ViewModels; + +namespace Elsa.Web.Activities.Http.ViewModels +{ + public class HttpResponseActionViewModel + { + public HttpResponseActionViewModel() + { + StatusCode = 200; + ContentType = new ExpressionViewModel("application/json", PlainTextEvaluator.SyntaxName); + } + + public int StatusCode { get; set; } + public ExpressionViewModel Body { get; set; } + public ExpressionViewModel ContentType { get; set; } + public ExpressionViewModel ResponseHeaders { get; set; } + } +} \ No newline at end of file diff --git a/src/web/modules/Elsa.Web.Activities.Http/Views/HttpResponseAction.Card.cshtml b/src/web/modules/Elsa.Web.Activities.Http/Views/HttpResponseAction.Card.cshtml new file mode 100644 index 000000000..0cf12bd0a --- /dev/null +++ b/src/web/modules/Elsa.Web.Activities.Http/Views/HttpResponseAction.Card.cshtml @@ -0,0 +1,6 @@ +@model ActivityShapeModel +@{ + var activityDescriptor = Model.Activity.Descriptor; +} +

@activityDescriptor.DisplayText

+

@activityDescriptor.Description

\ No newline at end of file diff --git a/src/web/modules/Elsa.Web.Activities.Http/Views/HttpResponseAction.Design.cshtml b/src/web/modules/Elsa.Web.Activities.Http/Views/HttpResponseAction.Design.cshtml new file mode 100644 index 000000000..3b7e026c8 --- /dev/null +++ b/src/web/modules/Elsa.Web.Activities.Http/Views/HttpResponseAction.Design.cshtml @@ -0,0 +1,5 @@ +@model ActivityShapeModel + +
+

@Model.DisplayText

+
\ No newline at end of file diff --git a/src/web/modules/Elsa.Web.Activities.Http/Views/HttpResponseAction.Edit.cshtml b/src/web/modules/Elsa.Web.Activities.Http/Views/HttpResponseAction.Edit.cshtml new file mode 100644 index 000000000..f64a386f7 --- /dev/null +++ b/src/web/modules/Elsa.Web.Activities.Http/Views/HttpResponseAction.Edit.cshtml @@ -0,0 +1,25 @@ +@model HttpResponseActionViewModel +
+ + + + @T["The HTTP status code to return."] +
+
+ + + + @T["The HTTP request body to send along with the response."] +
+
+ + + + @T["The Content-Type header value to return."] +
+
+ + + + @T["Any additional header to return, one per line. For example: my-header-one=foo\r\nmy-header-two=bar"] +
\ No newline at end of file diff --git a/src/web/modules/Elsa.Web.Components/ViewModels/ExpressionViewModel.cs b/src/web/modules/Elsa.Web.Components/ViewModels/ExpressionViewModel.cs index 25cb75366..d02b4b717 100644 --- a/src/web/modules/Elsa.Web.Components/ViewModels/ExpressionViewModel.cs +++ b/src/web/modules/Elsa.Web.Components/ViewModels/ExpressionViewModel.cs @@ -7,11 +7,16 @@ namespace Elsa.Web.Components.ViewModels public ExpressionViewModel() { } - - public ExpressionViewModel(WorkflowExpression workflowExpression) + + public ExpressionViewModel(string expression, string syntax) + { + Expression = expression; + Syntax = syntax; + } + + public ExpressionViewModel(WorkflowExpression workflowExpression) + : this(workflowExpression?.Expression, workflowExpression?.Syntax) { - Expression = workflowExpression?.Expression; - Syntax = workflowExpression?.Syntax; } public string Expression { get; set; }