Restore workflow journal (#3582)

* Refactor identity options

* Delete templates

Will be created as part of a separate repo.

* Little cleanup

* Increase token lifetime for reference server app

* Cleanup workflow definition editor plugin

* Fix workflow instance viewer component

* Update identity generator to use hyphen format

* Improve workflow and activity execution pipeline registration API

* Fix workflow journal

* Update backend

* Prevent unnecessary re-render

* Restore autolayout function
This commit is contained in:
Sipke Schoorstra 2023-01-02 22:24:05 +01:00 committed by GitHub
parent 0cadc9a712
commit 06e62b3bcc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
81 changed files with 530 additions and 449 deletions

View file

@ -138,10 +138,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{986E54
docker\Dockerfile = docker\Dockerfile
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "templates", "templates", "{26A89A29-FE67-4174-9809-7C63AAD6BD5D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElsaWorkflowsApp", "templates\elsa-workflow-app\ElsaWorkflowsApp\ElsaWorkflowsApp.csproj", "{15B2ABCB-7460-4E59-A991-10AD9190B1C9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.WorkflowSinks", "src\samples\aspnet\Elsa.Samples.WorkflowSinks\Elsa.Samples.WorkflowSinks.csproj", "{536EFB55-EEA1-4761-9A72-D4EA84A6DB70}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.EntityFrameworkCore.PostgreSql", "src\modules\Elsa.EntityFrameworkCore.PostgreSql\Elsa.EntityFrameworkCore.PostgreSql.csproj", "{AA84DBF7-F70F-4673-8DC2-6EFBE3E9BF83}"
@ -352,10 +348,6 @@ Global
{34FBB2D3-4E2B-4411-95F2-C6B56899826A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{34FBB2D3-4E2B-4411-95F2-C6B56899826A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{34FBB2D3-4E2B-4411-95F2-C6B56899826A}.Release|Any CPU.Build.0 = Release|Any CPU
{15B2ABCB-7460-4E59-A991-10AD9190B1C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{15B2ABCB-7460-4E59-A991-10AD9190B1C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{15B2ABCB-7460-4E59-A991-10AD9190B1C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{15B2ABCB-7460-4E59-A991-10AD9190B1C9}.Release|Any CPU.Build.0 = Release|Any CPU
{536EFB55-EEA1-4761-9A72-D4EA84A6DB70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{536EFB55-EEA1-4761-9A72-D4EA84A6DB70}.Debug|Any CPU.Build.0 = Debug|Any CPU
{536EFB55-EEA1-4761-9A72-D4EA84A6DB70}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -423,7 +415,6 @@ Global
{89157FB8-A25B-42EC-A91F-37DFEE274C8C} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{812B3B34-D2DD-4A3B-BA2D-3D3C52E0C062} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{34FBB2D3-4E2B-4411-95F2-C6B56899826A} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{15B2ABCB-7460-4E59-A991-10AD9190B1C9} = {26A89A29-FE67-4174-9809-7C63AAD6BD5D}
{536EFB55-EEA1-4761-9A72-D4EA84A6DB70} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{AA84DBF7-F70F-4673-8DC2-6EFBE3E9BF83} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
EndGlobalSection

View file

@ -13,8 +13,11 @@ var services = builder.Services;
var configuration = builder.Configuration;
var sqliteConnectionString = configuration.GetConnectionString("Sqlite")!;
var identityOptions = new IdentityOptions();
var identityTokenOptions = new IdentityTokenOptions();
var identitySection = configuration.GetSection("Identity");
var identityTokenSection = identitySection.GetSection("Tokens");
identitySection.Bind(identityOptions);
identityTokenSection.Bind(identityTokenOptions);
// Add Elsa services.
services
@ -23,8 +26,8 @@ services
.UseWorkflowsApi()
.UseIdentity(identity =>
{
identity.CreateDefaultUser = true;
identity.IdentityOptions = identityOptions;
identity.TokenOptions = identityTokenOptions;
})
.UseDefaultAuthentication()
.UseWorkflowRuntime(runtime => { runtime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)); })

View file

@ -14,6 +14,9 @@
"BasePath": ""
},
"Identity": {
"SigningKey": "secret-signing-key"
"CreateDefaultAdmin": true,
"Tokens": {
"SigningKey": "secret-signing-key"
}
}
}

View file

@ -1,11 +1,7 @@
using Elsa.EntityFrameworkCore.Extensions;
using Elsa.Extensions;
using Elsa.Http;
using Elsa.Identity;
using Elsa.Identity.Options;
using Elsa.JavaScript.Activities;
using Elsa.Jobs.Activities.Implementations;
using Elsa.Jobs.Activities.Middleware.Activities;
using Elsa.Jobs.Activities.Services;
using Elsa.EntityFrameworkCore.Modules.ActivityDefinitions;
using Elsa.EntityFrameworkCore.Modules.Labels;
@ -13,11 +9,6 @@ using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.MassTransit.Options;
using Elsa.Requirements;
using Elsa.Scheduling.Activities;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Middleware.Activities;
using Elsa.Workflows.Core.Middleware.Workflows;
using Elsa.Workflows.Management.Services;
using Elsa.WorkflowServer.Web.Jobs;
using Microsoft.AspNetCore.Authorization;
@ -26,32 +17,30 @@ var services = builder.Services;
var configuration = builder.Configuration;
var sqliteConnectionString = configuration.GetConnectionString("Sqlite")!;
var identityOptions = new IdentityOptions();
var identityTokenOptions = new IdentityTokenOptions();
var identitySection = configuration.GetSection("Identity");
var identityTokenSection = identitySection.GetSection("Tokens");
identitySection.Bind(identityOptions);
identityTokenSection.Bind(identityTokenOptions);
var rabbitMqOptions = new RabbitMqOptions();
configuration.GetSection(RabbitMqOptions.RabbitMq).Bind(rabbitMqOptions);
// Add Elsa services.
services
.AddElsa(elsa => elsa
.UseWorkflowManagement(management => management
.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString))
.AddActivitiesFrom<WriteLine>()
.AddActivitiesFrom<HttpEndpoint>()
.AddActivitiesFrom<Delay>()
.AddActivitiesFrom<RunJavaScript>()
.AddActivitiesFrom<Program>()
)
.AddActivitiesFrom<Program>()
.UseIdentity(identity =>
{
identity.CreateDefaultUser = true;
identity.IdentityOptions = identityOptions;
identity.TokenOptions = identityTokenOptions;
})
.UseDefaultAuthentication()
.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
.UseWorkflowRuntime(runtime =>
{
//runtime.UseProtoActor(proto => proto.PersistenceProvider = _ => new SqliteProvider(new SqliteConnectionStringBuilder(sqliteConnectionString)));
runtime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString));
runtime.UseAsyncWorkflowStateExporter();
})
.UseLabels(labels => labels.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
.UseActivityDefinitions(feature => feature.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
@ -78,24 +67,6 @@ var serviceProvider = app.Services;
var jobRegistry = serviceProvider.GetRequiredService<IJobRegistry>();
jobRegistry.Add(typeof(IndexBlockchainJob));
// Update activity providers.
var activityRegistryPopulator = serviceProvider.GetRequiredService<IActivityRegistryPopulator>();
activityRegistryPopulator.PopulateRegistryAsync(typeof(JobActivityProvider));
// Configure workflow engine execution pipeline.
serviceProvider.ConfigureDefaultWorkflowExecutionPipeline(pipeline =>
pipeline
.UsePersistentVariables()
.UseBookmarkPersistence()
.UseWorkflowContexts()
.UseDefaultActivityScheduler()
);
// Configure activity execution pipeline to use the job-based activity invoker.
serviceProvider.ConfigureDefaultActivityExecutionPipeline(pipeline => pipeline
.UseExceptionHandling()
.UseJobBasedActivityInvoker());
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();

View file

@ -12,7 +12,11 @@
"RabbitMq": "localhost"
},
"Identity": {
"SigningKey": "secret-signing-key"
"CreateDefaultAdmin": true,
"Tokens": {
"SigningKey": "secret-signing-key",
"Lifetime": "8:00:00"
}
},
"RabbitMq": {
"Username": "guest",

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props"/>
<Import Project="..\..\..\configureawait.props"/>
<Import Project="..\..\..\common.props" />
<Import Project="..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
@ -13,17 +13,18 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0"/>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0"/>
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Api.Common\Elsa.Api.Common.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Mediator\Elsa.Mediator.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj"/>
<ProjectReference Include="..\..\modules\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj"/>
<ProjectReference Include="..\..\common\Elsa.Api.Common\Elsa.Api.Common.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Jobs.Activities\Elsa.Jobs.Activities.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Mediator\Elsa.Mediator.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
</ItemGroup>
</Project>

View file

@ -11,20 +11,11 @@ public static class ServiceProviderExtensions
/// <summary>
/// Configure the default activity execution pipeline.
/// </summary>
public static IServiceProvider ConfigureDefaultActivityExecutionPipeline(this IServiceProvider services, Action<IActivityExecutionBuilder> setup)
public static IServiceProvider ConfigureDefaultActivityExecutionPipeline(this IServiceProvider services, Action<IActivityExecutionPipelineBuilder> setup)
{
var pipeline = services.GetRequiredService<IActivityExecutionPipeline>();
pipeline.Setup(setup);
return services;
}
/// <summary>
/// Configure the default workflow execution pipeline.
/// </summary>
public static IServiceProvider ConfigureDefaultWorkflowExecutionPipeline(this IServiceProvider services, Action<IWorkflowExecutionBuilder> setup)
{
var pipeline = services.GetRequiredService<IWorkflowExecutionPipeline>();
pipeline.Setup(setup);
return services;
}
}

View file

@ -24,7 +24,7 @@ public class ElsaFeature : FeatureBase
/// Set this to true to opt out of automatically registering activities from Elsa.Workflows.Core.
/// </summary>
public bool DisableAutomaticActivityRegistration { get; set; }
/// <inheritdoc />
public ElsaFeature(IModule module) : base(module)
{
@ -33,6 +33,14 @@ public class ElsaFeature : FeatureBase
/// <inheritdoc />
public override void Configure()
{
Module.UseWorkflowManagement(management => management.AddActivitiesFrom<WriteLine>());
Module
.UseWorkflows(workflows => workflows
.WithDefaultRuntimeWorkflowExecutionPipeline()
.WithJobBasedActivityExecutionPipeline())
.UseWorkflowManagement(management =>
{
if (!DisableAutomaticActivityRegistration)
management.AddActivitiesFrom<WriteLine>();
});
}
}

View file

@ -17,8 +17,8 @@ import { ActivityInputContext } from "./services/node-input-driver";
import { ContextMenuAnchorPoint, MenuItem, MenuItemGroup } from "./components/shared/context-menu/models";
import { DropdownButtonItem, DropdownButtonOrigin } from "./components/shared/dropdown-button/models";
import { Graph } from "@antv/x6";
import { AddActivityArgs, FlowchartPathItem, LayoutDirection, RenameActivityArgs, UpdateActivityArgs } from "./modules/flowchart/models";
import { OutNode } from "@antv/layout";
import { AddActivityArgs, FlowchartPathItem, RenameActivityArgs, UpdateActivityArgs } from "./modules/flowchart/models";
import { ActivityNodeShape } from "./modules/flowchart/shapes";
import { PanelActionClickArgs, PanelActionDefinition } from "./components/shared/form-panel/models";
import { ExpressionChangedArs } from "./components/designer/input-control-switch/input-control-switch";
@ -99,6 +99,7 @@ export namespace Components {
"items": Array<DropdownButtonItem>;
"origin": DropdownButtonOrigin;
"text": string;
"theme": string;
}
interface ElsaDropdownInput {
"inputContext": ActivityInputContext;
@ -108,7 +109,7 @@ export namespace Components {
}
interface ElsaFlowchart {
"addActivity": (args: AddActivityArgs) => Promise<Activity>;
"autoLayout": (direction: "TB" | "BT" | "LR" | "RL") => Promise<void>;
"autoLayout": (direction: LayoutDirection) => Promise<void>;
"export": () => Promise<Activity>;
"getGraph": () => Promise<Graph>;
"interactiveMode": boolean;
@ -259,7 +260,6 @@ export namespace Components {
"workflowDefinition"?: WorkflowDefinition;
}
interface ElsaWorkflowDefinitionEditorToolbar {
"autoLayout": (direction: "TB" | "BT" | "LR" | "RL") => Promise<void>;
"zoomToFit": () => Promise<void>;
}
interface ElsaWorkflowDefinitionEditorToolbox {
@ -409,6 +409,10 @@ export interface ElsaWorkflowDefinitionEditorCustomEvent<T> extends CustomEvent<
detail: T;
target: HTMLElsaWorkflowDefinitionEditorElement;
}
export interface ElsaWorkflowDefinitionEditorToolbarCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLElsaWorkflowDefinitionEditorToolbarElement;
}
export interface ElsaWorkflowDefinitionPropertiesEditorCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLElsaWorkflowDefinitionPropertiesEditorElement;
@ -916,6 +920,7 @@ declare namespace LocalJSX {
"onItemSelected"?: (event: ElsaDropdownButtonCustomEvent<DropdownButtonItem>) => void;
"origin"?: DropdownButtonOrigin;
"text"?: string;
"theme"?: string;
}
interface ElsaDropdownInput {
"inputContext"?: ActivityInputContext;
@ -1078,7 +1083,7 @@ declare namespace LocalJSX {
"workflowDefinition"?: WorkflowDefinition;
}
interface ElsaWorkflowDefinitionEditorToolbar {
"autoLayout"?: (direction: "TB" | "BT" | "LR" | "RL") => Promise<void>;
"onAutoLayout"?: (event: ElsaWorkflowDefinitionEditorToolbarCustomEvent<LayoutDirection>) => void;
"zoomToFit"?: () => Promise<void>;
}
interface ElsaWorkflowDefinitionEditorToolbox {

View file

@ -6,25 +6,29 @@ import {DropdownButtonItem, DropdownButtonOrigin} from "./models";
tag: 'elsa-dropdown-button',
shadow: false,
})
export class ElsaContextMenu {
export class DropdownButton {
@Prop() public text: string;
@Prop() public icon?: any;
@Prop() public origin: DropdownButtonOrigin = DropdownButtonOrigin.TopLeft;
@Prop() public items: Array<DropdownButtonItem> = [];
@Prop() public theme: string = 'Secondary';
@Event() public itemSelected: EventEmitter<DropdownButtonItem>
private contextMenu: HTMLElement;
private element: HTMLElement;
public render() {
const buttonClass = this.theme == 'Secondary' ? 'btn-secondary' : 'btn-primary';
const arrowClass = this.theme == 'Secondary' ? 'text-gray-400' : 'text-white';
return (
<div class="relative" ref={el => this.element = el}>
<button onClick={e => this.toggleMenu()} type="button"
class="w-full bg-white border border-gray-300 rounded-md shadow-sm px-4 py-2 inline-flex justify-center text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
class={`btn ${buttonClass} w-full border`}
aria-haspopup="true" aria-expanded="false">
{this.renderIcon()}
{this.text}
<svg class="ml-2.5 -mr-1.5 h-5 w-5 text-gray-400" x-description="Heroicon name: chevron-down" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<svg class={`ml-2.5 -mr-1.5 h-5 w-5 ${arrowClass}`} x-description="Heroicon name: chevron-down" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
@ -83,6 +87,10 @@ export class ElsaContextMenu {
private async onItemClick(e: Event, menuItem: DropdownButtonItem) {
e.preventDefault();
if(!!menuItem.handler)
menuItem.handler();
this.itemSelected.emit(menuItem);
this.closeContextMenu();
}

View file

@ -13,7 +13,7 @@ export class ModalDialog {
@Prop() modalDialogInstance: ModalDialogInstance;
@Prop() actions: Array<ModalActionDefinition> = [];
@Prop() size: string = 'sm:max-w-6xl';
@Prop() size: string = 'max-w-6xl';
@Prop() type: ModalType = ModalType.Default;
@Prop() autoHide: boolean = true;
@Prop() content: () => any = () => <div/>;
@ -68,12 +68,6 @@ export class ModalDialog {
}
}
componentWillRender() {
if (this.type == ModalType.Default) {
this.size += " sm:w-full";
}
}
componentDidRender() {
if (this.isVisible) {
enter(this.overlay);

View file

@ -17,7 +17,7 @@ export interface Activity {
metadata: any;
canStartWorkflow?: boolean;
runAsynchronously?: boolean;
applicationProperties: any;
customProperties: any;
[name: string]: any;
}
@ -30,6 +30,11 @@ export interface Container extends Activity {
variables: Array<Variable>;
}
export interface Workflow extends Activity {
root: Activity;
variables: Array<Variable>;
}
export interface Variable {
name: string;
typeName: string;

View file

@ -72,7 +72,7 @@ export class ActivityDefinitionsPlugin implements Plugin {
connections: [],
id: newName,
metadata: {},
applicationProperties: {},
customProperties: {},
variables: []
} as Flowchart;

View file

@ -7,7 +7,7 @@ import './ports';
import {ActivityNodeShape} from './shapes';
import {Activity, ActivityDeletedArgs, ActivityDescriptor, ActivitySelectedArgs, ContainerSelectedArgs, EditChildActivityArgs, GraphUpdatedArgs, Port, WorkflowUpdatedArgs} from '../../models';
import {createGraph} from './graph-factory';
import {AddActivityArgs, Connection, Flowchart, FlowchartModel, FlowchartNavigationItem, FlowchartPathItem, RenameActivityArgs, UpdateActivityArgs} from './models';
import {AddActivityArgs, Connection, Flowchart, FlowchartModel, FlowchartNavigationItem, FlowchartPathItem, LayoutDirection, RenameActivityArgs, UpdateActivityArgs} from './models';
import {NodeFactory} from "./node-factory";
import {Container} from "typedi";
import {ActivityNode, createActivityLookup, EventBus, flatten, PortProviderRegistry, walkActivities} from "../../services";
@ -23,7 +23,6 @@ import {DagreLayout, OutNode} from '@antv/layout';
import {adjustPortMarkupByNode, rebuildGraph} from '../../utils/graph';
import {WorkflowDefinition} from "../workflow-definitions/models/entities";
import FlowchartTunnel, {FlowchartState} from "./state";
import {WorkflowDefinitionUpdatedArgs} from "../workflow-definitions/models/ui";
const FlowchartTypeName = 'Elsa.Flowchart';
@ -115,7 +114,8 @@ export class FlowchartComponent {
}
@Method()
async autoLayout(direction: "TB" | "BT" | "LR" | "RL") {
async autoLayout(direction: LayoutDirection) {
debugger;
const dagreLayout = new DagreLayout({
type: 'dagre',
rankdir: direction,
@ -150,7 +150,8 @@ export class FlowchartComponent {
this.updateActivity({id: activity.id, originalId: activity.id, activity: activity});
});
//this.importInternal()
this.updateGraphInternal(flowchartModel.activities, flowchartModel.connections);
this.graphUpdated.emit({});
}
@Method()
@ -166,7 +167,7 @@ export class FlowchartComponent {
id: id,
type: descriptor.typeName,
version: descriptor.version,
applicationProperties: {},
customProperties: {},
metadata: {
designer: {
position: {
@ -276,8 +277,11 @@ export class FlowchartComponent {
await this.setupGraph(childFlowchart);
}
async componentDidLoad() {
async componentWillLoad() {
this.updateLookups();
}
async componentDidLoad() {
await this.createAndInitializeGraph();
}
@ -296,7 +300,7 @@ export class FlowchartComponent {
connections: [],
metadata: {},
variables: [],
applicationProperties: {},
customProperties: {},
canStartWorkflow: false
};
}
@ -392,6 +396,11 @@ export class FlowchartComponent {
private setupGraph = async (flowchart: Flowchart) => {
const activities = flowchart.activities;
const connections = flowchart.connections;
this.updateGraphInternal(activities, connections);
await this.scrollToStart();
};
private updateGraphInternal = (activities: Array<Activity>, connections: Array<Connection>) => {
const edges: Array<Edge.Metadata> = [];
// Create an X6 node for each activity.
@ -416,8 +425,7 @@ export class FlowchartComponent {
this.graph.unfreeze();
rebuildGraph(this.graph);
await this.scrollToStart();
};
}
private getFlowchartModel = (): FlowchartModel => {
const graph = this.graph;

View file

@ -47,3 +47,5 @@ export interface RenameActivityArgs {
newId: string;
activity: Activity;
}
export type LayoutDirection = 'LR' | 'TB' | 'RL' | 'BT';

View file

@ -15,6 +15,7 @@ import {ActivityPropertyChangedEventArgs, WorkflowDefinitionPropsUpdatedArgs, Wo
import {WorkflowDefinition} from "../models/entities";
import {WorkflowDefinitionsApi} from "../services/api"
import WorkflowDefinitionTunnel, {WorkflowDefinitionState} from "../../../state/workflow-definition-state";
import {LayoutDirection} from "../../flowchart/models";
@Component({
tag: 'elsa-workflow-definition-editor',
@ -226,7 +227,10 @@ export class WorkflowDefinitionEditor {
private onZoomToFit = async () => await this.flowchart.zoomToFit();
private onAutoLayout = async (direction: "TB" | "BT" | "LR" | "RL") => await this.flowchart.autoLayout(direction);
private onAutoLayout = async (direction: LayoutDirection) => {
debugger;
await this.flowchart.autoLayout(direction);
};
private onActivityUpdated = async (e: CustomEvent<ActivityUpdatedArgs>) => {
await this.flowchart.updateActivity({
@ -279,7 +283,7 @@ export class WorkflowDefinitionEditor {
return (
<WorkflowDefinitionTunnel.Provider state={state}>
<div class="absolute inset-0" ref={el => this.container = el}>
<elsa-workflow-definition-editor-toolbar zoomToFit={this.onZoomToFit} autoLayout={this.onAutoLayout}/>
<elsa-workflow-definition-editor-toolbar zoomToFit={this.onZoomToFit} onAutoLayout={(e: CustomEvent<LayoutDirection>) => this.onAutoLayout(e.detail)}/>
<elsa-panel
class="elsa-activity-picker-container z-30"
position={PanelPosition.Left}

View file

@ -1,4 +1,6 @@
import { Component, h, Prop } from '@stencil/core';
import {Component, h, Prop, Event, EventEmitter} from '@stencil/core';
import {DropdownButtonItem} from "../../../components/shared/dropdown-button/models";
import {LayoutDirection} from "../../flowchart/models";
@Component({
tag: 'elsa-workflow-definition-editor-toolbar',
@ -7,18 +9,22 @@ export class Toolbar {
@Prop()
public zoomToFit: () => Promise<void>;
@Prop()
public autoLayout: (direction: "TB" | "BT" | "LR" | "RL") => Promise<void>;
@Event()
public autoLayout: EventEmitter<LayoutDirection>;
render() {
const layoutButtons: Array<DropdownButtonItem> = [{
text: 'Horizontally',
handler: () => this.autoLayout.emit('LR')
},{
text: 'Vertically',
handler: () => this.autoLayout.emit('TB')
}];
return (
<div class="elsa-panel-toolbar flex justify-center absolute border-b border-gray-200 top-0 px-1 pl-4 pb-2 text-sm bg-white z-10 space-x-2">
<button class="btn btn-primary" onClick={() => this.autoLayout("LR")}>
Auto-Layout (LR)
</button>
<button class="btn btn-primary" onClick={() => this.autoLayout("TB")}>
Auto-Layout (TB)
</button>
<elsa-dropdown-button text="Auto-layout" theme="Primary" items={layoutButtons}/>
<button onClick={this.zoomToFit}class="btn btn-primary">
Zoom to fit
</button>

View file

@ -43,7 +43,7 @@ export class WorkflowDefinitionsPlugin implements Plugin {
const newWorkflowDefinitionItem: MenuItem = {
text: 'Workflow Definition',
clickHandler: this.onNewWorkflowDefinitionClick
clickHandler: this.onNewWorkflowDefinitionSelected
}
const importWorkflowDefinitionItem: MenuItem = {
@ -80,7 +80,7 @@ export class WorkflowDefinitionsPlugin implements Plugin {
connections: [],
id: newName,
metadata: {},
applicationProperties: {},
customProperties: {},
variables: []
} as Flowchart;
@ -146,7 +146,7 @@ export class WorkflowDefinitionsPlugin implements Plugin {
});
};
private onNewWorkflowDefinitionClick = async () => {
private onNewWorkflowDefinitionSelected = async () => {
await this.newWorkflow();
this.modalDialogService.hide(this.workflowDefinitionBrowserInstance);
};
@ -158,32 +158,16 @@ export class WorkflowDefinitionsPlugin implements Plugin {
private onWorkflowUpdated = async (e: CustomEvent<WorkflowDefinitionUpdatedArgs>) => {
const updatedWorkflowDefinition = e.detail.workflowDefinition;
await this.saveWorkflowDefinition(updatedWorkflowDefinition, false);
// if (e.detail.latestVersionNumber == undefined) {
// await this.saveWorkflowDefinition(updatedWorkflowDefinition, false);
// return;
// }
// if (updatedWorkflowDefinition.version == e.detail.latestVersionNumber || updatedWorkflowDefinition.isPublished) {
// const currentWorkflowDefinition = await this.api.get({definitionId: updatedWorkflowDefinition.definitionId, versionOptions: {version: updatedWorkflowDefinition.version}});
// if (!isEqual(currentWorkflowDefinition.root.activities, updatedWorkflowDefinition.root.activities) || !isEqual(currentWorkflowDefinition.variables, updatedWorkflowDefinition.variables)) {
// if (updatedWorkflowDefinition.isPublished)
// updatedWorkflowDefinition.version = e.detail.latestVersionNumber;
//
// await this.saveWorkflowDefinition(updatedWorkflowDefinition, false);
// }
// }
}
private onBrowseWorkflowDefinitions = async () => {
const closeAction = DefaultModalActions.Close();
const newAction = DefaultModalActions.New(this.onNewWorkflowDefinitionClick);
const newAction = DefaultModalActions.New(this.onNewWorkflowDefinitionSelected);
const actions = [closeAction, newAction];
this.workflowDefinitionBrowserInstance = this.modalDialogService.show(() =>
<elsa-workflow-definition-browser onWorkflowDefinitionSelected={this.onWorkflowDefinitionSelected} onNewWorkflowDefinitionSelected={this.onNewWorkflowDefinitionClick}/>,
<elsa-workflow-definition-browser onWorkflowDefinitionSelected={this.onWorkflowDefinitionSelected} onNewWorkflowDefinitionSelected={this.onNewWorkflowDefinitionSelected}/>,
{actions})
}
@ -198,16 +182,10 @@ export class WorkflowDefinitionsPlugin implements Plugin {
e.detail.begin();
const notification = NotificationService.createNotification({title: 'Publishing', id: uuid(), text: 'Workflow is being publishing. Please wait'})
const workflowDefinition = await this.workflowDefinitionEditorElement.getWorkflowDefinition();
//await this.eventBus.emit(NotificationEventTypes.Add, this, {id: workflowDefinition.definitionId, message: `Starting publishing ${workflowDefinition.name}`});
await this.saveWorkflowDefinition(workflowDefinition, true);
NotificationService.updateNotification(notification, {title: 'Workflow published', text: 'Published !'})
e.detail.complete();
// setTimeout(async () => {
// await this.eventBus.emit(NotificationEventTypes.Update, this, {id: workflowDefinition.definitionId, message: `${workflowDefinition.name} publish finished`});
// e.detail.complete();
// }, 2000)
}
private onExportClicked = async (e: CustomEvent) => {

View file

@ -108,8 +108,6 @@ export class WorkflowInstanceBrowser {
<th>Status</th>
<th class="optional">Created</th>
<th class="optional">Finished</th>
<th class="optional">Executed</th>
<th class="optional">Faulted</th>
<th/>
</tr>
</thead>
@ -143,8 +141,6 @@ export class WorkflowInstanceBrowser {
</td>
<td class="optional">{formatTimestamp(workflowInstance.createdAt, '-')}</td>
<td class="optional">{formatTimestamp(workflowInstance.finishedAt, '-')}</td>
<td class="optional">{formatTimestamp(workflowInstance.lastExecutedAt, '-')}</td>
<td class="optional">{formatTimestamp(workflowInstance.faultedAt, '-')}</td>
<td class="pr-6">
<elsa-context-menu menuItems={[
{text: 'Edit', clickHandler: e => this.onWorkflowInstanceClick(e, workflowInstance), icon: <EditIcon/>},

View file

@ -1,5 +1,5 @@
import {Component, h, Prop, State, Watch} from "@stencil/core";
import {ActivityDescriptor, WorkflowExecutionLogRecord, WorkflowInstance} from "../../../models";
import {ActivityDescriptor, Workflow, WorkflowExecutionLogRecord, WorkflowInstance} from "../../../models";
import {Container} from "typedi";
import {ActivityIconRegistry, ActivityNode, createActivityNodeMap, flatten, walkActivities} from "../../../services";
import {durationToString, formatTime, getDuration, Hash, isNullOrWhitespace} from "../../../utils";
@ -115,7 +115,7 @@ export class Journal {
const activityDisplayText = isNullOrWhitespace(activityMetadata.displayText) ? activity.id : activityMetadata.displayText;
const duration = durationToString(block.duration);
const status = block.completed ? 'Completed' : 'Started';
const icon = iconRegistry.get(activity.type)({size: ActivityIconSize.Small});
const icon = iconRegistry.getOrDefault(activity.type)({size: ActivityIconSize.Small});
const expanded = !!expandedBlocks.find(x => x == block);
const toggleIcon = expanded
@ -166,7 +166,19 @@ export class Journal {
if (!this.workflowInstance || !this.workflowDefinition)
return;
this.nodes = flatten(walkActivities(this.workflowDefinition.root));
const workflow: Workflow = {
type: 'Elsa.Workflow',
id: 'Workflow1', // Always 'Workflow1'.
version: this.workflowDefinition.version,
customProperties: this.workflowDefinition.customProperties,
canStartWorkflow: false,
runAsynchronously: false,
metadata: {},
root: this.workflowDefinition.root,
variables: this.workflowDefinition.variables
};
this.nodes = flatten(walkActivities(workflow));
this.nodeMap = createActivityNodeMap(this.nodes);
};

View file

@ -15,7 +15,7 @@ import {WorkflowEditorEventTypes} from "../../workflow-definitions/models/ui";
@Component({
tag: 'elsa-workflow-instance-viewer',
styleUrl: 'workflow-instance-viewer.scss',
styleUrl: 'viewer.scss',
})
export class WorkflowInstanceViewer {
private readonly pluginRegistry: PluginRegistry;
@ -99,7 +99,12 @@ export class WorkflowInstanceViewer {
public async importWorkflow(workflowDefinition: WorkflowDefinition, workflowInstance: WorkflowInstance): Promise<void> {
this.workflowInstanceState = workflowInstance;
await this.updateWorkflowDefinition(workflowDefinition);
//await this.flowchartElement.import(workflowDefinition.root);
// Update the flowchart after state is updated.
window.requestAnimationFrame(async () => {
await this.flowchartElement.updateGraph();
});
await this.eventBus.emit(WorkflowEditorEventTypes.WorkflowDefinition.Imported, this, {workflowDefinition});
}
// Updates the workflow definition without importing it into the designer.
@ -165,6 +170,7 @@ export class WorkflowInstanceViewer {
</elsa-panel>
<elsa-flowchart
ref={el => this.flowchartElement = el}
workflowDefinition={workflowDefinition}
interactiveMode={false}/>
<elsa-panel
class="elsa-workflow-editor-container"

View file

@ -53,7 +53,7 @@ export class WorkflowInstancesPlugin implements Plugin {
this.workflowInstanceBrowserInstance = this.modalDialogService.show(() =>
<elsa-workflow-instance-browser onWorkflowInstanceSelected={this.onWorkflowInstanceSelected}/>,
{actions: actions, size: 'max-w-10xl'})
{actions: actions, size: 'max-w-screen-2xl'})
}
private onWorkflowInstanceSelected = async (e: CustomEvent<WorkflowInstanceSummary>) => {

View file

@ -1,13 +1,30 @@
using Elsa.Workflows.Runtime.Features;
using Elsa.Workflows.Runtime.Implementations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.EntityFrameworkCore.Modules.Runtime
{
/// <summary>
/// Provides extensions to the <see cref="WorkflowRuntimeFeature"/> feature.
/// </summary>
public static class Extensions
{
/// <summary>
/// Configures the <see cref="WorkflowRuntimeFeature"/> to use the <see cref="EFCoreRuntimePersistenceFeature"/>.
/// </summary>
public static WorkflowRuntimeFeature UseEntityFrameworkCore(this WorkflowRuntimeFeature feature, Action<EFCoreRuntimePersistenceFeature>? configure = default)
{
feature.Module.Configure(configure);
return feature;
}
/// <summary>
/// Configures the workflow runtime to send workflow state to the <see cref="AsyncWorkflowStateExporter"/>.
/// </summary>
public static WorkflowRuntimeFeature UseAsyncWorkflowStateExporter(this WorkflowRuntimeFeature feature)
{
feature.WorkflowStateExporter = sp => ActivatorUtilities.CreateInstance<AsyncWorkflowStateExporter>(sp);
return feature;
}
}
}

View file

@ -24,7 +24,7 @@ public static class ModuleExtensions
/// </summary>
public static IModule UseIdentity(this IModule module, string signingKey, string issuer = "http://elsa.api", string audience = "http://elsa.api", TimeSpan? tokenLifetime = default)
{
module.UseIdentity(identity => identity.IdentityOptions = new IdentityOptions
module.UseIdentity(identity => identity.TokenOptions = new IdentityTokenOptions
{
Audience = audience,
Issuer = issuer,

View file

@ -21,7 +21,7 @@ public class DefaultAuthenticationFeature : FeatureBase
public override void Apply()
{
var identityFeature = Module.Configure<IdentityFeature>();
var identityOptions = identityFeature.IdentityOptions;
var identityOptions = identityFeature.TokenOptions;
Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)

View file

@ -23,16 +23,16 @@ public class IdentityFeature : FeatureBase
{
}
/// <summary>
/// A flag indicating whether a default user should be created.
/// </summary>
public bool CreateDefaultUser { get; set; }
/// <summary>
/// A delegate to configure <see cref="Options.IdentityOptions"/>.
/// </summary>
public IdentityOptions IdentityOptions { get; set; } = new();
/// <summary>
/// A delegate to configure <see cref="Options.IdentityTokenOptions"/>.
/// </summary>
public IdentityTokenOptions TokenOptions { get; set; } = new();
/// <summary>
/// A delegate that creates an instance of an implementation of <see cref="IUserStore"/>.
/// </summary>
@ -52,14 +52,14 @@ public class IdentityFeature : FeatureBase
/// <inheritdoc />
public override void ConfigureHostedServices()
{
if(CreateDefaultUser)
if(IdentityOptions.CreateDefaultAdmin)
Module.ConfigureHostedService<SetupDefaultUserHostedService>();
}
/// <inheritdoc />
public override void Apply()
{
Services.Configure<IdentityOptions>(options => options.CopyFrom(IdentityOptions));
Services.Configure<IdentityTokenOptions>(options => options.CopyFrom(TokenOptions));
Services
.AddMemoryStore<User, MemoryUserStore>()

View file

@ -12,9 +12,9 @@ namespace Elsa.Identity.Implementations;
public class DefaultAccessTokenIssuer : IAccessTokenIssuer
{
private readonly ISystemClock _systemClock;
private readonly IdentityOptions _identityOptions;
private readonly IdentityTokenOptions _identityOptions;
public DefaultAccessTokenIssuer(ISystemClock systemClock, IOptions<IdentityOptions> identityOptions)
public DefaultAccessTokenIssuer(ISystemClock systemClock, IOptions<IdentityTokenOptions> identityOptions)
{
_systemClock = systemClock;
_identityOptions = identityOptions.Value;

View file

@ -1,42 +1,6 @@
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
namespace Elsa.Identity.Options;
public class IdentityOptions
{
public string SigningKey { get; set; }
public string Issuer { get; set; } = "http://elsa.api";
public string Audience { get; set; } = "http://elsa.api";
public TimeSpan? Lifetime { get; set; } = TimeSpan.FromHours(1);
public SecurityKey CreateSecurityKey() => new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SigningKey));
public void ConfigureJwtBearerOptions(JwtBearerOptions options) =>
options.TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKey = CreateSecurityKey(),
ValidAudience = Audience,
ValidIssuer = Issuer
};
/// <summary>
/// Deconstructor.
/// </summary>
internal void Deconstruct(out string signingKey, out string issuer, out string audience, out TimeSpan? lifetime)
{
signingKey = SigningKey;
issuer = Issuer;
audience = Audience;
lifetime = Lifetime;
}
internal void CopyFrom(IdentityOptions identityOptions)
{
SigningKey = identityOptions.SigningKey;
Audience = identityOptions.Audience;
Issuer = identityOptions.Issuer;
Lifetime = identityOptions.Lifetime;
}
public bool CreateDefaultAdmin { get; set; }
}

View file

@ -0,0 +1,42 @@
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
namespace Elsa.Identity.Options;
public class IdentityTokenOptions
{
public string SigningKey { get; set; }
public string Issuer { get; set; } = "http://elsa.api";
public string Audience { get; set; } = "http://elsa.api";
public TimeSpan? Lifetime { get; set; } = TimeSpan.FromHours(1);
public SecurityKey CreateSecurityKey() => new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SigningKey));
public void ConfigureJwtBearerOptions(JwtBearerOptions options) =>
options.TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKey = CreateSecurityKey(),
ValidAudience = Audience,
ValidIssuer = Issuer
};
/// <summary>
/// Deconstructor.
/// </summary>
internal void Deconstruct(out string signingKey, out string issuer, out string audience, out TimeSpan? lifetime)
{
signingKey = SigningKey;
issuer = Issuer;
audience = Audience;
lifetime = Lifetime;
}
internal void CopyFrom(IdentityTokenOptions identityOptions)
{
SigningKey = identityOptions.SigningKey;
Audience = identityOptions.Audience;
Issuer = identityOptions.Issuer;
Lifetime = identityOptions.Lifetime;
}
}

View file

@ -0,0 +1,17 @@
using Elsa.Jobs.Activities.Middleware.Activities;
using Elsa.Workflows.Core.Pipelines.ActivityExecution;
using Elsa.Workflows.Core.Services;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Adds extensions to <see cref="IActivityExecutionPipelineBuilder"/>.
/// </summary>
public static class JobBasedActivityInvokerMiddlewareExtensions
{
/// <summary>
/// Installs the <see cref="JobBasedActivityInvokerMiddleware"/>.
/// </summary>
public static IActivityExecutionPipelineBuilder UseJobBasedActivityInvoker(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<JobBasedActivityInvokerMiddleware>();
}

View file

@ -0,0 +1,15 @@
using Elsa.Workflows.Core.Features;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Adds an extension method to the <see cref="WorkflowsFeature"/> that installs a default workflow runtime execution pipeline.
/// </summary>
public static class WorkflowsFeatureExtensions
{
/// <summary>
/// Installs a default workflow runtime execution pipeline.
/// </summary>
public static WorkflowsFeature WithJobBasedActivityExecutionPipeline(this WorkflowsFeature workflowsFeature) => workflowsFeature.WithActivityExecutionPipeline(pipeline => pipeline.UseJobBasedActivityInvoker());
}

View file

@ -5,16 +5,10 @@ using Elsa.Jobs.Services;
using Elsa.Workflows.Core.Middleware.Activities;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Pipelines.ActivityExecution;
using Elsa.Workflows.Core.Services;
using Elsa.Workflows.Management.Services;
namespace Elsa.Jobs.Activities.Middleware.Activities;
public static class JobBasedActivityInvokerMiddlewareExtensions
{
public static IActivityExecutionBuilder UseJobBasedActivityInvoker(this IActivityExecutionBuilder builder) => builder.UseMiddleware<JobBasedActivityInvokerMiddleware>();
}
/// <summary>
/// Executes the current activity from a background job if the activity is of kind <see cref="ActivityKind.Job"/> or <see cref="ActivityKind.Task"/>
/// </summary>
@ -26,6 +20,7 @@ public class JobBasedActivityInvokerMiddleware : DefaultActivityInvokerMiddlewar
private readonly IJobFactory _jobFactory;
private readonly IJobQueue _jobQueue;
/// <inheritdoc />
public JobBasedActivityInvokerMiddleware(
ActivityMiddlewareDelegate next,
IActivityRegistry activityRegistry,
@ -39,6 +34,7 @@ public class JobBasedActivityInvokerMiddleware : DefaultActivityInvokerMiddlewar
_jobQueue = jobQueue;
}
/// <inheritdoc />
protected override async ValueTask ExecuteActivityAsync(ActivityExecutionContext context)
{
var activity = context.Activity;

View file

@ -5,22 +5,30 @@ using Elsa.Features.Services;
using Elsa.Scheduling.Handlers;
using Elsa.Scheduling.Implementations;
using Elsa.Scheduling.Services;
using Elsa.Workflows.Management.Features;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Scheduling.Features;
/// <summary>
/// Provides scheduling features to the system.
/// </summary>
[DependsOn(typeof(SystemClockFeature))]
public class SchedulingFeature : FeatureBase
{
/// <inheritdoc />
public SchedulingFeature(IModule module) : base(module)
{
}
/// <inheritdoc />
public override void Apply()
{
Services
.AddSingleton<IWorkflowTriggerScheduler, WorkflowTriggerScheduler>()
.AddSingleton<IWorkflowBookmarkScheduler, WorkflowBookmarkScheduler>()
.AddNotificationHandlersFrom<ScheduleWorkflows>();
Module.Configure<WorkflowManagementFeature>(management => management.AddActivitiesFrom<SchedulingFeature>());
}
}

View file

@ -11,10 +11,10 @@ public static class WorkflowExecutionBuilderExtensions
/// <summary>
/// Installs middleware that enables the use of workflow context.
/// </summary>
public static IWorkflowExecutionBuilder UseWorkflowContexts(this IWorkflowExecutionBuilder builder) => builder.UseMiddleware<WorkflowContextWorkflowExecutionMiddleware>();
public static IWorkflowExecutionPipelineBuilder UseWorkflowContexts(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<WorkflowContextWorkflowExecutionMiddleware>();
/// <summary>
/// Installs middleware that enables the use of workflow context.
/// </summary>
public static IActivityExecutionBuilder UseWorkflowContexts(this IActivityExecutionBuilder builder) => builder.UseMiddleware<WorkflowContextActivityExecutionMiddleware>();
public static IActivityExecutionPipelineBuilder UseWorkflowContexts(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<WorkflowContextActivityExecutionMiddleware>();
}

View file

@ -3,10 +3,15 @@ using FastEndpoints;
namespace Elsa.Workflows.Api.Endpoints.WorkflowInstances.Get;
/// <summary>
/// Maps a <see cref="WorkflowInstance"/> to <see cref="Response"/>.
/// </summary>
public class WorkflowInstanceMapper : ResponseMapper<Response, WorkflowInstance>
{
/// <inheritdoc />
public override Response FromEntity(WorkflowInstance e) => new()
{
Id = e.Id,
DefinitionId = e.DefinitionId,
DefinitionVersionId = e.DefinitionVersionId,
Version = e.Version,

View file

@ -10,6 +10,7 @@ public class Request
public class Response
{
public string Id { get; set; }
public string DefinitionId { get; init; } = default!;
public string DefinitionVersionId { get; init; } = default!;
public int Version { get; init; }

View file

@ -41,7 +41,6 @@ public abstract class Composite : ActivityBase, IVariableContainer
/// The activity to schedule when this activity executes.
/// </summary>
[Port]
[Browsable(false)]
[JsonIgnore] // Composite activities' Root is intended to be constructed from code only.
public IActivity Root { get; set; } = new Sequence();

View file

@ -1,4 +1,5 @@
using System.ComponentModel;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;
namespace Elsa.Workflows.Core.Activities;
@ -7,6 +8,7 @@ namespace Elsa.Workflows.Core.Activities;
/// This activity is instantiated in case a workflow references an activity type that could not be found.
/// </summary>
[Browsable(false)]
[Activity("Elsa", "Workflows", "A placeholder activity that will be used in case a workflow definition references an activity type that cannot be found.")]
public class NotFoundActivity : Activity
{
/// <inheritdoc />

View file

@ -10,9 +10,12 @@ using Elsa.Workflows.Core.ActivityNodeResolvers;
using Elsa.Workflows.Core.Builders;
using Elsa.Workflows.Core.Expressions;
using Elsa.Workflows.Core.Implementations;
using Elsa.Workflows.Core.Middleware.Activities;
using Elsa.Workflows.Core.Middleware.Workflows;
using Elsa.Workflows.Core.Pipelines.ActivityExecution;
using Elsa.Workflows.Core.Pipelines.WorkflowExecution;
using Elsa.Workflows.Core.Serialization;
using Elsa.Workflows.Core.Serialization.Converters;
using Elsa.Workflows.Core.Services;
using Microsoft.Extensions.DependencyInjection;
@ -40,6 +43,16 @@ public class WorkflowsFeature : FeatureBase
/// </summary>
public Func<IServiceProvider, IStandardOutStreamProvider> StandardOutStreamProvider { get; set; } = _ => new StandardOutStreamProvider(Console.Out);
/// <summary>
/// A delegate to configure the <see cref="IWorkflowExecutionPipeline"/>.
/// </summary>
public Action<IWorkflowExecutionPipelineBuilder> WorkflowExecutionPipeline { get; set; } = builder => builder.UseDefaultActivityScheduler();
/// <summary>
/// A delegate to configure the <see cref="IActivityExecutionPipeline"/>.
/// </summary>
public Action<IActivityExecutionPipelineBuilder> ActivityExecutionPipeline { get; set; } = builder => builder.UseDefaultActivityInvoker();
/// <summary>
/// Fluent method to set <see cref="StandardInStreamProvider"/>.
/// </summary>
@ -58,6 +71,24 @@ public class WorkflowsFeature : FeatureBase
return this;
}
/// <summary>
/// Fluent method to configure the <see cref="IWorkflowExecutionPipeline"/>.
/// </summary>
public WorkflowsFeature WithWorkflowExecutionPipeline(Action<IWorkflowExecutionPipelineBuilder> setup)
{
WorkflowExecutionPipeline = setup;
return this;
}
/// <summary>
/// Fluent method to configure the <see cref="IActivityExecutionPipeline"/>.
/// </summary>
public WorkflowsFeature WithActivityExecutionPipeline(Action<IActivityExecutionPipelineBuilder> setup)
{
ActivityExecutionPipeline = setup;
return this;
}
/// <inheritdoc />
public override void Apply()
{
@ -78,7 +109,7 @@ public class WorkflowsFeature : FeatureBase
.AddSingleton<IActivitySchedulerFactory, ActivitySchedulerFactory>()
.AddSingleton<IHasher, Hasher>()
.AddSingleton<IBookmarkHasher, BookmarkHasher>()
.AddSingleton<IIdentityGenerator, RandomIdentityGenerator>()
.AddSingleton<IIdentityGenerator, GuidIdentityGenerator>()
.AddSingleton<IWorkflowExecutionContextFactory, DefaultWorkflowExecutionContextFactory>()
.AddSingleton<IBookmarkPayloadSerializer, BookmarkPayloadSerializer>()
.AddTransient<WorkflowBuilder>()
@ -89,7 +120,7 @@ public class WorkflowsFeature : FeatureBase
// Pipelines.
.AddSingleton<IActivityExecutionPipeline, ActivityExecutionPipeline>()
.AddSingleton<IWorkflowExecutionPipeline, WorkflowExecutionPipeline>()
.AddSingleton<IWorkflowExecutionPipeline>(sp => new WorkflowExecutionPipeline(sp, WorkflowExecutionPipeline))
// Built-in activity services.
.AddSingleton<IActivityPortResolver, OutboundActivityPortResolver>()

View file

@ -0,0 +1,12 @@
using Elsa.Workflows.Core.Services;
namespace Elsa.Workflows.Core.Implementations;
/// <summary>
/// Generates a unique identifier using <see cref="Guid"/>.
/// </summary>
public class GuidIdentityGenerator : IIdentityGenerator
{
/// <inheritdoc />
public string GenerateId() => Guid.NewGuid().ToString("N");
}

View file

@ -1,8 +0,0 @@
using Elsa.Workflows.Core.Services;
namespace Elsa.Workflows.Core.Implementations;
public class RandomIdentityGenerator : IIdentityGenerator
{
public string GenerateId() => Guid.NewGuid().ToString("N");
}

View file

@ -7,14 +7,14 @@ using Elsa.Workflows.Core.Services;
namespace Elsa.Workflows.Core.Middleware.Activities;
/// <summary>
/// Provides extension methods to <see cref="IActivityExecutionBuilder"/>.
/// Provides extension methods to <see cref="IActivityExecutionPipelineBuilder"/>.
/// </summary>
public static class ActivityInvokerMiddlewareExtensions
{
/// <summary>
/// Adds the <see cref="DefaultActivityInvokerMiddleware"/> component to the pipeline.
/// </summary>
public static IActivityExecutionBuilder UseDefaultActivityInvoker(this IActivityExecutionBuilder builder) => builder.UseMiddleware<DefaultActivityInvokerMiddleware>();
public static IActivityExecutionPipelineBuilder UseDefaultActivityInvoker(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<DefaultActivityInvokerMiddleware>();
}
/// <summary>

View file

@ -13,7 +13,7 @@ public static class ExceptionHandlingMiddlewareExtensions
/// <summary>
/// Installs the <see cref="ExceptionHandlingMiddleware"/> component in the activity execution pipeline.
/// </summary>
public static IActivityExecutionBuilder UseExceptionHandling(this IActivityExecutionBuilder builder) => builder.UseMiddleware<ExceptionHandlingMiddleware>();
public static IActivityExecutionPipelineBuilder UseExceptionHandling(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<ExceptionHandlingMiddleware>();
}
/// <summary>

View file

@ -32,5 +32,5 @@ public class LoggingMiddleware : IActivityExecutionMiddleware
public static class LoggingMiddlewareExtensions
{
public static IActivityExecutionBuilder UseLogging(this IActivityExecutionBuilder builder) => builder.UseMiddleware<LoggingMiddleware>();
public static IActivityExecutionPipelineBuilder UseLogging(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<LoggingMiddleware>();
}

View file

@ -9,7 +9,7 @@ public static class UseActivitySchedulerMiddlewareExtensions
/// <summary>
/// Installs middleware that executes scheduled work items (activities).
/// </summary>
public static IWorkflowExecutionBuilder UseDefaultActivityScheduler(this IWorkflowExecutionBuilder builder) => builder.UseMiddleware<DefaultActivitySchedulerMiddleware>();
public static IWorkflowExecutionPipelineBuilder UseDefaultActivityScheduler(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<DefaultActivitySchedulerMiddleware>();
}
public class DefaultActivitySchedulerMiddleware : WorkflowExecutionMiddleware

View file

@ -2,6 +2,7 @@ using System.ComponentModel;
using System.Text.Json.Serialization;
using Elsa.Expressions.Models;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Services;
namespace Elsa.Workflows.Core.Models;
@ -10,6 +11,7 @@ namespace Elsa.Workflows.Core.Models;
/// Represents an executable process.
/// </summary>
[Browsable(false)]
[Activity("Elsa", "Workflows", "A workflow is an activity that executes its Root activity.")]
public class Workflow : Composite, ICloneable
{
/// <summary>

View file

@ -5,15 +5,15 @@ namespace Elsa.Workflows.Core.Pipelines.ActivityExecution;
public static class ActivityExecutionMiddlewareExtensions
{
public static IActivityExecutionBuilder UseMiddleware<TMiddleware>(this IActivityExecutionBuilder builder, params object[] args) where TMiddleware : IActivityExecutionMiddleware
public static IActivityExecutionPipelineBuilder UseMiddleware<TMiddleware>(this IActivityExecutionPipelineBuilder pipelineBuilder, params object[] args) where TMiddleware : IActivityExecutionMiddleware
{
var middleware = typeof(TMiddleware);
return builder.Use(next =>
return pipelineBuilder.Use(next =>
{
var invokeMethod = MiddlewareHelpers.GetInvokeMethod(middleware);
var ctorArgs = new[] { next }.Concat(args).Select(x => x!).ToArray();
var instance = ActivatorUtilities.CreateInstance(builder.ServiceProvider, middleware, ctorArgs);
var instance = ActivatorUtilities.CreateInstance(pipelineBuilder.ServiceProvider, middleware, ctorArgs);
return (ActivityMiddlewareDelegate)invokeMethod.CreateDelegate(typeof(ActivityMiddlewareDelegate), instance);
});
}

View file

@ -19,9 +19,9 @@ public class ActivityExecutionPipeline : IActivityExecutionPipeline
}
/// <inheritdoc />
public ActivityMiddlewareDelegate Setup(Action<IActivityExecutionBuilder> setup)
public ActivityMiddlewareDelegate Setup(Action<IActivityExecutionPipelineBuilder> setup)
{
var builder = new ActivityExecutionPipelineBuilder(_serviceProvider);
var builder = new ActivityExecutionPipelinePipelineBuilder(_serviceProvider);
setup(builder);
_pipeline = builder.Build();
return _pipeline;

View file

@ -2,18 +2,18 @@ using Elsa.Workflows.Core.Services;
namespace Elsa.Workflows.Core.Pipelines.ActivityExecution;
public class ActivityExecutionPipelineBuilder : IActivityExecutionBuilder
public class ActivityExecutionPipelinePipelineBuilder : IActivityExecutionPipelineBuilder
{
private readonly IList<Func<ActivityMiddlewareDelegate, ActivityMiddlewareDelegate>> _components = new List<Func<ActivityMiddlewareDelegate, ActivityMiddlewareDelegate>>();
public ActivityExecutionPipelineBuilder(IServiceProvider serviceProvider)
public ActivityExecutionPipelinePipelineBuilder(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
public IServiceProvider ServiceProvider { get; }
public IActivityExecutionBuilder Use(Func<ActivityMiddlewareDelegate, ActivityMiddlewareDelegate> middleware)
public IActivityExecutionPipelineBuilder Use(Func<ActivityMiddlewareDelegate, ActivityMiddlewareDelegate> middleware)
{
_components.Add(middleware);
return this;

View file

@ -3,17 +3,23 @@ using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.Core.Pipelines.WorkflowExecution;
/// <summary>
/// Provides extensions to <see cref="IWorkflowExecutionPipelineBuilder"/> that adds support for installing <see cref="IWorkflowExecutionMiddleware"/> components.
/// </summary>
public static class WorkflowExecutionMiddlewareExtensions
{
public static IWorkflowExecutionBuilder UseMiddleware<TMiddleware>(this IWorkflowExecutionBuilder builder, params object[] args) where TMiddleware: IWorkflowExecutionMiddleware
/// <summary>
/// Installs the specified middleware component into the pipeline being built.
/// </summary>
public static IWorkflowExecutionPipelineBuilder UseMiddleware<TMiddleware>(this IWorkflowExecutionPipelineBuilder pipelineBuilder, params object[] args) where TMiddleware: IWorkflowExecutionMiddleware
{
var middleware = typeof(TMiddleware);
return builder.Use(next =>
return pipelineBuilder.Use(next =>
{
var invokeMethod = MiddlewareHelpers.GetInvokeMethod(middleware);
var ctorParams = new[] { next }.Concat(args).Select(x => x!).ToArray();
var instance = ActivatorUtilities.CreateInstance(builder.ApplicationServices, middleware, ctorParams);
var ctorParams = new[] { next }.Concat(args).Select(x => x).ToArray();
var instance = ActivatorUtilities.CreateInstance(pipelineBuilder.ServiceProvider, middleware, ctorParams);
return (WorkflowMiddlewareDelegate)invokeMethod.CreateDelegate(typeof(WorkflowMiddlewareDelegate), instance);
});
}

View file

@ -4,15 +4,26 @@ using Elsa.Workflows.Core.Services;
namespace Elsa.Workflows.Core.Pipelines.WorkflowExecution;
/// <inheritdoc />
public class WorkflowExecutionPipeline : IWorkflowExecutionPipeline
{
private readonly IServiceProvider _serviceProvider;
private WorkflowMiddlewareDelegate? _pipeline;
public WorkflowExecutionPipeline(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
/// <summary>
/// Constructor.
/// </summary>
public WorkflowExecutionPipeline(IServiceProvider serviceProvider, Action<IWorkflowExecutionPipelineBuilder> pipelineBuilder)
{
_serviceProvider = serviceProvider;
Setup(pipelineBuilder);
}
/// <inheritdoc />
public WorkflowMiddlewareDelegate Pipeline => _pipeline ??= CreateDefaultPipeline();
public WorkflowMiddlewareDelegate Setup(Action<IWorkflowExecutionBuilder> setup)
/// <inheritdoc />
public WorkflowMiddlewareDelegate Setup(Action<IWorkflowExecutionPipelineBuilder> setup)
{
var builder = new WorkflowExecutionPipelineBuilder(_serviceProvider);
setup(builder);
@ -20,6 +31,7 @@ public class WorkflowExecutionPipeline : IWorkflowExecutionPipeline
return _pipeline;
}
/// <inheritdoc />
public async Task ExecuteAsync(WorkflowExecutionContext context) => await Pipeline(context);
private WorkflowMiddlewareDelegate CreateDefaultPipeline() => Setup(x => x.UseDefaultActivityScheduler());

View file

@ -2,30 +2,38 @@ using Elsa.Workflows.Core.Services;
namespace Elsa.Workflows.Core.Pipelines.WorkflowExecution;
public class WorkflowExecutionPipelineBuilder : IWorkflowExecutionBuilder
/// <inheritdoc />
public class WorkflowExecutionPipelineBuilder : IWorkflowExecutionPipelineBuilder
{
private const string ServicesKey = "workflow-execution.Services";
private readonly IList<Func<WorkflowMiddlewareDelegate, WorkflowMiddlewareDelegate>> _components = new List<Func<WorkflowMiddlewareDelegate, WorkflowMiddlewareDelegate>>();
/// <summary>
/// Constructor.
/// </summary>
public WorkflowExecutionPipelineBuilder(IServiceProvider serviceProvider)
{
ApplicationServices = serviceProvider;
ServiceProvider = serviceProvider;
}
public IDictionary<string, object?> Properties { get; } = new Dictionary<string, object?>();
/// <inheritdoc />
public IDictionary<object, object?> Properties { get; } = new Dictionary<object, object?>();
public IServiceProvider ApplicationServices
/// <inheritdoc />
public IServiceProvider ServiceProvider
{
get => GetProperty<IServiceProvider>(ServicesKey)!;
set => SetProperty(ServicesKey, value);
}
public IWorkflowExecutionBuilder Use(Func<WorkflowMiddlewareDelegate, WorkflowMiddlewareDelegate> middleware)
/// <inheritdoc />
public IWorkflowExecutionPipelineBuilder Use(Func<WorkflowMiddlewareDelegate, WorkflowMiddlewareDelegate> middleware)
{
_components.Add(middleware);
return this;
}
/// <inheritdoc />
public WorkflowMiddlewareDelegate Build()
{
WorkflowMiddlewareDelegate pipeline = _ => new ValueTask();
@ -36,6 +44,13 @@ public class WorkflowExecutionPipelineBuilder : IWorkflowExecutionBuilder
return pipeline;
}
/// <inheritdoc />
public IWorkflowExecutionPipelineBuilder Reset()
{
_components.Clear();
return this;
}
private T? GetProperty<T>(string key) => Properties.TryGetValue(key, out var value) ? (T?)value : default(T);
private void SetProperty<T>(string key, T value) => Properties[key] = value;
}

View file

@ -5,7 +5,7 @@ namespace Elsa.Workflows.Core.Services;
public interface IActivityExecutionPipeline
{
ActivityMiddlewareDelegate Setup(Action<IActivityExecutionBuilder> setup);
ActivityMiddlewareDelegate Setup(Action<IActivityExecutionPipelineBuilder> setup);
ActivityMiddlewareDelegate Pipeline { get; }
Task ExecuteAsync(ActivityExecutionContext context);
}

View file

@ -2,9 +2,9 @@ using Elsa.Workflows.Core.Pipelines.ActivityExecution;
namespace Elsa.Workflows.Core.Services;
public interface IActivityExecutionBuilder
public interface IActivityExecutionPipelineBuilder
{
IServiceProvider ServiceProvider { get; }
IActivityExecutionBuilder Use(Func<ActivityMiddlewareDelegate, ActivityMiddlewareDelegate> middleware);
IActivityExecutionPipelineBuilder Use(Func<ActivityMiddlewareDelegate, ActivityMiddlewareDelegate> middleware);
public ActivityMiddlewareDelegate Build();
}

View file

@ -1,6 +1,12 @@
namespace Elsa.Workflows.Core.Services;
/// <summary>
/// Represents a unique identity generator.
/// </summary>
public interface IIdentityGenerator
{
/// <summary>
/// Generates a unique identifier.
/// </summary>
string GenerateId();
}

View file

@ -3,7 +3,7 @@ using Elsa.Workflows.Core.Models;
namespace Elsa.Workflows.Core.Services;
/// <summary>
/// A workflow builder collects information about a workflow to be built programmatically.
/// A workflow pipelineBuilder collects information about a workflow to be built programmatically.
/// </summary>
public interface IWorkflowBuilder
{
@ -98,7 +98,7 @@ public interface IWorkflowBuilder
IWorkflowBuilder WithActivationStrategyType<T>() where T : IWorkflowActivationStrategy;
/// <summary>
/// Build a new <see cref="Workflow"/> instance using the information collected in this builder.
/// Build a new <see cref="Workflow"/> instance using the information collected in this pipelineBuilder.
/// </summary>
Workflow BuildWorkflow();

View file

@ -1,11 +0,0 @@
using Elsa.Workflows.Core.Pipelines.WorkflowExecution;
namespace Elsa.Workflows.Core.Services;
public interface IWorkflowExecutionBuilder
{
public IDictionary<string, object?> Properties { get; }
IServiceProvider ApplicationServices { get; }
IWorkflowExecutionBuilder Use(Func<WorkflowMiddlewareDelegate, WorkflowMiddlewareDelegate> middleware);
public WorkflowMiddlewareDelegate Build();
}

View file

@ -5,7 +5,7 @@ namespace Elsa.Workflows.Core.Services;
public interface IWorkflowExecutionPipeline
{
WorkflowMiddlewareDelegate Setup(Action<IWorkflowExecutionBuilder> setup);
WorkflowMiddlewareDelegate Setup(Action<IWorkflowExecutionPipelineBuilder> setup);
WorkflowMiddlewareDelegate Pipeline { get; }
Task ExecuteAsync(WorkflowExecutionContext context);
}

View file

@ -0,0 +1,34 @@
using Elsa.Workflows.Core.Pipelines.WorkflowExecution;
namespace Elsa.Workflows.Core.Services;
/// <summary>
/// Used to build a workflow execution pipeline.
/// </summary>
public interface IWorkflowExecutionPipelineBuilder
{
/// <summary>
/// A general-purpose dictionary of values that can be used by middleware components.
/// </summary>
public IDictionary<object, object?> Properties { get; }
/// <summary>
/// The current service provider to resolve services from.
/// </summary>
IServiceProvider ServiceProvider { get; }
/// <summary>
/// Installs the specified delegate as a middleware component.
/// </summary>
IWorkflowExecutionPipelineBuilder Use(Func<WorkflowMiddlewareDelegate, WorkflowMiddlewareDelegate> middleware);
/// <summary>
/// Constructs the final <see cref="WorkflowMiddlewareDelegate"/> delegate that invokes each installed middleware component.
/// </summary>
public WorkflowMiddlewareDelegate Build();
/// <summary>
/// Clears the current pipeline.
/// </summary>
IWorkflowExecutionPipelineBuilder Reset();
}

View file

@ -3,7 +3,7 @@ using Elsa.Workflows.Core.Models;
namespace Elsa.Workflows.Core.Services;
/// <summary>
/// A base class for implementing workflow definitions using the builder API.
/// A base class for implementing workflow definitions using the pipelineBuilder API.
/// </summary>
public abstract class WorkflowBase : IWorkflow
{
@ -43,7 +43,7 @@ public abstract class WorkflowBase : IWorkflow
}
/// <summary>
/// A base class for implementing workflow definitions that can return a result using the builder API.
/// A base class for implementing workflow definitions that can return a result using the pipelineBuilder API.
/// </summary>
public abstract class WorkflowBase<TResult> : WorkflowBase
{

View file

@ -1,5 +1,6 @@
using Elsa.Features.Services;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Services;
using Elsa.Workflows.Management.Features;
// ReSharper disable once CheckNamespace
@ -22,4 +23,14 @@ public static class ModuleExtensions
});
return module;
}
/// <summary>
/// Adds all types implementing <see cref="IActivity"/> to the system.
/// </summary>
public static IModule AddActivitiesFrom<TMarkerType>(this IModule module) => module.UseWorkflowManagement(management => management.AddActivitiesFrom<TMarkerType>());
/// <summary>
/// Adds the specified activity type to the system.
/// </summary>
public static IModule AddActivity<T>(this IModule module) where T:IActivity => module.UseWorkflowManagement(management => management.AddActivity<T>());
}

View file

@ -89,12 +89,9 @@ public class WorkflowManagementFeature : FeatureBase
/// </summary>
public WorkflowManagementFeature AddActivitiesFrom<TMarker>()
{
var activityTypes = typeof(TMarker).Assembly.GetExportedTypes().Where(x =>
{
var browsableAttr = x.GetCustomAttribute<BrowsableAttribute>();
var isBrowsable = browsableAttr == null || browsableAttr.Browsable;
return typeof(IActivity).IsAssignableFrom(x) && !x.IsAbstract && !x.IsInterface && !x.IsGenericType && isBrowsable;
}).ToList();
var activityTypes = typeof(TMarker).Assembly.GetExportedTypes()
.Where(x => typeof(IActivity).IsAssignableFrom(x) && !x.IsAbstract && !x.IsInterface && !x.IsGenericType)
.ToList();
return AddActivities(activityTypes);
}

View file

@ -10,12 +10,16 @@ public class ActivityRegistryPopulator : IActivityRegistryPopulator
private readonly IEnumerable<IActivityProvider> _providers;
private readonly IActivityRegistry _registry;
/// <summary>
/// Constructor.
/// </summary>
public ActivityRegistryPopulator(IEnumerable<IActivityProvider> providers, IActivityRegistry registry)
{
_providers = providers;
_registry = registry;
}
/// <inheritdoc />
public async Task PopulateRegistryAsync(CancellationToken cancellationToken)
{
_registry.Clear();
@ -24,6 +28,7 @@ public class ActivityRegistryPopulator : IActivityRegistryPopulator
await PopulateRegistryAsync(provider, cancellationToken);
}
/// <inheritdoc />
public async Task PopulateRegistryAsync(Type providerType, CancellationToken cancellationToken = default)
{
_registry.ClearProvider(providerType);

View file

@ -4,8 +4,14 @@ using Elsa.Workflows.Runtime.Features;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Adds extensions to setup the <see cref="WorkflowRuntimeFeature"/> feature.
/// </summary>
public static class ModuleExtensions
{
/// <summary>
/// Enables the <see cref="WorkflowRuntimeFeature"/> feature.
/// </summary>
public static IModule UseWorkflowRuntime(this IModule module, Action<WorkflowRuntimeFeature>? configure = default)
{
module.Configure(configure);

View file

@ -1,24 +0,0 @@
using Elsa.Workflows.Core.Pipelines.WorkflowExecution;
using Elsa.Workflows.Core.Services;
using Elsa.Workflows.Runtime.Middleware;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
public static class WorkflowExecutionPipelineBuilderExtensions
{
/// <summary>
/// Installs middleware that persists the workflow instance before and after workflow execution.
/// </summary>
public static IWorkflowExecutionBuilder UsePersistentVariables(this IWorkflowExecutionBuilder builder) => builder.UseMiddleware<PersistentVariablesMiddleware>();
/// <summary>
/// Installs middleware that persists bookmarks after workflow execution.
/// </summary>
public static IWorkflowExecutionBuilder UseBookmarkPersistence(this IWorkflowExecutionBuilder builder) => builder.UseMiddleware<PersistBookmarkMiddleware>();
/// <summary>
/// Installs middleware that persist the workflow execution journal.
/// </summary>
public static IWorkflowExecutionBuilder UseWorkflowExecutionLogPersistence(this IWorkflowExecutionBuilder builder) => builder.UseMiddleware<PersistWorkflowExecutionLogMiddleware>();
}

View file

@ -0,0 +1,40 @@
using Elsa.Features.Services;
using Elsa.Workflows.Core.Middleware.Workflows;
using Elsa.Workflows.Core.Pipelines.WorkflowExecution;
using Elsa.Workflows.Core.Services;
using Elsa.Workflows.Runtime.Middleware;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Provides extensions to <see cref="IWorkflowExecutionPipelineBuilder"/> that add various middleware components.
/// </summary>
public static class WorkflowExecutionPipelineBuilderExtensions
{
/// <summary>
/// Configures the workflow execution pipeline with commonly used components.
/// </summary>
public static IWorkflowExecutionPipelineBuilder UseDefaultRuntimePipeline(this IWorkflowExecutionPipelineBuilder pipelineBuilder) =>
pipelineBuilder
.Reset()
.UsePersistentVariables()
.UseBookmarkPersistence()
.UseWorkflowExecutionLogPersistence()
.UseDefaultActivityScheduler();
/// <summary>
/// Installs middleware that persists the workflow instance before and after workflow execution.
/// </summary>
public static IWorkflowExecutionPipelineBuilder UsePersistentVariables(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<PersistentVariablesMiddleware>();
/// <summary>
/// Installs middleware that persists bookmarks after workflow execution.
/// </summary>
public static IWorkflowExecutionPipelineBuilder UseBookmarkPersistence(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<PersistBookmarkMiddleware>();
/// <summary>
/// Installs middleware that persist the workflow execution journal.
/// </summary>
public static IWorkflowExecutionPipelineBuilder UseWorkflowExecutionLogPersistence(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<PersistWorkflowExecutionLogMiddleware>();
}

View file

@ -0,0 +1,15 @@
using Elsa.Workflows.Core.Features;
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Adds an extension method to the <see cref="WorkflowsFeature"/> that installs a default workflow runtime execution pipeline.
/// </summary>
public static class WorkflowsFeatureExtensions
{
/// <summary>
/// Installs a default workflow runtime execution pipeline.
/// </summary>
public static WorkflowsFeature WithDefaultRuntimeWorkflowExecutionPipeline(this WorkflowsFeature workflowsFeature) => workflowsFeature.WithWorkflowExecutionPipeline(pipeline => pipeline.UseDefaultRuntimePipeline());
}

View file

@ -55,9 +55,24 @@ public class WorkflowRuntimeFeature : FeatureBase
/// </summary>
public Func<IServiceProvider, IBookmarkStore> BookmarkStore { get; set; } = sp => sp.GetRequiredService<MemoryBookmarkStore>();
/// <summary>
/// A factory that instantiates an <see cref="ITriggerStore"/>.
/// </summary>
public Func<IServiceProvider, ITriggerStore> WorkflowTriggerStore { get; set; } = sp => sp.GetRequiredService<MemoryTriggerStore>();
/// <summary>
/// A factory that instantiates an <see cref="IWorkflowExecutionLogStore"/>.
/// </summary>
public Func<IServiceProvider, IWorkflowExecutionLogStore> WorkflowExecutionLogStore { get; set; } = sp => sp.GetRequiredService<MemoryWorkflowExecutionLogStore>();
/// <summary>
/// A factory that instantiates an <see cref="IDistributedLockProvider"/>.
/// </summary>
public Func<IServiceProvider, IDistributedLockProvider> DistributedLockProvider { get; set; } = _ => new FileDistributedSynchronizationProvider(new DirectoryInfo( Path.Combine(Environment.CurrentDirectory, "App_Data/locks")));
/// <summary>
/// A factory that instantiates an <see cref="IWorkflowStateExporter"/>.
/// </summary>
public Func<IServiceProvider, IWorkflowStateExporter> WorkflowStateExporter { get; set; } = sp => sp.GetRequiredService<NoopWorkflowStateExporter>();
/// <summary>

View file

@ -36,10 +36,12 @@ public class AsyncWorkflowStateExporter : IWorkflowStateExporter, ICommandHandle
_workflowInstanceStore = workflowInstanceStore;
_systemClock = systemClock;
}
/// <inheritdoc />
public async ValueTask ExportAsync(Workflow workflow, WorkflowState workflowState, CancellationToken cancellationToken) =>
await _backgroundCommandSender.SendAsync(new ExportWorkflowStateToDb(workflowState), cancellationToken);
/// <inheritdoc />
public async Task<Unit> HandleAsync(ExportWorkflowStateToDb command, CancellationToken cancellationToken)
{
var workflowState = command.WorkflowState;

View file

@ -9,7 +9,7 @@ using Elsa.Workflows.Runtime.Services;
namespace Elsa.Workflows.Runtime.Middleware;
/// <summary>
/// Takes care of loading &amp; persisting workflow variables.
/// Takes care of loading and persisting bookmarks.
/// </summary>
public class PersistBookmarkMiddleware : WorkflowExecutionMiddleware
{

View file

@ -14,12 +14,14 @@ public class PersistWorkflowExecutionLogMiddleware : WorkflowExecutionMiddleware
private readonly IWorkflowExecutionLogStore _workflowExecutionLogStore;
private readonly IIdentityGenerator _identityGenerator;
/// <inheritdoc />
public PersistWorkflowExecutionLogMiddleware(WorkflowMiddlewareDelegate next, IWorkflowExecutionLogStore workflowExecutionLogStore, IIdentityGenerator identityGenerator) : base(next)
{
_workflowExecutionLogStore = workflowExecutionLogStore;
_identityGenerator = identityGenerator;
}
/// <inheritdoc />
public override async ValueTask InvokeAsync(WorkflowExecutionContext context)
{
// Invoke next middleware.

View file

@ -22,8 +22,11 @@ var services = builder.Services;
var configuration = builder.Configuration;
var sqliteConnectionString = configuration.GetConnectionString("Sqlite")!;
var identityOptions = new IdentityOptions();
var identityTokenOptions = new IdentityTokenOptions();
var identitySection = configuration.GetSection("Identity");
var identityTokenSection = identitySection.GetSection("Tokens");
identitySection.Bind(identityOptions);
identityTokenSection.Bind(identityTokenOptions);
// Add Elsa services.
services
@ -47,8 +50,8 @@ services
)
.UseIdentity(identity =>
{
identity.CreateDefaultUser = true;
identity.IdentityOptions = identityOptions;
identity.TokenOptions = identityTokenOptions;
})
.UseWorkflowRuntime(runtime =>
{
@ -76,7 +79,7 @@ services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader()
// Authentication & Authorization.
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, identityOptions.ConfigureJwtBearerOptions);
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, identityTokenOptions.ConfigureJwtBearerOptions);
services.AddHttpContextAccessor();
services.AddSingleton<IAuthorizationHandler, LocalHostRequirementHandler>();
@ -88,15 +91,6 @@ services.AddAuthorization(options => options.AddPolicy(IdentityPolicyNames.Secur
var app = builder.Build();
var serviceProvider = app.Services;
// Configure workflow engine execution pipeline.
serviceProvider.ConfigureDefaultWorkflowExecutionPipeline(pipeline =>
pipeline
.UsePersistentVariables()
.UseBookmarkPersistence()
.UseWorkflowContexts()
.UseDefaultActivityScheduler()
);
// Configure activity execution pipeline to use the job-based activity invoker.
serviceProvider.ConfigureDefaultActivityExecutionPipeline(pipeline => pipeline.UseJobBasedActivityInvoker());

View file

@ -11,7 +11,10 @@
"Sqlite": "Data Source=elsa.sqlite.db;Cache=Shared;"
},
"Identity": {
"SigningKey": "secret-signing-key"
"CreateDefaultAdmin": true,
"Tokens": {
"SigningKey": "secret-signing-key"
}
},
"Telnyx": {
"ApiKey": "",

View file

@ -19,8 +19,8 @@ builder.Services.AddElsa(elsa =>
// Configure identity so that we can create a default admin user.
elsa.UseIdentity(identity =>
{
identity.CreateDefaultUser = builder.Environment.IsDevelopment();
identity.IdentityOptions.SigningKey = "secret-token-signing-key";
identity.IdentityOptions.CreateDefaultAdmin = builder.Environment.IsDevelopment();
identity.TokenOptions.SigningKey = "secret-token-signing-key";
});
// Use default authentication (JWT).

View file

@ -19,8 +19,8 @@ builder.Services.AddElsa(elsa =>
// Configure identity so that we can create a default admin user.
elsa.UseIdentity(identity =>
{
identity.CreateDefaultUser = builder.Environment.IsDevelopment();
identity.IdentityOptions.SigningKey = "secret-token-signing-key";
identity.IdentityOptions.CreateDefaultAdmin = builder.Environment.IsDevelopment();
identity.TokenOptions.SigningKey = "secret-token-signing-key";
});
// Use default authentication (JWT).

View file

@ -1,21 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\bundles\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.EntityFrameworkCore.Sqlite\Elsa.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Http\Elsa.Http.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Identity\Elsa.Identity.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.JavaScript\Elsa.JavaScript.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Liquid\Elsa.Liquid.csproj" />
<ProjectReference Include="..\..\..\src\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />
</ItemGroup>
</Project>

View file

@ -1,71 +0,0 @@
using Elsa.EntityFrameworkCore.Extensions;
using Elsa.EntityFrameworkCore.Modules.ActivityDefinitions;
using Elsa.EntityFrameworkCore.Modules.Labels;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Extensions;
using Elsa.Identity;
using Elsa.Identity.Options;
using Elsa.Requirements;
using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
var sqliteConnectionString = configuration.GetConnectionString("Sqlite")!;
var identityOptions = new IdentityOptions();
var identitySection = configuration.GetSection("Identity");
identitySection.Bind(identityOptions);
// Add Elsa services.
services
.AddElsa(elsa => elsa
.UseWorkflows()
.UseWorkflowsApi()
.UseIdentity(identity =>
{
identity.CreateDefaultUser = true;
identity.IdentityOptions = identityOptions;
})
.UseDefaultAuthentication()
.UseWorkflowRuntime(runtime => { runtime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)); })
.UseLabels(labels => labels.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
.UseActivityDefinitions(feature => feature.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
.UseJavaScript()
.UseLiquid()
.UseHttp()
);
services.AddHealthChecks();
services.AddHttpContextAccessor();
services.AddSingleton<IAuthorizationHandler, LocalHostRequirementHandler>();
services.AddAuthorization(options => options.AddPolicy(IdentityPolicyNames.SecurityRoot, policy => policy.AddRequirements(new LocalHostRequirement())));
// Razor Pages.
services.AddRazorPages();
// Configure middleware pipeline.
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseWorkflowsApi();
app.UseWorkflows();
app.UseEndpoints(endpoints =>
{
endpoints.MapFallbackToPage("/Index");
});
app.MapRazorPages();
app.Run();

View file

@ -1,37 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:20241",
"sslPort": 44391
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5118",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7113;http://localhost:5118",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View file

@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View file

@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}