From 9a01aefe170f4605deef7dd1731865fbdb81416b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 9 Jun 2021 18:08:22 +0200 Subject: [PATCH] Add support for webhook triggers --- Elsa.sln | 9 +--- .../Bookmarks/HttpEndpointBookmark.cs | 3 +- .../Middleware/HttpEndpointMiddleware.cs | 2 +- .../WebhookActivityTypeProvider.cs | 25 ++++++---- .../Bookmarks/WebhookBookmark.cs | 35 ++++++++++++++ .../WebhookOptionsBuilderExtensions.cs | 5 +- .../Bookmarks/BookmarkProviderContext.cs | 22 +++++---- .../WorkflowBlueprintWrapperExtensions.cs | 6 ++- ...ForActivityBlueprintAndWorkflowProvider.cs | 2 +- .../src/assets/styles/tailwind.css | 46 +++++++++++++++++++ .../src/globals/tailwind.css | 46 +++++++++++++++++++ 11 files changed, 166 insertions(+), 35 deletions(-) create mode 100644 src/activities/webhooks/Elsa.Activities.Webhooks/Bookmarks/WebhookBookmark.cs diff --git a/Elsa.sln b/Elsa.sln index 4a62862e1..121101abe 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -233,13 +233,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "blazor", "blazor", "{D86B94 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "aspnetcore", "aspnetcore", "{C869CC72-9A98-4246-9A76-6A50F48AFD85}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElsaDashboard.Samples.Monolith", "src\samples\dashboard\blazor\ElsaDashboard.Samples.Monolith\ElsaDashboard.Samples.Monolith.csproj", "{C0ECCB6E-B6FA-413E-B717-648E9CB71A97}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElsaDashboard.Samples.Server", "src\samples\dashboard\blazor\ElsaDashboard.Samples.Server\ElsaDashboard.Samples.Server.csproj", "{9B56C09A-E4D5-4FDD-AD41-EDD1F52B7B9B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElsaDashboard.Samples.WebAssembly", "src\samples\dashboard\blazor\ElsaDashboard.Samples.WebAssembly\ElsaDashboard.Samples.WebAssembly.csproj", "{84876EBC-8109-475E-8C4E-A5CB714DB429}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElsaDashboard.Samples.AspNetCore.Monolith", "src\samples\dashboard\aspnetcore\ElsaDashboard.Samples.AspNetCore.Monolith\ElsaDashboard.Samples.AspNetCore.Monolith.csproj", "{F1EF5639-021A-4D22-9D4A-73D57C88EBFA}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElsaDashboard.Samples.AspNetCore.Monolith", "src\samples\dashboard\aspnetcore\ElsaDashboard.Samples.AspNetCore.Monolith\ElsaDashboard.Samples.AspNetCore.Monolith.csproj", "{F1EF5639-021A-4D22-9D4A-73D57C88EBFA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.Persistence.YesSql", "src\samples\persistence\Elsa.Samples.Persistence.YesSql\Elsa.Samples.Persistence.YesSql.csproj", "{3A30EFFE-32D8-40B8-9C62-427037BB5424}" @@ -263,8 +258,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.CustomActivity EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "aspnetcore", "aspnetcore", "{28C5C63F-2DCF-46BE-9576-1F50A7CFE8C4}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElsaDashboard.Web", "src\dashboards\aspnetcore\ElsaDashboard\ElsaDashboard.Web.csproj", "{5F81C84A-4C04-48FF-981C-23E48A7ACA6C}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{3BF2C5A9-AC69-4587-833F-C16CFC97E100}" ProjectSection(SolutionItems) = preProject .github\workflows\publish-latest-dashboard-and-server-docker.yml = .github\workflows\publish-latest-dashboard-and-server-docker.yml diff --git a/src/activities/Elsa.Activities.Http/Bookmarks/HttpEndpointBookmark.cs b/src/activities/Elsa.Activities.Http/Bookmarks/HttpEndpointBookmark.cs index 17c579a4b..2c64540dc 100644 --- a/src/activities/Elsa.Activities.Http/Bookmarks/HttpEndpointBookmark.cs +++ b/src/activities/Elsa.Activities.Http/Bookmarks/HttpEndpointBookmark.cs @@ -16,8 +16,7 @@ namespace Elsa.Activities.Http.Bookmarks public override async ValueTask> GetBookmarksAsync(BookmarkProviderContext context, CancellationToken cancellationToken) { var path = ToLower(await context.ReadActivityPropertyAsync(x => x.Path, cancellationToken))!; - var correlationId = ToLower(context.ActivityExecutionContext.WorkflowExecutionContext.CorrelationId); - var methods = (await context.ReadActivityPropertyAsync(x => x.Methods, cancellationToken))?.Select(x => x.ToLowerInvariant()) ?? Enumerable.Empty(); + var methods = (await context.ReadActivityPropertyAsync(x => x.Methods, cancellationToken))?.Select(ToLower) ?? Enumerable.Empty(); BookmarkResult CreateBookmark(string method) => Result(new(path, method)); return methods.Select(CreateBookmark); diff --git a/src/activities/Elsa.Activities.Http/Middleware/HttpEndpointMiddleware.cs b/src/activities/Elsa.Activities.Http/Middleware/HttpEndpointMiddleware.cs index 20fd38f72..56e7d7ae6 100644 --- a/src/activities/Elsa.Activities.Http/Middleware/HttpEndpointMiddleware.cs +++ b/src/activities/Elsa.Activities.Http/Middleware/HttpEndpointMiddleware.cs @@ -67,7 +67,7 @@ namespace Elsa.Activities.Http.Middleware { var pendingWorkflowInstance = pendingWorkflowInstances[pendingWorkflow.WorkflowInstanceId]; var workflowBlueprintWrapper = workflowBlueprintWrappers[pendingWorkflowInstance.DefinitionId]; - var activityWrapper = workflowBlueprintWrapper.GetActivity(pendingWorkflow.ActivityId!); + var activityWrapper = workflowBlueprintWrapper.GetUnfilteredActivity(pendingWorkflow.ActivityId!); var readContent = await activityWrapper!.EvaluatePropertyValueAsync(x => x.ReadContent, cancellationToken); var inputModel = commonInputModel; diff --git a/src/activities/webhooks/Elsa.Activities.Webhooks/ActivityTypes/WebhookActivityTypeProvider.cs b/src/activities/webhooks/Elsa.Activities.Webhooks/ActivityTypes/WebhookActivityTypeProvider.cs index 50d86a507..e86520011 100644 --- a/src/activities/webhooks/Elsa.Activities.Webhooks/ActivityTypes/WebhookActivityTypeProvider.cs +++ b/src/activities/webhooks/Elsa.Activities.Webhooks/ActivityTypes/WebhookActivityTypeProvider.cs @@ -16,6 +16,7 @@ namespace Elsa.Activities.Webhooks.ActivityTypes { public class WebhookActivityTypeProvider : IActivityTypeProvider { + public const string WebhookMarkerAttribute = "WebhookMarker"; private const string WebhookActivityCategory = "Webhooks"; private readonly IWebhookDefinitionStore _webhookDefinitionStore; @@ -59,17 +60,17 @@ namespace Elsa.Activities.Webhooks.ActivityTypes InputProperties = new[] { new ActivityInputDescriptor( - nameof(HttpEndpoint.Methods), + nameof(HttpEndpoint.Methods), typeof(HashSet), - ActivityInputUIHints.Dropdown, + ActivityInputUIHints.CheckList, "Request Method", "Specify what request method this webhook should handle. Leave empty to handle both GET and POST requests", - new HashSet { "", "GET", "POST" }, - "Webhooks", + new[] { "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD" }, + default, 0, "POST", - SyntaxNames.Literal, - new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid }) + SyntaxNames.Json, + new[] { SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid }) } }; @@ -78,18 +79,22 @@ namespace Elsa.Activities.Webhooks.ActivityTypes var activity = await _activityActivator.ActivateActivityAsync(context); activity.Path = webhook.Path; - activity.ReadContent = true; - activity.TargetType = webhook.PayloadTypeName is not null and not "" ? Type.GetType(webhook.PayloadTypeName) : throw new Exception($"Type {webhook.PayloadTypeName} not found"); - return activity; + activity.TargetType = !string.IsNullOrWhiteSpace(webhook.PayloadTypeName) ? Type.GetType(webhook.PayloadTypeName) ?? throw new Exception($"Type {webhook.PayloadTypeName} not found") : null; + return activity; } return new ActivityType { TypeName = webhook.Name, Type = typeof(HttpEndpoint), - Description = webhook.Description is not null and not "" ? webhook.Description : $"A webhook at {webhook.Path}", + Description = !string.IsNullOrWhiteSpace(webhook.Description) ? webhook.Description : $"A webhook at {webhook.Path}", DisplayName = webhook.Name, ActivateAsync = ActivateActivityAsync, + Attributes = new Dictionary + { + [WebhookMarkerAttribute] = true, + ["Path"] = webhook.Path, + }, Describe = () => descriptor }; } diff --git a/src/activities/webhooks/Elsa.Activities.Webhooks/Bookmarks/WebhookBookmark.cs b/src/activities/webhooks/Elsa.Activities.Webhooks/Bookmarks/WebhookBookmark.cs new file mode 100644 index 000000000..647707d94 --- /dev/null +++ b/src/activities/webhooks/Elsa.Activities.Webhooks/Bookmarks/WebhookBookmark.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Activities.Http; +using Elsa.Activities.Http.Bookmarks; +using Elsa.Activities.Webhooks.ActivityTypes; +using Elsa.Bookmarks; +using Microsoft.AspNetCore.Http; + +namespace Elsa.Activities.Webhooks.Bookmarks +{ + public record WebhookBookmark(string WebhookActivityTypeName) : IBookmark; + + public class WebhookBookmarkProvider : BookmarkProvider + { + public override bool SupportsActivity(BookmarkProviderContext context) + { + var activityType = context.ActivityType; + return activityType.Attributes.ContainsKey(WebhookActivityTypeProvider.WebhookMarkerAttribute); + } + + public override async ValueTask> GetBookmarksAsync(BookmarkProviderContext context, CancellationToken cancellationToken) + { + var path = ToLower((string)context.ActivityType.Attributes["Path"])!; + var methods = (await context.ReadActivityPropertyAsync>(x => x.Methods, cancellationToken))?.Select(ToLower) ?? Enumerable.Empty(); + + BookmarkResult CreateBookmark(string method) => Result(new(path, method), nameof(HttpEndpoint)); + return methods.Select(CreateBookmark); + } + + private static string? ToLower(string? s) => s?.ToLowerInvariant(); + + } +} \ No newline at end of file diff --git a/src/activities/webhooks/Elsa.Activities.Webhooks/Extensions/WebhookOptionsBuilderExtensions.cs b/src/activities/webhooks/Elsa.Activities.Webhooks/Extensions/WebhookOptionsBuilderExtensions.cs index fecd8d9ad..79a76f44a 100644 --- a/src/activities/webhooks/Elsa.Activities.Webhooks/Extensions/WebhookOptionsBuilderExtensions.cs +++ b/src/activities/webhooks/Elsa.Activities.Webhooks/Extensions/WebhookOptionsBuilderExtensions.cs @@ -1,5 +1,6 @@ using System; using Elsa.Activities.Webhooks.ActivityTypes; +using Elsa.Activities.Webhooks.Bookmarks; using Elsa.Activities.Webhooks.Persistence.Decorators; using Elsa.Activities.Webhooks.Services; using Elsa.Services; @@ -22,11 +23,11 @@ namespace Elsa.Activities.Webhooks.Extensions configure?.Invoke(webhookOptionsBuilder); // Services. - services .AddScoped() .AddScoped(sp => webhookOptionsBuilder.WebhookOptions.WebhookDefinitionStoreFactory(sp)) - .AddScoped(); + .AddScoped() + .AddBookmarkProvider(); services.Decorate(); services.Decorate(); diff --git a/src/core/Elsa.Abstractions/Bookmarks/BookmarkProviderContext.cs b/src/core/Elsa.Abstractions/Bookmarks/BookmarkProviderContext.cs index e8e2e827c..25d34888f 100644 --- a/src/core/Elsa.Abstractions/Bookmarks/BookmarkProviderContext.cs +++ b/src/core/Elsa.Abstractions/Bookmarks/BookmarkProviderContext.cs @@ -20,17 +20,8 @@ namespace Elsa.Bookmarks public ActivityType ActivityType { get; } public BookmarkIndexingMode Mode { get; } public IActivityBlueprintWrapper GetActivity() where TActivity : IActivity => new ActivityBlueprintWrapper(ActivityExecutionContext); - } - - public class BookmarkProviderContext : BookmarkProviderContext where TActivity: IActivity - { - public BookmarkProviderContext(ActivityExecutionContext activityExecutionContext, ActivityType activityType, BookmarkIndexingMode mode) : base(activityExecutionContext, activityType, mode) - { - } - - public IActivityBlueprintWrapper Activity => GetActivity(); - public async ValueTask ReadActivityPropertyAsync(Expression> propertyExpression, CancellationToken cancellationToken = default) + public async ValueTask ReadActivityPropertyAsync(Expression> propertyExpression, CancellationToken cancellationToken = default) where TActivity : IActivity { var activityBlueprint = GetActivity(); @@ -40,4 +31,15 @@ namespace Elsa.Bookmarks return activityBlueprint.GetPropertyValue(propertyExpression); } } + + public class BookmarkProviderContext : BookmarkProviderContext where TActivity: IActivity + { + public BookmarkProviderContext(ActivityExecutionContext activityExecutionContext, ActivityType activityType, BookmarkIndexingMode mode) : base(activityExecutionContext, activityType, mode) + { + } + + public IActivityBlueprintWrapper Activity => GetActivity(); + + public ValueTask ReadActivityPropertyAsync(Expression> propertyExpression, CancellationToken cancellationToken = default) => base.ReadActivityPropertyAsync(propertyExpression, cancellationToken); + } } \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Extensions/WorkflowBlueprintWrapperExtensions.cs b/src/core/Elsa.Abstractions/Extensions/WorkflowBlueprintWrapperExtensions.cs index cb7a7266d..c5bd116c6 100644 --- a/src/core/Elsa.Abstractions/Extensions/WorkflowBlueprintWrapperExtensions.cs +++ b/src/core/Elsa.Abstractions/Extensions/WorkflowBlueprintWrapperExtensions.cs @@ -20,7 +20,11 @@ namespace Elsa } public static IActivityBlueprintWrapper? FindActivity(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, Func, bool>? predicate = default) where TActivity : IActivity => workflowBlueprintWrapper.Filter(predicate).FirstOrDefault(); - public static IActivityBlueprintWrapper? GetActivity(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, string id) where TActivity : IActivity => workflowBlueprintWrapper.FindActivity(x => x.ActivityBlueprint.Id == id)!; + public static IActivityBlueprintWrapper GetActivity(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, string id) where TActivity : IActivity => workflowBlueprintWrapper.FindActivity(x => x.ActivityBlueprint.Id == id)!; public static IActivityBlueprintWrapper? GetActivity(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, string id) => workflowBlueprintWrapper.Activities.FirstOrDefault(x => x.ActivityBlueprint.Id == id); + + public static IActivityBlueprintWrapper? FindUnfilteredActivity(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, Func, bool> predicate) where TActivity : IActivity => workflowBlueprintWrapper.Activities.Select(x => x.As()).Where(predicate).FirstOrDefault(); + + public static IActivityBlueprintWrapper GetUnfilteredActivity(this IWorkflowBlueprintWrapper workflowBlueprintWrapper, string id) where TActivity : IActivity => workflowBlueprintWrapper.FindUnfilteredActivity(x => x.ActivityBlueprint.Id == id)!; } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs b/src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs index 951656844..4a034f1e4 100644 --- a/src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs +++ b/src/core/Elsa.Core/Triggers/TriggersForActivityBlueprintAndWorkflowProvider.cs @@ -87,7 +87,7 @@ namespace Elsa.Triggers { var bookmarkResults = (await provider.GetBookmarksAsync(context, cancellationToken)).ToList(); return bookmarkResults - .Select(x => new WorkflowTrigger(workflowBlueprint, activityBlueprint.Id, activityBlueprint.Type, _bookmarkHasher.Hash(x.Bookmark), x.Bookmark)) + .Select(x => new WorkflowTrigger(workflowBlueprint, activityBlueprint.Id, x.ActivityTypeName ?? activityBlueprint.Type, _bookmarkHasher.Hash(x.Bookmark), x.Bookmark)) .ToList(); } } diff --git a/src/designer/elsa-workflows-studio/src/assets/styles/tailwind.css b/src/designer/elsa-workflows-studio/src/assets/styles/tailwind.css index dc2d431ea..b9b77d107 100644 --- a/src/designer/elsa-workflows-studio/src/assets/styles/tailwind.css +++ b/src/designer/elsa-workflows-studio/src/assets/styles/tailwind.css @@ -1118,6 +1118,12 @@ select { margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); } +.elsa-space-y-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); +} + .elsa-space-y-8 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse))); @@ -2544,6 +2550,14 @@ select { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.elsa-grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.elsa-col-span-4 { + grid-column: span 4 / span 4; +} + .elsa-transform { --tw-translate-x: 0; --tw-translate-y: 0; @@ -3212,6 +3226,10 @@ svg.jtk-connector.jtk-hover path { border-color: rgba(229, 231, 235, var(--tw-divide-opacity)); } + .sm\:elsa-rounded-md { + border-radius: 0.375rem; + } + .sm\:elsa-rounded-lg { border-radius: 0.5rem; } @@ -4357,11 +4375,26 @@ svg.jtk-connector.jtk-hover path { max-width: 65ch; } + .lg\:elsa-py-0 { + padding-top: 0px; + padding-bottom: 0px; + } + + .lg\:elsa-px-0 { + padding-left: 0px; + padding-right: 0px; + } + .lg\:elsa-px-8 { padding-left: 2rem; padding-right: 2rem; } + .lg\:elsa-py-12 { + padding-top: 3rem; + padding-bottom: 3rem; + } + .lg\:elsa-pl-2 { padding-left: 0.5rem; } @@ -4374,13 +4407,26 @@ svg.jtk-connector.jtk-hover path { gap: 2rem; } + .lg\:elsa-gap-x-5 { + -moz-column-gap: 1.25rem; + column-gap: 1.25rem; + } + .lg\:elsa-grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); } + .lg\:elsa-col-span-2 { + grid-column: span 2 / span 2; + } + .lg\:elsa-col-span-6 { grid-column: span 6 / span 6; } + + .lg\:elsa-col-span-9 { + grid-column: span 9 / span 9; + } } @media (min-width: 1280px) { diff --git a/src/designer/elsa-workflows-studio/src/globals/tailwind.css b/src/designer/elsa-workflows-studio/src/globals/tailwind.css index dc2d431ea..b9b77d107 100644 --- a/src/designer/elsa-workflows-studio/src/globals/tailwind.css +++ b/src/designer/elsa-workflows-studio/src/globals/tailwind.css @@ -1118,6 +1118,12 @@ select { margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); } +.elsa-space-y-6 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); +} + .elsa-space-y-8 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse))); @@ -2544,6 +2550,14 @@ select { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.elsa-grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.elsa-col-span-4 { + grid-column: span 4 / span 4; +} + .elsa-transform { --tw-translate-x: 0; --tw-translate-y: 0; @@ -3212,6 +3226,10 @@ svg.jtk-connector.jtk-hover path { border-color: rgba(229, 231, 235, var(--tw-divide-opacity)); } + .sm\:elsa-rounded-md { + border-radius: 0.375rem; + } + .sm\:elsa-rounded-lg { border-radius: 0.5rem; } @@ -4357,11 +4375,26 @@ svg.jtk-connector.jtk-hover path { max-width: 65ch; } + .lg\:elsa-py-0 { + padding-top: 0px; + padding-bottom: 0px; + } + + .lg\:elsa-px-0 { + padding-left: 0px; + padding-right: 0px; + } + .lg\:elsa-px-8 { padding-left: 2rem; padding-right: 2rem; } + .lg\:elsa-py-12 { + padding-top: 3rem; + padding-bottom: 3rem; + } + .lg\:elsa-pl-2 { padding-left: 0.5rem; } @@ -4374,13 +4407,26 @@ svg.jtk-connector.jtk-hover path { gap: 2rem; } + .lg\:elsa-gap-x-5 { + -moz-column-gap: 1.25rem; + column-gap: 1.25rem; + } + .lg\:elsa-grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); } + .lg\:elsa-col-span-2 { + grid-column: span 2 / span 2; + } + .lg\:elsa-col-span-6 { grid-column: span 6 / span 6; } + + .lg\:elsa-col-span-9 { + grid-column: span 9 / span 9; + } } @media (min-width: 1280px) {