Feature/send http request (#3355)

* Implemented send http request activity

* Implemented checkbox input and updated send http request activity

* Updated value of mime types constants
This commit is contained in:
cristinamudura 2022-10-25 15:52:55 +02:00 committed by GitHub
parent b9864a25fa
commit 0cd01c687b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 405 additions and 9 deletions

View file

@ -67,6 +67,7 @@ services
.AddActivity<Switch>()
.AddActivity<RunJavaScript>()
.AddActivity<Event>()
.AddActivity<SendHttpRequest>()
)
.Use<IdentityFeature>(identity =>
{

View file

@ -79,6 +79,9 @@ export namespace Components {
interface ElsaCheckListInput {
"inputContext": ActivityInputContext;
}
interface ElsaCheckboxInput {
"inputContext": ActivityInputContext;
}
interface ElsaCodeEditorInput {
"inputContext": ActivityInputContext;
}
@ -358,6 +361,12 @@ declare global {
prototype: HTMLElsaCheckListInputElement;
new (): HTMLElsaCheckListInputElement;
};
interface HTMLElsaCheckboxInputElement extends Components.ElsaCheckboxInput, HTMLStencilElement {
}
var HTMLElsaCheckboxInputElement: {
prototype: HTMLElsaCheckboxInputElement;
new (): HTMLElsaCheckboxInputElement;
};
interface HTMLElsaCodeEditorInputElement extends Components.ElsaCodeEditorInput, HTMLStencilElement {
}
var HTMLElsaCodeEditorInputElement: {
@ -650,6 +659,7 @@ declare global {
"elsa-button-group": HTMLElsaButtonGroupElement;
"elsa-canvas": HTMLElsaCanvasElement;
"elsa-check-list-input": HTMLElsaCheckListInputElement;
"elsa-checkbox-input": HTMLElsaCheckboxInputElement;
"elsa-code-editor-input": HTMLElsaCodeEditorInputElement;
"elsa-context-menu": HTMLElsaContextMenuElement;
"elsa-copy-button": HTMLElsaCopyButtonElement;
@ -739,6 +749,9 @@ declare namespace LocalJSX {
interface ElsaCheckListInput {
"inputContext"?: ActivityInputContext;
}
interface ElsaCheckboxInput {
"inputContext"?: ActivityInputContext;
}
interface ElsaCodeEditorInput {
"inputContext"?: ActivityInputContext;
}
@ -980,6 +993,7 @@ declare namespace LocalJSX {
"elsa-button-group": ElsaButtonGroup;
"elsa-canvas": ElsaCanvas;
"elsa-check-list-input": ElsaCheckListInput;
"elsa-checkbox-input": ElsaCheckboxInput;
"elsa-code-editor-input": ElsaCodeEditorInput;
"elsa-context-menu": ElsaContextMenu;
"elsa-copy-button": ElsaCopyButton;
@ -1042,6 +1056,7 @@ declare module "@stencil/core" {
"elsa-button-group": LocalJSX.ElsaButtonGroup & JSXBase.HTMLAttributes<HTMLElsaButtonGroupElement>;
"elsa-canvas": LocalJSX.ElsaCanvas & JSXBase.HTMLAttributes<HTMLElsaCanvasElement>;
"elsa-check-list-input": LocalJSX.ElsaCheckListInput & JSXBase.HTMLAttributes<HTMLElsaCheckListInputElement>;
"elsa-checkbox-input": LocalJSX.ElsaCheckboxInput & JSXBase.HTMLAttributes<HTMLElsaCheckboxInputElement>;
"elsa-code-editor-input": LocalJSX.ElsaCodeEditorInput & JSXBase.HTMLAttributes<HTMLElsaCodeEditorInputElement>;
"elsa-context-menu": LocalJSX.ElsaContextMenu & JSXBase.HTMLAttributes<HTMLElsaContextMenuElement>;
"elsa-copy-button": LocalJSX.ElsaCopyButton & JSXBase.HTMLAttributes<HTMLElsaCopyButtonElement>;

View file

@ -0,0 +1,14 @@
import {FunctionalComponent, h} from '@stencil/core';
import {ActivityIconSettings, getActivityIconCssClass} from "./models";
export const SendHttpRequestIcon: FunctionalComponent<ActivityIconSettings> = (settings) => (
<svg class={getActivityIconCssClass(settings)} 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"/>
<line x1="3.6" y1="9" x2="20.4" y2="9"/>
<line x1="3.6" y1="15" x2="20.4" y2="15"/>
<path d="M11.5 3a17 17 0 0 0 0 18"/>
<path d="M12.5 3a17 17 0 0 1 0 18"/>
</svg>
);

View file

@ -0,0 +1,54 @@
import {Component, Prop, h, State} from '@stencil/core';
import {LiteralExpression, SyntaxNames} from "../../models";
import {ActivityInputContext} from "../../services/node-input-driver";
import {getInputPropertyValue } from "../../utils";
import {ExpressionChangedArs} from "../designer/input-control-switch/input-control-switch";
@Component({
tag: 'elsa-checkbox-input',
shadow: false
})
export class Checkbox {
@Prop() public inputContext: ActivityInputContext;
@State() private isChecked?: boolean;
public async componentWillLoad() {
this.isChecked = this.getSelectedValue();
}
private getSelectedValue = (): boolean => {
const input = getInputPropertyValue(this.inputContext);
return (input?.expression as LiteralExpression)?.value;
};
render() {
const inputContext = this.inputContext;
const inputDescriptor = inputContext.inputDescriptor;
const fieldName = inputDescriptor.name;
const fieldId = inputDescriptor.name;
const displayName = inputDescriptor.displayName;
const hint = inputDescriptor.description;
const input = getInputPropertyValue(inputContext);
const value = (input?.expression as LiteralExpression)?.value;
const syntax = input?.expression?.type ?? inputDescriptor.defaultSyntax;
const isChecked = this.isChecked;
return (
<elsa-input-control-switch label={displayName} hint={hint} syntax={syntax} expression={value}
onExpressionChanged={this.onExpressionChanged}>
<input type="checkbox" name={fieldName} id={fieldId} value={value} checked={isChecked} onChange={this.onPropertyEditorChanged}/>
</elsa-input-control-switch>
);
}
private onPropertyEditorChanged = (e: Event) => {
const inputElement = e.target as HTMLInputElement;
this.isChecked = inputElement.checked;
this.inputContext.inputChanged(inputElement.checked, SyntaxNames.Literal);
}
private onExpressionChanged = (e: CustomEvent<ExpressionChangedArs>) => {
debugger;
this.inputContext.inputChanged(e.detail.expression, e.detail.syntax);
}
}

View file

@ -16,6 +16,7 @@ import {
} from "../components/icons/activities";
import {WriteHttpResponseIcon} from "../components/icons/activities/write-http-response";
import {FlowJoinIcon} from "../components/icons/activities/flow-join";
import {SendHttpRequestIcon} from "../components/icons/activities/send-http-request"
export type ActivityType = string;
export type ActivityIcon = (ActivityIconSettings?) => any;
@ -39,6 +40,7 @@ export class ActivityIconRegistry {
this.add('Elsa.Event', settings => <EventIcon size={settings?.size}/>);
this.add('Elsa.RunJavaScript', settings => <RunJavaScriptIcon size={settings?.size}/>);
this.add('Elsa.FlowJoin', settings => <FlowJoinIcon size={settings?.size}/>);
this.add('Elsa.SendHttpRequest', settings => <SendHttpRequestIcon size={settings?.size}/>);
}
public add(activityType: ActivityType, icon: ActivityIcon) {

View file

@ -17,6 +17,7 @@ export class InputControlRegistry {
this.add('radio-list', c => <elsa-radio-list-input inputContext={c}/>);
this.add('multi-text', c => <elsa-multi-text-input inputContext={c}/>);
this.add('code-editor', c => <elsa-code-editor-input inputContext={c}/>);
this.add('checkbox', c => <elsa-checkbox-input inputContext={c}/>);
}
public add(uiHint: UIHint, control: RenderActivityPropInputControl) {

View file

@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Elsa.Http.ContentWriters;
using Elsa.Http.Models;
using Elsa.Http.Parsers;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Management.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using HttpRequestHeaders = Elsa.Http.Models.HttpRequestHeaders;
namespace Elsa.Http;
[Activity("Elsa", "HTTP", "Send Http Request.", DisplayName = "Send HTTP Request")]
public class SendHttpRequest : Activity
{
[Input]
public Input<Uri?> Url { get; set; } = default!;
[Input(
Options = new[] {"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"},
UIHint = InputUIHints.Dropdown
)]
public Input<string?> Method { get; set; }
public Input<string?> Content { get; set; } = default!;
[Input(
Options = new[] { "", "text/plain", "text/html", "application/json", "application/xml", "application/x-www-form-urlencoded" },
UIHint = InputUIHints.Dropdown
)]
public Input<string?> ContentType { get; set; } = default!;
[Input(Category = "Security")]
public Input<string?> Authorization { get; set; } = default!;
public Input<bool> ReadContent { get; set; } = new(false);
[Input(
Options = new[] { "", "JsonElement", "Plain Text" },
UIHint = InputUIHints.Dropdown
)]
public Input<string?> ResponseContentParserName { get; set; } = default!;
[Input(Category = "Security")]
public Input<Dictionary<string, string>?> RequestHeaders { get; set; } = new(new HttpRequestHeaders());
[Output]
public Output<object>? ResponseContent { get; set; }
[Output]
public Output<HttpResponseModel> Response { get; set; }
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var request = PrepareRequest(context);
var httpClientFactory = context.GetService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequest));
var cancellationToken = context.CancellationToken;
var response = await httpClient.SendAsync(request, cancellationToken);
var allHeaders =
response.Headers.ToDictionary(x => x.Key, x => x.Value.ToArray())
.Concat(response.Content.Headers.ToDictionary(x => x.Key, x => x.Value.ToArray()));
var responseModel = new HttpResponseModel(response.StatusCode, new Dictionary<string, string[]>())
{
StatusCode = response!.StatusCode,
Headers = new Dictionary<string, string[]>(allHeaders)
};
context.Set(Response, responseModel);
if (HasContent(response) && context.Get(ReadContent))
{
var parsers = context.GetService<IEnumerable<IHttpResponseContentReader>>();
var formatter = SelectContentParser(parsers.ToList(), context.Get(ResponseContentParserName), context.Get(ContentType));
context.Set(ResponseContent, await formatter.ReadAsync(response, context, cancellationToken));
}
}
private IHttpResponseContentReader SelectContentParser(List<IHttpResponseContentReader> parsers, string? parserName, string? contentType)
{
if (string.IsNullOrWhiteSpace(parserName))
{
var simpleContentType = contentType?.Split(';').First() ?? "";
var parser = parsers.OrderByDescending(x => x.Priority).ToList();
return parser.FirstOrDefault(x => x.GetSupportsContentType(simpleContentType)) ?? parser.Last();
}
else
{
var parser = parsers.FirstOrDefault(x => x.Name == parserName);
if (parser == null)
throw new InvalidOperationException("The specified parser does not exist");
return parser;
}
}
private bool HasContent(HttpResponseMessage response)
{
return response?.Content != null &&
response.Content.Headers.ContentLength > 0;
}
private HttpRequestMessage PrepareRequest(ActivityExecutionContext context)
{
var method = context.Get(Method);
var request = new HttpRequestMessage(
new HttpMethod(method),
context.Get(Url));
var headers = context.Get(RequestHeaders);
var requestHeaders = new HeaderDictionary(headers
.ToDictionary(x => x.Key, x => new StringValues(x.Value.Split(','))));
if (!string.IsNullOrWhiteSpace(context.Get(Authorization)))
request.Headers.Authorization = AuthenticationHeaderValue.Parse(context.Get(Authorization));
foreach (var header in requestHeaders)
request.Headers.Add(header.Key, header.Value.AsEnumerable());
var contentType = context.Get(ContentType);
var contentWriters = context.GetRequiredService<IEnumerable<IHttpRequestContentWriter>>();
var contentWriter = SelectContentWriter(contentType, contentWriters);
request.Content = contentWriter.GetContent(contentType, context.Get(Content));
return request;
}
private IHttpRequestContentWriter SelectContentWriter(string contentType,
IEnumerable<IHttpRequestContentWriter> _requestContentWriters)
{
if (string.IsNullOrWhiteSpace(contentType))
{
return new StringHttpRequestContentWriter();
}
return _requestContentWriters.First(w => w.SupportsContentType(contentType));
}
}

View file

@ -0,0 +1,8 @@
namespace Elsa.Http.Constants;
public static class MimeTypes
{
public const string ApplicationXml = "application/xml";
public const string ApplicationJson = "application/json";
public const string ApplicationWwwFormUrlEncoded = "application/x-www-form-urlencoded";
}

View file

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using Elsa.Http.Constants;
namespace Elsa.Http.ContentWriters;
public class FormUrlEncodedHttpRequestContentWriter : IHttpRequestContentWriter
{
private List<string> SupportedContentTypes = new() {MimeTypes.ApplicationWwwFormUrlEncoded};
public bool SupportsContentType(string contentType)
{
return SupportedContentTypes.Contains(contentType);
}
public HttpContent GetContent<T>( T content, string? contentType = null)
{
return new FormUrlEncodedContent(GetContentAsDictionary(content));
}
private Dictionary<string, string> GetContentAsDictionary<TType>(TType body)
{
return body is string || body is JsonObject ?
JsonSerializer.Deserialize<Dictionary<string, string>>(JsonSerializer.Serialize(body)) :
(Dictionary<string, string>)Convert.ChangeType(body, typeof(Dictionary<string, string>));
}
}

View file

@ -0,0 +1,9 @@
using System.Net.Http;
namespace Elsa.Http.ContentWriters;
public interface IHttpRequestContentWriter
{
bool SupportsContentType(string contentType);
HttpContent GetContent<T>(T content, string? contentType = null);
}

View file

@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using Elsa.Http.Constants;
namespace Elsa.Http.ContentWriters;
public class StringHttpRequestContentWriter : IHttpRequestContentWriter
{
private List<string> SupportedContentTypes = new() {MimeTypes.ApplicationJson, MimeTypes.ApplicationXml};
public bool SupportsContentType(string contentType)
{
return SupportedContentTypes.Contains(contentType);
}
public HttpContent GetContent<T>(T content, string? contentType = null)
{
var serializedContent = JsonSerializer.Serialize(content);
return new StringContent(serializedContent, Encoding. UTF8, contentType);
}
}

View file

@ -12,6 +12,7 @@
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Routing" Version="2.2.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
</ItemGroup>
<ItemGroup>

View file

@ -1,9 +1,11 @@
using System;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.Http.ContentWriters;
using Elsa.Http.Handlers;
using Elsa.Http.Implementations;
using Elsa.Http.Options;
using Elsa.Http.Parsers;
using Elsa.Http.Services;
using Elsa.Mediator.Extensions;
using Microsoft.Extensions.DependencyInjection;
@ -43,9 +45,18 @@ public class HttpFeature : FeatureBase
Services.Configure<HttpActivityOptions>(options => options.BasePath = BasePath);
Services
.AddHttpClient()
.AddSingleton<IRouteMatcher, RouteMatcher>()
.AddSingleton<IRouteTable, RouteTable>()
.AddNotificationHandlersFrom<UpdateRouteTable>()
.AddHttpContextAccessor();
.AddHttpContextAccessor()
// Add Content Parsers
.AddSingleton<IHttpResponseContentReader, JsonElementHttpResponseContentReader>()
//Add Request Content Writers
.AddSingleton<IHttpRequestContentWriter, StringHttpRequestContentWriter>()
.AddSingleton<IHttpRequestContentWriter, FormUrlEncodedHttpRequestContentWriter>()
;
}
}

View file

@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace Elsa.Http.Models;
public class HttpRequestHeaders : Dictionary<string, string>
{
public string ContentType => this["content-type"];
}

View file

@ -0,0 +1,6 @@
using System.Collections.Generic;
using System.Net;
namespace Elsa.Http.Models;
public record HttpResponseModel(HttpStatusCode StatusCode, IDictionary<string, string[]> Headers);

View file

@ -0,0 +1,13 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Elsa.Http.Parsers;
public interface IHttpResponseContentReader
{
string Name { get; }
int Priority { get; }
bool GetSupportsContentType(string contentType);
Task<object> ReadAsync(HttpResponseMessage response, object context, CancellationToken cancellationToken);
}

View file

@ -0,0 +1,19 @@
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Elsa.Http.Parsers;
public class JsonElementHttpResponseContentReader : IHttpResponseContentReader
{
public string Name => "JsonElement";
public int Priority => 0;
public bool GetSupportsContentType(string contentType) => contentType.Contains("/json", StringComparison.OrdinalIgnoreCase);
public async Task<object> ReadAsync(HttpResponseMessage response, object context, CancellationToken cancellationToken)
{
var json = (await response.Content.ReadAsStringAsync()).Trim();
return JsonDocument.Parse(json).RootElement;
}
}

View file

@ -0,0 +1,13 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Elsa.Http.Parsers;
public class PlainTextHttpResponseContentReader : IHttpResponseContentReader
{
public string Name => "Plain Text";
public int Priority => -1;
public bool GetSupportsContentType(string contentType) => true;
public async Task<object> ReadAsync(HttpResponseMessage response, object context, CancellationToken cancellationToken) => await response.Content.ReadAsStringAsync();
}

View file

@ -30,7 +30,7 @@ services
.UseRuntime(runtime =>
{
runtime
.UseEntityFrameworkCore(ef => ef.UseSqlite())
.UseEntityFrameworkCore(f =>f.UseSqlite())
.AddWorkflow<HelloWorldWorkflow>()
.AddWorkflow<HttpWorkflow>()
.AddWorkflow<ForkedHttpWorkflow>()
@ -61,13 +61,13 @@ var app = builder.Build();
var serviceProvider = app.Services;
// Configure workflow engine execution pipeline.
serviceProvider.ConfigureDefaultWorkflowExecutionPipeline(pipeline =>
pipeline
.UseWorkflowExecutionLogPersistence()
.UsePersistentVariables()
.UseWorkflowContexts()
.UseStackBasedActivityScheduler()
);
// serviceProvider.ConfigureDefaultWorkflowExecutionPipeline(pipeline =>
// pipeline
// .UseWorkflowExecutionLogPersistence()
// .UsePersistentVariables()
// .UseWorkflowContexts()
// .UseStackBasedActivityScheduler()
// );
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();

View file

@ -36,6 +36,16 @@ services
// Razor Pages.
services.AddRazorPages();
services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
);
});
// Configure middleware pipeline.
var app = builder.Build();
@ -54,4 +64,10 @@ app.UseAuthorization();
app.UseHttpActivities();
app.MapRazorPages();
app.UseCors(x => x
.AllowAnyMethod()
.AllowAnyHeader()
.SetIsOriginAllowed(origin => true) // allow any origin
.AllowCredentials());
app.Run();