HTTP Request with status codes as outcomes (#3768)
* Implement configurable expected status outcomes on HTTP Request activity * Add Catch all outcome * Implement dynamic port update
This commit is contained in:
parent
ac9ecbb2d4
commit
b1564bf646
|
|
@ -7,7 +7,6 @@ export * from "./flow-node";
|
|||
export * from "./flowchart";
|
||||
export * from "./foreach";
|
||||
export * from "./http-endpoint";
|
||||
export * from "./http-request";
|
||||
export * from "./http-response";
|
||||
export * from "./if";
|
||||
export * from "./models";
|
||||
|
|
|
|||
|
|
@ -2,12 +2,16 @@ import {Node} from "@antv/x6";
|
|||
import {Activity, ActivityDescriptor} from "../../models";
|
||||
|
||||
export interface ActivityNodeHandler {
|
||||
createDesignerNode: (context: CreateUINodeContext) => Node.Metadata;
|
||||
createDesignerNode: (context: UINodeContext) => Node.Metadata;
|
||||
createPorts: (context: UIPortContext) => Array<any>;
|
||||
}
|
||||
|
||||
export interface CreateUINodeContext {
|
||||
activityDescriptor: ActivityDescriptor;
|
||||
activity: Activity;
|
||||
export interface UINodeContext extends UIPortContext{
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface UIPortContext {
|
||||
activityDescriptor: ActivityDescriptor;
|
||||
activity: Activity;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'reflect-metadata';
|
||||
import {Node} from "@antv/x6";
|
||||
import {Container, Service} from "typedi"
|
||||
import {ActivityNodeHandler, CreateUINodeContext} from "./activity-node-handler";
|
||||
import {ActivityNodeHandler, UINodeContext, UIPortContext} from "./activity-node-handler";
|
||||
import {PortProviderContext, PortProviderRegistry} from "../../services";
|
||||
import {PortMode} from "../../models";
|
||||
import {v4 as uuid} from 'uuid';
|
||||
|
|
@ -14,8 +14,24 @@ export class DefaultNodeHandler implements ActivityNodeHandler {
|
|||
this.portProviderRegistry = Container.get(PortProviderRegistry);
|
||||
}
|
||||
|
||||
createDesignerNode(context: CreateUINodeContext): Node.Metadata {
|
||||
createDesignerNode(context: UINodeContext): Node.Metadata {
|
||||
const {activityDescriptor, activity, x, y} = context;
|
||||
const portModels = this.createPorts(context);
|
||||
|
||||
return {
|
||||
id: activity.id,
|
||||
shape: 'activity',
|
||||
activity: activity,
|
||||
activityDescriptor: activityDescriptor,
|
||||
x: x,
|
||||
y: y,
|
||||
data: activity,
|
||||
ports: portModels
|
||||
} as Node.Metadata;
|
||||
}
|
||||
|
||||
createPorts(context: UIPortContext): Array<any> {
|
||||
const {activityDescriptor, activity} = context;
|
||||
const provider = this.portProviderRegistry.get(activityDescriptor.typeName);
|
||||
const providerContext: PortProviderContext = {activityDescriptor, activity};
|
||||
const inPorts = [{name: 'In', displayName: null, mode: PortMode.Port}];
|
||||
|
|
@ -54,15 +70,6 @@ export class DefaultNodeHandler implements ActivityNodeHandler {
|
|||
|
||||
const portModels = [...leftPortModels, ...rightPortModels];
|
||||
|
||||
return {
|
||||
id: activity.id,
|
||||
shape: 'activity',
|
||||
activity: activity,
|
||||
activityDescriptor: activityDescriptor,
|
||||
x: x,
|
||||
y: y,
|
||||
data: activity,
|
||||
ports: portModels
|
||||
} as Node.Metadata;
|
||||
return portModels;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import FromJSONData = Model.FromJSONData;
|
|||
import PointLike = Point.PointLike;
|
||||
import {generateUniqueActivityName} from "../../utils/generate-activity-name";
|
||||
import {DagreLayout, OutNode} from '@antv/layout';
|
||||
import {adjustPortMarkupByNode, rebuildGraph} from '../../utils/graph';
|
||||
import {adjustPortMarkupByNode, getPortNameByPortId, rebuildGraph} from '../../utils/graph';
|
||||
import FlowchartTunnel, {FlowchartState} from "./state";
|
||||
|
||||
const FlowchartTypeName = 'Elsa.Flowchart';
|
||||
|
|
@ -196,19 +196,25 @@ export class FlowchartComponent {
|
|||
const originalId = args.originalId;
|
||||
const nodeId = originalId;
|
||||
const activity = args.activity;
|
||||
const node = this.graph.getNodes().find(x => x.id == nodeId) as ActivityNodeShape;
|
||||
const node = this.graph.getNodes().find(x => x.id == nodeId);
|
||||
const nodeShape = node as ActivityNodeShape;
|
||||
|
||||
if (!!node) {
|
||||
|
||||
// Update the node's data with the activity.
|
||||
node.setData(activity, {overwrite: true});
|
||||
nodeShape.setData(activity, {overwrite: true});
|
||||
|
||||
// Updating the node's activity property to trigger a rerender.
|
||||
node.activity = activity;
|
||||
nodeShape.activity = activity;
|
||||
|
||||
// If the ID of the activity changed, we need to update connection references (X6 stores deep copies of data).
|
||||
if (activityId !== originalId)
|
||||
this.syncEdgeData(nodeId, activity);
|
||||
|
||||
// Update ports.
|
||||
if (args.updatePorts) {
|
||||
this.updatePorts(node, activity);
|
||||
}
|
||||
}
|
||||
|
||||
this.updateLookups();
|
||||
|
|
@ -671,6 +677,22 @@ export class FlowchartComponent {
|
|||
await this.setupGraph(childFlowchart);
|
||||
}
|
||||
|
||||
private updatePorts = (node: any, activity: Activity) => {
|
||||
const descriptor = this.getActivityDescriptor(activity.type);
|
||||
const desiredPorts = this.nodeFactory.createPorts(descriptor, activity);
|
||||
const actualPorts = node.ports.items;
|
||||
|
||||
const addedPorts = desiredPorts.filter(x => !actualPorts.some(y => getPortNameByPortId(y.id) == getPortNameByPortId(x.id)));
|
||||
const removedPorts = actualPorts.filter(x => !desiredPorts.some(y => getPortNameByPortId(y.id) == getPortNameByPortId(x.id)));
|
||||
|
||||
if(addedPorts.length > 0 )
|
||||
node.addPorts(addedPorts);
|
||||
|
||||
if(removedPorts.length > 0)
|
||||
node.removePorts(removedPorts);
|
||||
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
const path = this.path;
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export interface UpdateActivityArgs {
|
|||
id: string;
|
||||
originalId: string;
|
||||
activity: Activity;
|
||||
updatePorts?: boolean;
|
||||
}
|
||||
|
||||
export interface RenameActivityArgs {
|
||||
|
|
|
|||
|
|
@ -16,4 +16,9 @@ export class NodeFactory {
|
|||
const handler = this.handlerRegistry.createHandler(activityDescriptor.typeName);
|
||||
return handler.createDesignerNode({activityDescriptor, activity, x, y});
|
||||
}
|
||||
|
||||
public createPorts(activityDescriptor: ActivityDescriptor, activity: Activity): Array<any> {
|
||||
const handler = this.handlerRegistry.createHandler(activityDescriptor.typeName);
|
||||
return handler.createPorts({activityDescriptor, activity});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import 'reflect-metadata';
|
||||
import {h} from '@stencil/core';
|
||||
import {Container, Service} from "typedi";
|
||||
import {ActivityIconRegistry, PortProviderRegistry} from "../../../services";
|
||||
import {Plugin} from "../../../models";
|
||||
import {FlowHttpRequestPortProvider} from "./flow-http-request-port-provider";
|
||||
import {HttpRequestIcon} from "../icons";
|
||||
|
||||
@Service()
|
||||
export class FlowHttpRequestPlugin implements Plugin {
|
||||
static readonly ActivityTypeName: string = 'Elsa.FlowSendHttpRequest';
|
||||
|
||||
constructor() {
|
||||
const activityTypeName = FlowHttpRequestPlugin.ActivityTypeName;
|
||||
const portProviderRegistry = Container.get(PortProviderRegistry);
|
||||
const iconRegistry = Container.get(ActivityIconRegistry);
|
||||
|
||||
portProviderRegistry.add(activityTypeName, () => Container.get(FlowHttpRequestPortProvider));
|
||||
iconRegistry.add(FlowHttpRequestPlugin.ActivityTypeName, settings => <HttpRequestIcon size={settings?.size}/>);
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import 'reflect-metadata';
|
||||
import {Service} from "typedi";
|
||||
import {Activity, ActivityInput, InputDescriptor, JsonExpression, Port, PortMode} from "../../../models";
|
||||
import {PortProvider, PortProviderContext} from "../../../services";
|
||||
import {FlowSendHttpRequest} from "./models";
|
||||
|
||||
@Service()
|
||||
export class FlowHttpRequestPortProvider implements PortProvider {
|
||||
|
||||
getOutboundPorts(context: PortProviderContext): Array<Port> {
|
||||
const activity = context.activity as FlowSendHttpRequest;
|
||||
|
||||
if (activity == null)
|
||||
return [];
|
||||
|
||||
const expectedStatusCodes = activity.expectedStatusCodes as ActivityInput;
|
||||
|
||||
if(!expectedStatusCodes)
|
||||
return [];
|
||||
|
||||
const statusCodesJson = (expectedStatusCodes.expression as JsonExpression).value;
|
||||
const statusCodes = JSON.parse(statusCodesJson) as Array<string>;
|
||||
const catchAllPort = {name: 'Catch all', displayName: 'Catch all', mode: PortMode.Port};
|
||||
const outcomes = [...statusCodes.map(x => ({name: x.toString(), displayName: x.toString(), mode: PortMode.Port})), catchAllPort];
|
||||
|
||||
return outcomes;
|
||||
}
|
||||
|
||||
resolvePort(portName: string, context: PortProviderContext): Activity | Array<Activity> {
|
||||
return null;
|
||||
}
|
||||
|
||||
assignPort(portName: string, activity: Activity, context: PortProviderContext) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import {Activity, ActivityInput} from "../../../models";
|
||||
|
||||
export interface FlowSendHttpRequest extends Activity {
|
||||
expectedStatusCodes: ActivityInput;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import {FunctionalComponent, h} from '@stencil/core';
|
||||
import {ActivityIconSettings, getActivityIconCssClass} from "./models";
|
||||
import {ActivityIconSettings, getActivityIconCssClass} from "../../components/icons/activities";
|
||||
|
||||
export const HttpRequestIcon: FunctionalComponent<ActivityIconSettings> = (settings) => (
|
||||
<svg class={getActivityIconCssClass(settings)} width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import 'reflect-metadata';
|
||||
import {h} from '@stencil/core';
|
||||
import {Container, Service} from "typedi";
|
||||
import {ActivityIconRegistry, PortProviderRegistry} from "../../../services";
|
||||
import {Plugin} from "../../../models";
|
||||
import {HttpRequestPortProvider} from "./http-request-port-provider";
|
||||
import {HttpRequestIcon} from "../icons";
|
||||
|
||||
@Service()
|
||||
export class HttpRequestPlugin implements Plugin {
|
||||
static readonly ActivityTypeName: string = 'Elsa.SendHttpRequest';
|
||||
|
||||
constructor() {
|
||||
const activityTypeName = HttpRequestPlugin.ActivityTypeName;
|
||||
const portProviderRegistry = Container.get(PortProviderRegistry);
|
||||
const iconRegistry = Container.get(ActivityIconRegistry);
|
||||
|
||||
portProviderRegistry.add(activityTypeName, () => Container.get(HttpRequestPortProvider));
|
||||
iconRegistry.add(HttpRequestPlugin.ActivityTypeName, settings => <HttpRequestIcon size={settings?.size}/>);
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import 'reflect-metadata';
|
||||
import {Service} from "typedi";
|
||||
import {Activity, Port, PortMode} from "../../../models";
|
||||
import {PortProvider, PortProviderContext} from "../../../services";
|
||||
import {HttpStatusCodeCase, SendHttpRequest} from "./models";
|
||||
|
||||
@Service()
|
||||
export class HttpRequestPortProvider implements PortProvider {
|
||||
|
||||
getOutboundPorts(context: PortProviderContext): Array<Port> {
|
||||
const activity = context.activity as SendHttpRequest;
|
||||
|
||||
if(activity == null)
|
||||
return [];
|
||||
|
||||
const defaultPort = {name: 'unmatchedStatusCode', displayName: 'Unmatched status code', mode: PortMode.Embedded, isBrowsable: false}; // Hide the port from the designer until the editor uI is finished.
|
||||
const casesArray = this.getCases(activity);
|
||||
const ports = casesArray.map(x => ({name: x.statusCode.toString(), displayName: x.statusCode.toString(), mode: PortMode.Embedded}));
|
||||
|
||||
return [...ports, defaultPort];
|
||||
}
|
||||
|
||||
resolvePort(portName: string, context: PortProviderContext): Activity | Array<Activity> {
|
||||
const activity = context.activity as SendHttpRequest;
|
||||
const cases = this.getCases(activity);
|
||||
const matchingStatusCode = cases.find(x => x.statusCode.toString() == portName);
|
||||
return !matchingStatusCode ? activity.unmatchedStatusCode : matchingStatusCode.activity;
|
||||
}
|
||||
|
||||
assignPort(portName: string, activity: Activity, context: PortProviderContext) {
|
||||
const sendHttpRequestActivity = context.activity as SendHttpRequest;
|
||||
const cases = this.getCases(sendHttpRequestActivity);
|
||||
const matchingCase = cases.find(x => x.statusCode.toString() === portName);
|
||||
|
||||
if(!matchingCase)
|
||||
return;
|
||||
|
||||
matchingCase.activity = activity;
|
||||
}
|
||||
|
||||
private getCases(activity: SendHttpRequest): Array<HttpStatusCodeCase> {
|
||||
const cases = activity.expectedStatusCodes;
|
||||
return !cases ? [] : cases;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import {Activity} from "../../../models";
|
||||
|
||||
export interface SendHttpRequest extends Activity {
|
||||
expectedStatusCodes: Array<HttpStatusCodeCase>;
|
||||
unmatchedStatusCode?: Activity;
|
||||
}
|
||||
|
||||
export interface HttpStatusCodeCase {
|
||||
statusCode: number;
|
||||
activity?: Activity;
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import {Component, h, Prop, State, Watch} from "@stencil/core";
|
||||
import {camelCase} from 'lodash';
|
||||
import {ActivityInputContext} from "../../services/activity-input-driver";
|
||||
import {mapSyntaxToLanguage} from "../../utils";
|
||||
import {SyntaxNames} from "../../models";
|
||||
import {MonacoValueChangedArgs} from "../../components/shared/monaco-editor/monaco-editor";
|
||||
import {TrashBinButtonIcon} from "../../components/icons/buttons/trash-bin";
|
||||
import {PlusButtonIcon} from "../../components/icons/buttons/plus";
|
||||
import {ActivityInputContext} from "../../../services/activity-input-driver";
|
||||
import {mapSyntaxToLanguage} from "../../../utils";
|
||||
import {SyntaxNames} from "../../../models";
|
||||
import {MonacoValueChangedArgs} from "../../../components/shared/monaco-editor/monaco-editor";
|
||||
import {TrashBinButtonIcon} from "../../../components/icons/buttons/trash-bin";
|
||||
import {PlusButtonIcon} from "../../../components/icons/buttons/plus";
|
||||
import {FlowSwitchCase} from "./models";
|
||||
|
||||
@Component({
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import 'reflect-metadata';
|
||||
import {FunctionalComponent, h} from '@stencil/core';
|
||||
import {Container, Service} from "typedi";
|
||||
import {ActivityIconRegistry, InputControlRegistry, PortProviderRegistry} from "../../services";
|
||||
import {Plugin} from "../../models";
|
||||
import {ActivityIconRegistry, InputControlRegistry, PortProviderRegistry} from "../../../services";
|
||||
import {Plugin} from "../../../models";
|
||||
import {FlowSwitchPortProvider} from "./flow-switch-port-provider";
|
||||
import {ActivityIconSettings, getActivityIconCssClass} from "../../components/icons/activities";
|
||||
import {ActivityIconSettings, getActivityIconCssClass} from "../../../components/icons/activities";
|
||||
|
||||
@Service()
|
||||
export class FlowSwitchPlugin implements Plugin {
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import 'reflect-metadata';
|
||||
import {Service} from "typedi";
|
||||
import {Activity, Port, PortMode} from "../../models";
|
||||
import {Activity, Port, PortMode} from "../../../models";
|
||||
import {FlowSwitchActivity} from "./models";
|
||||
import {PortProvider, PortProviderContext} from "../../services";
|
||||
import {PortProvider, PortProviderContext} from "../../../services";
|
||||
|
||||
@Service()
|
||||
export class FlowSwitchPortProvider implements PortProvider {
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import {Activity, Expression} from "../../models";
|
||||
import {Activity, Expression} from "../../../models";
|
||||
|
||||
export interface FlowSwitchCase {
|
||||
label: string;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import {Activity, ActivityInput, Expression} from "../../models";
|
||||
import {Activity, ActivityInput, Expression} from "../../../models";
|
||||
|
||||
export interface SwitchCase {
|
||||
label: string;
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
import {Component, h, Prop, State, Watch} from "@stencil/core";
|
||||
import {camelCase} from 'lodash';
|
||||
import {ActivityInputContext} from "../../services/activity-input-driver";
|
||||
import {mapSyntaxToLanguage} from "../../utils";
|
||||
import {SyntaxNames} from "../../models";
|
||||
import {ActivityInputContext} from "../../../services/activity-input-driver";
|
||||
import {mapSyntaxToLanguage} from "../../../utils";
|
||||
import {SyntaxNames} from "../../../models";
|
||||
import {SwitchCase} from "./models";
|
||||
import {MonacoValueChangedArgs} from "../../components/shared/monaco-editor/monaco-editor";
|
||||
import {TrashBinButtonIcon} from "../../components/icons/buttons/trash-bin";
|
||||
import {PlusButtonIcon} from "../../components/icons/buttons/plus";
|
||||
import {FormEntry} from "../../components/shared/forms/form-entry";
|
||||
import {MonacoValueChangedArgs} from "../../../components/shared/monaco-editor/monaco-editor";
|
||||
import {TrashBinButtonIcon} from "../../../components/icons/buttons/trash-bin";
|
||||
import {PlusButtonIcon} from "../../../components/icons/buttons/plus";
|
||||
|
||||
@Component({
|
||||
tag: 'elsa-switch-editor',
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import 'reflect-metadata';
|
||||
import {FunctionalComponent, h} from '@stencil/core';
|
||||
import {Container, Service} from "typedi";
|
||||
import {ActivityIconRegistry, InputControlRegistry, PortProviderRegistry} from "../../services";
|
||||
import {Plugin} from "../../models";
|
||||
import {ActivityIconRegistry, InputControlRegistry, PortProviderRegistry} from "../../../services";
|
||||
import {Plugin} from "../../../models";
|
||||
import {SwitchPortProvider} from "./switch-port-provider";
|
||||
import {ActivityIconSettings, getActivityIconCssClass} from "../../components/icons/activities";
|
||||
import {ActivityIconSettings, getActivityIconCssClass} from "../../../components/icons/activities";
|
||||
|
||||
@Service()
|
||||
export class SwitchPlugin implements Plugin {
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import 'reflect-metadata';
|
||||
import {Service} from "typedi";
|
||||
import {camelCase} from 'lodash';
|
||||
import {Activity, Port, PortMode} from "../../models";
|
||||
import {Activity, Port, PortMode} from "../../../models";
|
||||
import {SwitchActivity, SwitchCase} from "./models";
|
||||
import {PortProvider, PortProviderContext} from "../../services";
|
||||
import {PortProvider, PortProviderContext} from "../../../services";
|
||||
|
||||
@Service()
|
||||
export class SwitchPortProvider implements PortProvider {
|
||||
|
|
@ -227,6 +227,7 @@ export class WorkflowDefinitionEditor {
|
|||
}
|
||||
|
||||
private updateActivityInternal = async (args: UpdateActivityArgs) => {
|
||||
args.updatePorts = true; // TODO: Make this configurable from a activity plugin.
|
||||
await this.flowchart.updateActivity(args);
|
||||
this.saveChangesDebounced();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ import {
|
|||
IfIcon,
|
||||
ReadLineIcon, WriteLineIcon,
|
||||
RunJavaScriptIcon,
|
||||
HttpEndpointIcon, HttpResponseIcon, HttpRequestIcon,
|
||||
CorrelateIcon
|
||||
HttpEndpointIcon, HttpResponseIcon, CorrelateIcon
|
||||
} from "../components/icons/activities";
|
||||
import {ForIcon} from "../components/icons/activities/for";
|
||||
import {FinishIcon} from "../components/icons/activities/finish";
|
||||
|
|
@ -56,7 +55,6 @@ export class ActivityIconRegistry {
|
|||
this.add('Elsa.RunJavaScript', settings => <RunJavaScriptIcon size={settings?.size}/>);
|
||||
this.add('Elsa.FlowJoin', settings => <FlowJoinIcon size={settings?.size}/>);
|
||||
this.add('Elsa.FlowNode', settings => <FlowNodeIcon size={settings?.size}/>);
|
||||
this.add('Elsa.SendHttpRequest', settings => <HttpRequestIcon size={settings?.size}/>);
|
||||
this.add("Elsa.Correlate", settings => <CorrelateIcon size={settings?.size}/>)
|
||||
this.add("Elsa.Start", settings => <StartIcon size={settings?.size}/>)
|
||||
this.add("Elsa.Finish", settings => <FinishIcon size={settings?.size}/>)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import 'reflect-metadata';
|
||||
import {Container, Service} from "typedi";
|
||||
import {SwitchPlugin} from "../modules/switch/switch-plugin";
|
||||
import {Plugin} from "../models";
|
||||
import {SequencePlugin} from "../modules/sequence/sequence-plugin";
|
||||
import {FlowSwitchPlugin} from "../modules/flow-switch/flow-switch-plugin";
|
||||
import {WorkflowDefinitionsPlugin} from "../modules/workflow-definitions/plugins/workflow-definitions-plugin";
|
||||
import {CompositeActivityVersionPlugin} from "../modules/workflow-definitions/plugins/composite-version-plugin";
|
||||
import {WorkflowInstancesPlugin} from "../modules/workflow-instances/plugin";
|
||||
import {LoginPlugin} from "../modules/login/plugin";
|
||||
import {HomePagePlugin} from "../modules/home/plugin";
|
||||
import {FlowchartPlugin} from "../modules/flowchart/plugin";
|
||||
import { SwitchPlugin } from '../modules/switch/sequence/switch-plugin';
|
||||
import {FlowSwitchPlugin} from "../modules/switch/flow/flow-switch-plugin";
|
||||
import {FlowHttpRequestPlugin} from "../modules/http-request/flow/flow-http-request-plugin";
|
||||
import {HttpRequestPlugin} from "../modules/http-request/sequence/http-request-plugin";
|
||||
|
||||
// A registry of plugins.
|
||||
@Service()
|
||||
|
|
@ -26,6 +28,8 @@ export class PluginRegistry {
|
|||
this.add(Container.get(SequencePlugin));
|
||||
this.add(Container.get(SwitchPlugin));
|
||||
this.add(Container.get(FlowSwitchPlugin));
|
||||
this.add(Container.get(FlowHttpRequestPlugin));
|
||||
this.add(Container.get(HttpRequestPlugin));
|
||||
}
|
||||
|
||||
add(plugin: Plugin) {
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ function findMatchingPortForEdge(node: Node<Node.Properties>, position: string,
|
|||
return node.getPorts().find(p => p.position == position && p.type == portType && getPortNameByPortId(p.id) == portName);
|
||||
}
|
||||
|
||||
function getPortNameByPortId(portId: string) {
|
||||
export function getPortNameByPortId(portId: string) {
|
||||
return portId.includes('_') ? (portId.split('_')[1] == 'null' ? null : portId.split('_')[1]) : portId;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
using System.ComponentModel;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json.Serialization;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Http.ContentWriters;
|
||||
using Elsa.Workflows.Core;
|
||||
using Elsa.Workflows.Core.Attributes;
|
||||
using Elsa.Workflows.Core.Contracts;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Management.Models;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using HttpRequestHeaders = Elsa.Http.Models.HttpRequestHeaders;
|
||||
|
||||
|
|
@ -13,9 +16,112 @@ namespace Elsa.Http;
|
|||
/// <summary>
|
||||
/// Send an HTTP request.
|
||||
/// </summary>
|
||||
[Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "HTTP Request", Kind = ActivityKind.Task)]
|
||||
public class SendHttpRequest : CodeActivity<HttpResponse>
|
||||
[Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "Flow HTTP Request", Kind = ActivityKind.Task)]
|
||||
[PublicAPI]
|
||||
public class FlowSendHttpRequest : SendHttpRequestBase
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of expected status codes to handle.
|
||||
/// </summary>
|
||||
[Input(Description = "A list of expected status codes to handle.", UIHint = InputUIHints.MultiText)]
|
||||
public Input<ICollection<int>> ExpectedStatusCodes { get; set; } = default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response)
|
||||
{
|
||||
var expectedStatusCodes = ExpectedStatusCodes.TryGet(context) ?? new List<int>(0);
|
||||
var statusCode = (int)response.StatusCode;
|
||||
var hasMatchingStatusCode = expectedStatusCodes.Contains(statusCode);
|
||||
var outcome = hasMatchingStatusCode ? statusCode.ToString() : "Catch all";
|
||||
|
||||
await context.CompleteActivityWithOutcomesAsync(outcome);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send an HTTP request.
|
||||
/// </summary>
|
||||
[Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "HTTP Request", Kind = ActivityKind.Task)]
|
||||
[PublicAPI]
|
||||
public class SendHttpRequest : SendHttpRequestBase
|
||||
{
|
||||
/// <summary>
|
||||
/// A list of expected status codes to handle and the corresponding activity to execute when the status code matches.
|
||||
/// </summary>
|
||||
[Input(
|
||||
Description = "A list of expected status codes to handle and the corresponding activity to execute when the status code matches.",
|
||||
UIHint = InputUIHints.MultiText,
|
||||
|
||||
// TODO: Need to implement a custom UI hint for this.
|
||||
IsBrowsable = false
|
||||
)]
|
||||
public ICollection<HttpStatusCodeCase> ExpectedStatusCodes { get; set; } = new List<HttpStatusCodeCase>();
|
||||
|
||||
/// <summary>
|
||||
/// The activity to execute when the HTTP status code does not match any of the expected status codes.
|
||||
/// </summary>
|
||||
[Port]
|
||||
[Browsable(false)] // TODO: Need to implement a custom UI hint for this.
|
||||
public IActivity? CatchAll { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response)
|
||||
{
|
||||
var expectedStatusCodes = ExpectedStatusCodes;
|
||||
var statusCode = (int)response.StatusCode;
|
||||
var matchingCase = expectedStatusCodes.FirstOrDefault(x => x.StatusCode == statusCode);
|
||||
var activity = matchingCase?.Activity ?? CatchAll;
|
||||
|
||||
await context.ScheduleActivityAsync(activity, OnChildActivityCompletedAsync);
|
||||
}
|
||||
|
||||
private async ValueTask OnChildActivityCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext)
|
||||
{
|
||||
await context.CompleteActivityAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A binding between an HTTP status code and an activity.
|
||||
/// </summary>
|
||||
public class HttpStatusCodeCase
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="HttpStatusCodeCase"/> class.
|
||||
/// </summary>
|
||||
[JsonConstructor]
|
||||
public HttpStatusCodeCase()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="HttpStatusCodeCase"/> class.
|
||||
/// </summary>
|
||||
public HttpStatusCodeCase(int statusCode, IActivity activity)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
Activity = activity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP status code to match.
|
||||
/// </summary>
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The activity to execute when the HTTP status code matches.
|
||||
/// </summary>
|
||||
public IActivity? Activity { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for activities that send HTTP requests.
|
||||
/// </summary>
|
||||
public abstract class SendHttpRequestBase : Activity<HttpResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// The URL to send the request to.
|
||||
/// </summary>
|
||||
[Input] public Input<Uri?> Url { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -72,15 +178,22 @@ public class SendHttpRequest : CodeActivity<HttpResponse>
|
|||
{
|
||||
var request = PrepareRequest(context);
|
||||
var httpClientFactory = context.GetRequiredService<IHttpClientFactory>();
|
||||
var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequest));
|
||||
var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequestBase));
|
||||
var cancellationToken = context.CancellationToken;
|
||||
var response = await httpClient.SendAsync(request, cancellationToken);
|
||||
var parsedContent = await ParseContentAsync(context, response.Content);
|
||||
|
||||
context.Set(Result, response);
|
||||
context.Set(ParsedContent, parsedContent);
|
||||
|
||||
await HandleResponseAsync(context, response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the response.
|
||||
/// </summary>
|
||||
protected abstract ValueTask HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response);
|
||||
|
||||
private async Task<object?> ParseContentAsync(ActivityExecutionContext context, HttpContent httpContent)
|
||||
{
|
||||
if (!HasContent(httpContent))
|
||||
|
|
@ -90,10 +203,10 @@ public class SendHttpRequest : CodeActivity<HttpResponse>
|
|||
var targetType = ParsedContent.GetTargetType(context);
|
||||
var contentStream = await httpContent.ReadAsStreamAsync(cancellationToken);
|
||||
var contentType = httpContent.Headers.ContentType?.MediaType!;
|
||||
|
||||
|
||||
return await context.ParseContentAsync(contentStream, contentType, targetType, cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
private static bool HasContent(HttpContent httpContent) => httpContent.Headers.ContentLength > 0;
|
||||
|
||||
private HttpRequestMessage PrepareRequest(ActivityExecutionContext context)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public class HttpFeature : FeatureBase
|
|||
public Func<IServiceProvider, IHttpEndpointWorkflowFaultHandler> HttpEndpointWorkflowFaultHandler { get; set; } = ActivatorUtilities.GetServiceOrCreateInstance<DefaultHttpEndpointWorkflowFaultHandler>;
|
||||
|
||||
/// <summary>
|
||||
/// A delegate to configure the <see cref="HttpClient"/> used when by the <see cref="SendHttpRequest"/> activity.
|
||||
/// A delegate to configure the <see cref="HttpClient"/> used when by the <see cref="FlowSendHttpRequest"/> activity.
|
||||
/// </summary>
|
||||
public Action<IServiceProvider, HttpClient> HttpClient { get; set; } = (_, _) => { };
|
||||
|
||||
|
|
@ -84,7 +84,7 @@ public class HttpFeature : FeatureBase
|
|||
|
||||
Services.Configure(configureOptions);
|
||||
|
||||
var httpClientBuilder = Services.AddHttpClient<SendHttpRequest>(HttpClient);
|
||||
var httpClientBuilder = Services.AddHttpClient<SendHttpRequestBase>(HttpClient);
|
||||
HttpClientBuilder(httpClientBuilder);
|
||||
|
||||
Services
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ public class DefaultHttpEndpointWorkflowFaultHandler : IHttpEndpointWorkflowFaul
|
|||
{
|
||||
private readonly SerializerOptionsProvider _serializerOptionsProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultHttpEndpointWorkflowFaultHandler"/> class.
|
||||
/// </summary>
|
||||
public DefaultHttpEndpointWorkflowFaultHandler(SerializerOptionsProvider serializerOptionsProvider)
|
||||
{
|
||||
_serializerOptionsProvider = serializerOptionsProvider;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Text.Json;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Models;
|
||||
using Elsa.Workflows.Core.Serialization.Converters;
|
||||
|
||||
namespace Elsa.Workflows.Core.Expressions;
|
||||
|
||||
|
|
@ -19,6 +20,7 @@ public class JsonExpression<T> : JsonExpression
|
|||
|
||||
public class JsonExpressionHandler : IExpressionHandler
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ValueTask<object?> EvaluateAsync(IExpression expression, Type returnType, ExpressionExecutionContext context)
|
||||
{
|
||||
var jsonExpression = (JsonExpression)expression;
|
||||
|
|
@ -27,7 +29,10 @@ public class JsonExpressionHandler : IExpressionHandler
|
|||
if (string.IsNullOrWhiteSpace(value))
|
||||
return ValueTask.FromResult(default(object?));
|
||||
|
||||
var model = JsonSerializer.Deserialize(value, returnType);
|
||||
var serializerOptions = new JsonSerializerOptions();
|
||||
serializerOptions.Converters.Add(new IntegerConverter());
|
||||
|
||||
var model = JsonSerializer.Deserialize(value, returnType, serializerOptions);
|
||||
return ValueTask.FromResult(model);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Elsa.Workflows.Core.Serialization.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Converts integers to and from JSON strings.
|
||||
/// </summary>
|
||||
public class IntegerConverter : JsonConverter<int>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
// Read the JSON string value and parse it as an integer
|
||||
var value = reader.GetString()!;
|
||||
var integer = int.Parse(value);
|
||||
|
||||
// Return the parsed integer
|
||||
return integer;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
|
||||
{
|
||||
// Write the integer as a JSON number
|
||||
writer.WriteNumberValue(value);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ using Elsa.Http;
|
|||
using Elsa.Workflows.Core.Abstractions;
|
||||
using Elsa.Workflows.Core.Activities;
|
||||
using Elsa.Workflows.Core.Contracts;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Elsa.Samples.OutboundHttpRequests.Workflows;
|
||||
|
|
|
|||
Loading…
Reference in a new issue