Update models and UI to support configurable activity output storage

This commit is contained in:
Sipke Schoorstra 2021-06-13 16:24:43 +02:00
parent 332741d54f
commit 1db05d3b0b
32 changed files with 319 additions and 173 deletions

View file

@ -102,6 +102,8 @@ namespace Elsa.Activities.Http
)]
public ICollection<int>? SupportedStatusCodes { get; set; } = new HashSet<int>(new[] { 200 });
[ActivityOutput] public HttpResponseModel Response { get; set; }
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
var request = CreateRequest();
@ -126,12 +128,14 @@ namespace Elsa.Activities.Http
var statusCode = (int) response.StatusCode;
var statusOutcome = statusCode.ToString();
var isSupportedStatusCode = SupportedStatusCodes?.Contains(statusCode) == true;
var supportedStatusCodes = SupportedStatusCodes;
var isSupportedStatusCode = supportedStatusCodes == null || !supportedStatusCodes.Any() || SupportedStatusCodes?.Contains(statusCode) == true;
var outcomes = new List<string> { OutcomeNames.Done, statusOutcome };
if (!isSupportedStatusCode)
outcomes.Add("Unsupported Status Code");
Response = responseModel;
return Combine(Output(responseModel), Outcomes(outcomes));
}

View file

@ -16,6 +16,7 @@ namespace Elsa.Client.Models
[DataMember(Order = 8)] public bool PersistWorkflow { get; set; }
[DataMember(Order = 9)] public bool LoadWorkflowContext { get; set; }
[DataMember(Order = 10)] public bool SaveWorkflowContext { get; set; }
[DataMember(Order = 11)] public ICollection<ActivityDefinitionProperty> Properties { get; set; } = new List<ActivityDefinitionProperty>();
[DataMember(Order = 11)] public string? OutputStorageProviderName { get; set; }
[DataMember(Order = 12)] public ICollection<ActivityDefinitionProperty> Properties { get; set; } = new List<ActivityDefinitionProperty>();
}
}

View file

@ -33,18 +33,17 @@ namespace Elsa.Client.Models
[DataMember(Order = 14)] public Variables Variables { get; set; }
[DataMember(Order = 15)] public object? Output { get; set; }
[DataMember(Order = 16)] public IDictionary<string, JObject> ActivityData { get; set; } = new Dictionary<string, JObject>();
[DataMember(Order = 17)] public IDictionary<string, object> ActivityOutput { get; set; } = new Dictionary<string, object>();
[DataMember(Order = 18)]
[DataMember(Order = 17)]
public HashSet<BlockingActivity> BlockingActivities
{
get => _blockingActivities;
set => _blockingActivities = new HashSet<BlockingActivity>(value, BlockingActivityEqualityComparer.Instance);
}
[DataMember(Order = 19)] public WorkflowFault? Fault { get; set; }
[DataMember(Order = 20)] public SimpleStack<ScheduledActivity> ScheduledActivities { get; set; }
[DataMember(Order = 21)] public SimpleStack<ActivityScope> Scopes { get; set; }
[DataMember(Order = 22)] public ScheduledActivity? CurrentActivity { get; set; }
[DataMember(Order = 18)] public WorkflowFault? Fault { get; set; }
[DataMember(Order = 19)] public SimpleStack<ScheduledActivity> ScheduledActivities { get; set; }
[DataMember(Order = 20)] public SimpleStack<ActivityScope> Scopes { get; set; }
[DataMember(Order = 21)] public ScheduledActivity? CurrentActivity { get; set; }
}
}

View file

@ -21,8 +21,9 @@ namespace Elsa.ActivityResults
{
var workflowStorageService = activityExecutionContext.GetService<IWorkflowStorageService>();
var workflowStorageContext = new WorkflowStorageContext(activityExecutionContext.WorkflowInstance, activityExecutionContext.ActivityId);
await workflowStorageService.SaveAsync(StorageProviderName, workflowStorageContext, ActivityOutput.PropertyName, Output, cancellationToken);
activityExecutionContext.WorkflowInstance.Output = new WorkflowOutputReference(StorageProviderName, workflowStorageContext.ActivityId);
var storageProviderName = StorageProviderName ?? activityExecutionContext.ActivityBlueprint.OutputStorageProviderName;
await workflowStorageService.SaveAsync(storageProviderName, workflowStorageContext, ActivityOutput.PropertyName, Output, cancellationToken);
activityExecutionContext.WorkflowInstance.Output = new WorkflowOutputReference(storageProviderName, workflowStorageContext.ActivityId);
}
}
}

View file

@ -12,6 +12,8 @@ namespace Elsa.Models
public bool PersistWorkflow { get; set; }
public bool LoadWorkflowContext { get; set; }
public bool SaveWorkflowContext { get; set; }
public string? OutputStorageProviderName { get; set; }
public ICollection<ActivityDefinitionProperty> Properties { get; set; } = new List<ActivityDefinitionProperty>();
public IDictionary<string, string> PropertyStorageProviders { get; } = new Dictionary<string, string>();
}
}

View file

@ -33,7 +33,6 @@ namespace Elsa.Models
public Variables Variables { get; set; }
public WorkflowOutputReference? Output { get; set; }
public IDictionary<string, IDictionary<string, object>> ActivityData { get; set; } = new Dictionary<string, IDictionary<string, object>>();
public IDictionary<string, object> ActivityOutput { get; set; } = new Dictionary<string, object>();
public HashSet<BlockingActivity> BlockingActivities
{

View file

@ -45,8 +45,8 @@ namespace Elsa.Services.Models
public bool PersistWorkflow { get; set; }
public bool LoadWorkflowContext { get; set; }
public bool SaveWorkflowContext { get; set; }
public IDictionary<string, string> PropertyStorageProviders { get; } = new Dictionary<string, string>();
public string? OutputStorageProviderName { get; }
public IDictionary<string, string> PropertyStorageProviders { get; set; } = new Dictionary<string, string>();
public string? OutputStorageProviderName { get; set; }
public string? Source { get; set; }

View file

@ -1,4 +1,5 @@
using System.Threading;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Providers.WorkflowStorage;
@ -7,6 +8,7 @@ namespace Elsa.Services.WorkflowStorage
public interface IWorkflowStorageService
{
IWorkflowStorageProvider GetProviderByNameOrDefault(string? providerName = null);
IEnumerable<IWorkflowStorageProvider> ListProviders();
ValueTask SaveAsync(string? providerName, WorkflowStorageContext context, string key, object? value, CancellationToken cancellationToken = default);
ValueTask<object?> LoadAsync(string? providerName, WorkflowStorageContext context, string key, CancellationToken cancellationToken = default);
ValueTask DeleteAsync(string? providerName, WorkflowStorageContext context, string key, CancellationToken cancellationToken = default);

View file

@ -20,6 +20,7 @@ namespace Elsa.Services.WorkflowStorage
}
public IWorkflowStorageProvider GetProviderByNameOrDefault(string? providerName = default) => providerName != null ? _providersLookup.GetItem(providerName) ?? _defaultStorageProvider : _defaultStorageProvider;
public IEnumerable<IWorkflowStorageProvider> ListProviders() => _providersLookup.Values;
public async ValueTask SaveAsync(string? providerName, WorkflowStorageContext context, string key, object? value, CancellationToken cancellationToken = default)
{

View file

@ -137,7 +137,9 @@ namespace Elsa.Services.Workflows
PersistWorkflow = activityDefinition.PersistWorkflow,
LoadWorkflowContext = activityDefinition.LoadWorkflowContext,
SaveWorkflowContext = activityDefinition.SaveWorkflowContext,
ActivityPropertyProviders = await CreatePropertyProviders(compositeActivityDefinition, cancellationToken)
ActivityPropertyProviders = await CreatePropertyProviders(compositeActivityDefinition, cancellationToken),
OutputStorageProviderName = activityDefinition.OutputStorageProviderName,
PropertyStorageProviders = activityDefinition.PropertyStorageProviders
};
list.Add(compositeActivityBlueprint);
@ -158,6 +160,8 @@ namespace Elsa.Services.Workflows
PersistWorkflowEnabled = activityDefinition.PersistWorkflow,
LoadWorkflowContextEnabled = activityDefinition.LoadWorkflowContext,
SaveWorkflowContextEnabled = activityDefinition.SaveWorkflowContext,
OutputStorageProviderName = activityDefinition.OutputStorageProviderName,
PropertyStorageProviders = activityDefinition.PropertyStorageProviders
};
compositeActivity.Build(compositeActivityBuilder);
@ -183,6 +187,8 @@ namespace Elsa.Services.Workflows
PersistWorkflow = activityDefinition.PersistWorkflow,
LoadWorkflowContext = activityDefinition.LoadWorkflowContext,
SaveWorkflowContext = activityDefinition.SaveWorkflowContext,
OutputStorageProviderName = activityDefinition.OutputStorageProviderName,
PropertyStorageProviders = activityDefinition.PropertyStorageProviders
});
}

View file

@ -8,7 +8,7 @@ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { ActivityDefinitionProperty, ActivityDescriptor, ActivityModel, ActivityPropertyDescriptor, SelectListItem, VersionOptions, WorkflowBlueprint, WorkflowDefinition, WorkflowExecutionLogRecord, WorkflowModel } from "./models";
import { LocationSegments, MatchResults, RouterHistory } from "@stencil/router";
import { MenuItem } from "./components/controls/elsa-context-menu/models";
import { ActivityContextMenuState, WorkflowDesignerMode } from "./components/designers/tree/elsa-designer-tree/models";
import { ActivityContextMenuState, LayoutDirection, WorkflowDesignerMode } from "./components/designers/tree/elsa-designer-tree/models";
import { DropdownButtonItem, DropdownButtonOrigin } from "./components/controls/elsa-dropdown-button/models";
import { MonacoValueChangedArgs } from "./components/controls/elsa-monaco/elsa-monaco";
import { Map } from "./utils/utils";
@ -39,6 +39,7 @@ export namespace Components {
"activityBorderColor"?: (activity: ActivityModel) => string;
"activityContextMenu"?: ActivityContextMenuState;
"activityContextMenuButton"?: (activity: ActivityModel) => string;
"layoutDirection": LayoutDirection;
"mode": WorkflowDesignerMode;
"model": WorkflowModel;
"removeActivity": (activity: ActivityModel) => Promise<void>;
@ -559,6 +560,7 @@ declare namespace LocalJSX {
"activityBorderColor"?: (activity: ActivityModel) => string;
"activityContextMenu"?: ActivityContextMenuState;
"activityContextMenuButton"?: (activity: ActivityModel) => string;
"layoutDirection"?: LayoutDirection;
"mode"?: WorkflowDesignerMode;
"model"?: WorkflowModel;
"onActivityContextMenuButtonClicked"?: (event: CustomEvent<ActivityContextMenuState>) => void;

View file

@ -7,7 +7,7 @@ import * as d3 from 'd3';
import dagreD3 from 'dagre-d3';
import state from '../../../../utils/store';
import {ActivityIcon} from '../../../icons/activity-icon';
import {ActivityContextMenuState, WorkflowDesignerMode} from "./models";
import {ActivityContextMenuState, LayoutDirection, WorkflowDesignerMode} from "./models";
@Component({
tag: 'elsa-designer-tree',
@ -22,6 +22,7 @@ export class ElsaWorkflowDesigner {
@Prop() activityBorderColor?: (activity: ActivityModel) => string;
@Prop() activityContextMenu?: ActivityContextMenuState;
@Prop() mode: WorkflowDesignerMode = WorkflowDesignerMode.Edit;
@Prop() layoutDirection: LayoutDirection = LayoutDirection.Vertical;
@Event({eventName: 'workflow-changed', bubbles: true, composed: true, cancelable: true}) workflowChanged: EventEmitter<WorkflowModel>;
@Event() activitySelected: EventEmitter<ActivityModel>;
@Event() activityDeselected: EventEmitter<ActivityModel>;
@ -164,6 +165,7 @@ export class ElsaWorkflowDesigner {
outcomes: activityDescriptor.outcomes,
displayName: activityDescriptor.displayName,
properties: [],
propertyStorageProviders: {}
};
for (const property of activityDescriptor.inputProperties) {
@ -316,6 +318,9 @@ export class ElsaWorkflowDesigner {
setEntities() {
this.graph = new dagreD3.graphlib.Graph().setGraph({});
const layoutDirection = this.layoutDirection;
this.graph.graph().rankdir = layoutDirection == LayoutDirection.Vertical ? 'TB' : 'LR';
const rootActivities = this.getRootActivities();
// Start node.

View file

@ -11,4 +11,9 @@ export interface ActivityContextMenuState {
x: number;
y: number;
activity?: ActivityModel | null;
}
export enum LayoutDirection {
Horizontal,
Vertical
}

View file

@ -51,11 +51,11 @@ export class ElsaWorkflowBlueprintViewerScreen {
isPublished: false,
loadWorkflowContext: false,
isSingleton: false,
persistOutput: false,
saveWorkflowContext: false,
variables: {data: {}},
type: null,
properties: {data: {}}
properties: {data: {}},
propertyStorageProviders: {}
};
const client = createElsaClient(this.serverUrl);
@ -126,10 +126,11 @@ export class ElsaWorkflowBlueprintViewerScreen {
type: source.type,
properties: properties,
outcomes: [...activityDescriptor.outcomes],
persistOutput: source.persistOutput,
outputStorageProviderName: source.outputStorageProviderName,
persistWorkflow: source.persistWorkflow,
saveWorkflowContext: source.saveWorkflowContext,
loadWorkflowContext: source.loadWorkflowContext
loadWorkflowContext: source.loadWorkflowContext,
propertyStorageProviders: source.propertyStorageProviders
}
}

View file

@ -1,9 +1,9 @@
import {Component, Host, h, State, Event} from '@stencil/core';
import {Component, Event, h, Host, State} from '@stencil/core';
import {eventBus} from '../../../../services/event-bus';
import state from '../../../../utils/store';
import {ActivityDescriptor, ActivityModel, ActivityPropertyDescriptor, EventTypes} from "../../../../models";
import {ActivityDescriptor, ActivityModel, ActivityPropertyDescriptor, EventTypes, WorkflowStorageDescriptor} from "../../../../models";
import {propertyDisplayManager} from '../../../../services/property-display-manager';
import {checkBox, FormContext, textArea, textInput} from "../../../../utils/forms";
import {checkBox, FormContext, section, selectField, SelectOption, textArea, textInput} from "../../../../utils/forms";
@Component({
tag: 'elsa-activity-editor-modal',
@ -11,6 +11,7 @@ import {checkBox, FormContext, textArea, textInput} from "../../../../utils/form
})
export class ElsaActivityEditorModal {
@State() workflowStorageDescriptors: Array<WorkflowStorageDescriptor> = [];
@State() activityModel: ActivityModel;
@State() activityDescriptor: ActivityDescriptor;
@State() selectedTab: string = 'Properties';
@ -18,7 +19,7 @@ export class ElsaActivityEditorModal {
form: HTMLFormElement;
formContext: FormContext;
renderProps: any;
// Force a new key every time we show the editor to make sure Stencil creates new components.
// This prevents the issue where the designer has e.g. one activity where the user edits the properties, cancels out, then opens the editor again, seeing the entered value still there.
timestamp: Date = new Date();
@ -27,8 +28,9 @@ export class ElsaActivityEditorModal {
eventBus.on(EventTypes.ShowActivityEditor, async (activity: ActivityModel, animate: boolean) => {
this.activityModel = JSON.parse(JSON.stringify(activity));
this.activityDescriptor = state.activityDescriptors.find(x => x.type == activity.type);
this.workflowStorageDescriptors = state.workflowStorageDescriptors;
this.formContext = new FormContext(this.activityModel, newValue => this.activityModel = newValue);
this.selectedTab = 'Properties';
this.selectedTab = 'Properties';
this.timestamp = new Date();
await this.dialog.show(animate);
});
@ -37,9 +39,9 @@ export class ElsaActivityEditorModal {
updateActivity(formData: FormData) {
const activity = this.activityModel;
const activityDescriptor = this.activityDescriptor;
const properties: Array<ActivityPropertyDescriptor> = activityDescriptor.inputProperties;
const inputProperties: Array<ActivityPropertyDescriptor> = activityDescriptor.inputProperties;
for (const property of properties)
for (const property of inputProperties)
propertyDisplayManager.update(activity, property, formData);
}
@ -61,25 +63,26 @@ export class ElsaActivityEditorModal {
this.selectedTab = tab;
}
componentWillRender(){
const activityDescriptor: ActivityDescriptor = this.activityDescriptor || {displayName: '', type: '', outcomes: [], category: '', traits: 0, browsable: false, inputProperties: [], description: ''};
componentWillRender() {
const activityDescriptor: ActivityDescriptor = this.activityDescriptor || {displayName: '', type: '', outcomes: [], category: '', traits: 0, browsable: false, inputProperties: [], outputProperties: [], description: ''};
const propertyCategories = activityDescriptor.inputProperties.filter(x => x.category).map(x => x.category).distinct();
const defaultProperties = activityDescriptor.inputProperties.filter(x => !x.category || x.category.length == 0);
let tabs: Array<string> = [];
if(defaultProperties.length > 0) {
if (defaultProperties.length > 0) {
tabs.push('Properties');
}
tabs.push('Common');
tabs.push('Behaviors');
tabs = [...tabs, ...propertyCategories];
tabs.push('Common');
tabs.push('Storage');
let selectedTab = this.selectedTab;
if(tabs.findIndex(x => x === selectedTab) < 0)
if (tabs.findIndex(x => x === selectedTab) < 0)
selectedTab = tabs[0];
const activityModel: ActivityModel = this.activityModel || {type: '', activityId: '', outcomes: [], properties: []};
const activityModel: ActivityModel = this.activityModel || {type: '', activityId: '', outcomes: [], properties: [], propertyStorageProviders: {}};
this.renderProps = {
activityDescriptor,
@ -103,7 +106,7 @@ export class ElsaActivityEditorModal {
return (
<Host class="elsa-block">
<elsa-modal-dialog ref={el => this.dialog = el} >
<elsa-modal-dialog ref={el => this.dialog = el}>
<div slot="content" class="elsa-py-8 elsa-pb-0">
<form onSubmit={e => this.onSubmit(e)} ref={el => this.form = el} key={this.timestamp.getTime().toString()}>
<div class="elsa-flex elsa-px-8">
@ -159,23 +162,43 @@ export class ElsaActivityEditorModal {
renderSelectedTab(activityModel: ActivityModel, activityDescriptor: ActivityDescriptor, categories: Array<string>) {
return [
this.renderWorkflowContextTab(activityModel),
this.renderStorageTab(activityModel, activityDescriptor),
this.renderCommonTab(activityModel),
this.renderPropertiesTab(activityModel, activityDescriptor),
this.renderCategoryTabs(activityModel, activityDescriptor, categories)
this.renderCategoryTabs(activityModel, activityDescriptor, categories),
];
}
renderWorkflowContextTab(activityModel: ActivityModel) {
renderStorageTab(activityModel: ActivityModel, activityDescriptor: ActivityDescriptor) {
const formContext = this.formContext;
let storageDescriptorOptions: Array<SelectOption> = this.workflowStorageDescriptors.map(x => ({value: x.name, text: x.displayName}));
let outputProperties = activityDescriptor.outputProperties;
storageDescriptorOptions = [{value: null, text: 'Default'}, ...storageDescriptorOptions];
const renderOutputProperty = function (propertyDescriptor: ActivityPropertyDescriptor) {
const propertyName = propertyDescriptor.name;
const fieldName = `propertyStorageProviders.${propertyName}`;
return selectField(formContext, fieldName, propertyName, activityModel.propertyStorageProviders[propertyName], storageDescriptorOptions, 'Select a storage provider.', fieldName);
}
return (
<div class={`flex ${this.getHiddenClass('Behaviors')}`}>
<div class={`flex ${this.getHiddenClass('Storage')}`}>
<div class="elsa-space-y-8 elsa-w-full">
{section('Workflow Context')}
{checkBox(formContext, 'loadWorkflowContext', 'Load Workflow Context', activityModel.loadWorkflowContext, 'When enabled, this will load the workflow context into memory before executing this activity.', 'loadWorkflowContext')}
{checkBox(formContext, 'saveWorkflowContext', 'Save Workflow Context', activityModel.saveWorkflowContext, 'When enabled, this will save the workflow context back into storage after executing this activity.', 'saveWorkflowContext')}
{section('Workflow Instance')}
{checkBox(formContext, 'persistWorkflow', 'Save Workflow Instance', activityModel.persistWorkflow, 'When enabled, this will save the workflow instance back into storage right after executing this activity.', 'persistWorkflow')}
{checkBox(formContext, 'persistOutput', 'Save Activity Output', activityModel.persistOutput, 'When enabled, this will store this activity\'s output as part of the workflow instance. Enable this when you plan to reference this output from other activities', 'persistOutput')}
{section('Activity Output', 'Configure what storage to use when persisting activity output.')}
{selectField(formContext, 'outputStorageProviderName', 'Output Storage', activityModel.outputStorageProviderName, storageDescriptorOptions, 'Select a storage provider for this activity\'s output.', 'outputStorageProviderName')}
{Object.keys(outputProperties).length > 0 ? (
[section('Activity Output Properties', 'Configure the desired storage for each output property of this activity.'), outputProperties.map(renderOutputProperty)]
) : undefined}
</div>
</div>
);
@ -198,7 +221,7 @@ export class ElsaActivityEditorModal {
renderPropertiesTab(activityModel: ActivityModel, activityDescriptor: ActivityDescriptor) {
const propertyDescriptors: Array<ActivityPropertyDescriptor> = this.renderProps.defaultProperties;
if(propertyDescriptors.length == 0)
if (propertyDescriptors.length == 0)
return undefined;
const key = `activity-settings:${activityModel.activityId}`;

View file

@ -1,9 +1,8 @@
import {Component, Event, h, Host, State} from '@stencil/core';
import '../../../utils/utils';
import {eventBus} from '../../../services/event-bus';
import {ActivityDescriptor, ActivityDescriptorDisplayContext, ActivityTraits, EventTypes} from "../../../models";
import state from '../../../utils/store';
import {ActivityIcon} from "../../icons/activity-icon";
import {eventBus} from '../../../../services/event-bus';
import {ActivityDescriptor, ActivityDescriptorDisplayContext, ActivityTraits, EventTypes} from "../../../../models";
import state from '../../../../utils/store';
import {ActivityIcon} from "../../../icons/activity-icon";
@Component({
tag: 'elsa-activity-picker-modal',

View file

@ -109,8 +109,10 @@ export class ElsaWorkflowDefinitionEditorScreen {
@Watch("serverUrl")
async serverUrlChangedHandler(newValue: string) {
if (newValue && newValue.length > 0)
if (newValue && newValue.length > 0) {
await this.loadActivityDescriptors();
await this.loadWorkflowStorageDescriptors();
}
}
@Watch("monacoLibPath")
@ -150,6 +152,11 @@ export class ElsaWorkflowDefinitionEditorScreen {
state.activityDescriptors = await client.activitiesApi.list();
}
async loadWorkflowStorageDescriptors() {
const client = createElsaClient(this.serverUrl);
state.workflowStorageDescriptors = await client.workflowStorageProvidersApi.list();
}
updateWorkflowDefinition(value: WorkflowDefinition) {
this.workflowDefinition = value;
this.workflowModel = this.mapWorkflowModel(value);
@ -203,8 +210,9 @@ export class ElsaWorkflowDefinitionEditorScreen {
persistWorkflow: x.persistWorkflow,
loadWorkflowContext: x.loadWorkflowContext,
saveWorkflowContext: x.saveWorkflowContext,
persistOutput: x.persistOutput,
properties: x.properties
outputStorageProviderName: x.outputStorageProviderName,
properties: x.properties,
propertyStorageProviders: x.propertyStorageProviders
})),
connections: workflowModel.connections.map<ConnectionDefinition>(x => ({
sourceActivityId: x.sourceId,
@ -275,10 +283,11 @@ export class ElsaWorkflowDefinitionEditorScreen {
type: source.type,
properties: source.properties,
outcomes: [...activityDescriptor.outcomes],
persistOutput: source.persistOutput,
outputStorageProviderName: source.outputStorageProviderName,
persistWorkflow: source.persistWorkflow,
saveWorkflowContext: source.saveWorkflowContext,
loadWorkflowContext: source.loadWorkflowContext
loadWorkflowContext: source.loadWorkflowContext,
propertyStorageProviders: source.propertyStorageProviders
}
}

View file

@ -86,11 +86,11 @@ export class ElsaWorkflowInstanceViewerScreen {
isPublished: false,
loadWorkflowContext: false,
isSingleton: false,
persistOutput: false,
saveWorkflowContext: false,
variables: {data: {}},
type: null,
properties: {data: {}}
properties: {data: {}},
propertyStorageProviders: {}
};
const client = createElsaClient(this.serverUrl);
@ -163,10 +163,11 @@ export class ElsaWorkflowInstanceViewerScreen {
type: source.type,
properties: properties,
outcomes: [...activityDescriptor.outcomes],
persistOutput: source.persistOutput,
outputStorageProviderName: source.outputStorageProviderName,
persistWorkflow: source.persistWorkflow,
saveWorkflowContext: source.saveWorkflowContext,
loadWorkflowContext: source.loadWorkflowContext
loadWorkflowContext: source.loadWorkflowContext,
propertyStorageProviders: source.propertyStorageProviders
}
}

View file

@ -46,9 +46,10 @@ export interface ActivityBlueprint {
persistWorkflow: boolean;
loadWorkflowContext: boolean;
saveWorkflowContext: boolean;
persistOutput: boolean;
outputStorageProviderName?: string;
source?: string;
properties: Variables;
propertyStorageProviders: Map<string>;
}
export interface Connection {
@ -139,8 +140,9 @@ export interface ActivityDefinition {
persistWorkflow: boolean;
loadWorkflowContext: boolean;
saveWorkflowContext: boolean;
persistOutput: boolean;
outputStorageProviderName: string;
properties: Array<ActivityDefinitionProperty>;
propertyStorageProviders: Map<string>;
}
export interface WorkflowExecutionLogRecord {
@ -260,6 +262,7 @@ export interface ActivityDescriptor {
outcomes: Array<string>;
browsable: boolean;
inputProperties: Array<ActivityPropertyDescriptor>;
outputProperties: Array<ActivityPropertyDescriptor>;
}
export interface ActivityPropertyDescriptor {
@ -274,6 +277,11 @@ export interface ActivityPropertyDescriptor {
supportedSyntaxes: Array<string>
}
export interface WorkflowStorageDescriptor {
name: string;
displayName?: string;
}
export interface PagedList<T> {
items: Array<T>;
page?: number;

View file

@ -1,4 +1,5 @@
import {ActivityDefinitionProperty, WorkflowPersistenceBehavior} from "./domain";
import {Map} from '../utils/utils';
export interface WorkflowModel {
activities: Array<ActivityModel>;
@ -15,9 +16,10 @@ export interface ActivityModel {
outcomes: Array<string>;
properties: Array<ActivityDefinitionProperty>;
persistWorkflow?: boolean;
persistOutput?: boolean;
outputStorageProviderName?: string;
loadWorkflowContext?: boolean;
saveWorkflowContext?: boolean;
propertyStorageProviders: Map<string>;
}
export interface ConnectionModel {

View file

@ -11,7 +11,7 @@ import {
WorkflowContextOptions,
WorkflowDefinition,
WorkflowDefinitionSummary, WorkflowExecutionLogRecord, WorkflowFault, WorkflowInstance, WorkflowInstanceSummary,
WorkflowPersistenceBehavior, WorkflowStatus
WorkflowPersistenceBehavior, WorkflowStatus, WorkflowStorageDescriptor
} from "../models";
export const createElsaClient = function (serverUrl: string): ElsaClient {
@ -166,123 +166,134 @@ export const createElsaClient = function (serverUrl: string): ElsaClient {
const response = await httpClient.get(`v1/workflow-instances/${workflowInstanceId}/activity-stats/${activityId}`);
return response.data;
}
},
workflowStorageProvidersApi: {
list: async () => {
const response = await httpClient.get<Array<WorkflowStorageDescriptor>>('v1/workflow-storage-providers');
return response.data;
}
}
}
}
export interface ElsaClient {
activitiesApi: ActivitiesApi;
workflowDefinitionsApi: WorkflowDefinitionsApi;
workflowRegistryApi: WorkflowRegistryApi;
workflowInstancesApi: WorkflowInstancesApi;
workflowExecutionLogApi: WorkflowExecutionLogApi;
scriptingApi: ScriptingApi;
designerApi: DesignerApi;
activityStatsApi: ActivityStatsApi;
}
export interface ElsaClient {
activitiesApi: ActivitiesApi;
workflowDefinitionsApi: WorkflowDefinitionsApi;
workflowRegistryApi: WorkflowRegistryApi;
workflowInstancesApi: WorkflowInstancesApi;
workflowExecutionLogApi: WorkflowExecutionLogApi;
scriptingApi: ScriptingApi;
designerApi: DesignerApi;
activityStatsApi: ActivityStatsApi;
workflowStorageProvidersApi: WorkflowStorageProvidersApi;
}
export interface ActivitiesApi {
list(): Promise<Array<ActivityDescriptor>>;
}
export interface ActivitiesApi {
list(): Promise<Array<ActivityDescriptor>>;
}
export interface WorkflowDefinitionsApi {
export interface WorkflowDefinitionsApi {
list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise<PagedList<WorkflowDefinitionSummary>>;
list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise<PagedList<WorkflowDefinitionSummary>>;
getByDefinitionAndVersion(definitionId: string, versionOptions: VersionOptions): Promise<WorkflowDefinition>;
getByDefinitionAndVersion(definitionId: string, versionOptions: VersionOptions): Promise<WorkflowDefinition>;
save(request: SaveWorkflowDefinitionRequest): Promise<WorkflowDefinition>;
save(request: SaveWorkflowDefinitionRequest): Promise<WorkflowDefinition>;
delete(definitionId: string): Promise<void>;
delete(definitionId: string): Promise<void>;
retract(workflowDefinitionId: string): Promise<WorkflowDefinition>;
retract(workflowDefinitionId: string): Promise<WorkflowDefinition>;
export(workflowDefinitionId: string, versionOptions: VersionOptions): Promise<ExportWorkflowResponse>;
export(workflowDefinitionId: string, versionOptions: VersionOptions): Promise<ExportWorkflowResponse>;
import(workflowDefinitionId: string, file: File): Promise<WorkflowDefinition>;
}
import(workflowDefinitionId: string, file: File): Promise<WorkflowDefinition>;
}
export interface WorkflowRegistryApi {
list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise<PagedList<WorkflowBlueprintSummary>>;
export interface WorkflowRegistryApi {
list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise<PagedList<WorkflowBlueprintSummary>>;
get(id: string, versionOptions: VersionOptions): Promise<WorkflowBlueprint>;
}
get(id: string, versionOptions: VersionOptions): Promise<WorkflowBlueprint>;
}
export interface WorkflowInstancesApi {
list(page?: number, pageSize?: number, workflowDefinitionId?: string, workflowStatus?: WorkflowStatus, orderBy?: OrderBy, searchTerm?: string): Promise<PagedList<WorkflowInstanceSummary>>;
export interface WorkflowInstancesApi {
list(page?: number, pageSize?: number, workflowDefinitionId?: string, workflowStatus?: WorkflowStatus, orderBy?: OrderBy, searchTerm?: string): Promise<PagedList<WorkflowInstanceSummary>>;
get(id: string): Promise<WorkflowInstance>;
get(id: string): Promise<WorkflowInstance>;
delete(id: string): Promise<void>;
delete(id: string): Promise<void>;
bulkDelete(request: BulkDeleteWorkflowsRequest): Promise<BulkDeleteWorkflowsResponse>;
}
bulkDelete(request: BulkDeleteWorkflowsRequest): Promise<BulkDeleteWorkflowsResponse>;
}
export interface WorkflowExecutionLogApi {
export interface WorkflowExecutionLogApi {
get(workflowInstanceId: string, page?: number, pageSize?: number): Promise<PagedList<WorkflowExecutionLogRecord>>;
get(workflowInstanceId: string, page?: number, pageSize?: number): Promise<PagedList<WorkflowExecutionLogRecord>>;
}
}
export interface BulkDeleteWorkflowsRequest {
workflowInstanceIds: Array<string>;
}
export interface BulkDeleteWorkflowsRequest {
workflowInstanceIds: Array<string>;
}
export interface BulkDeleteWorkflowsResponse {
deletedWorkflowCount: number;
}
export interface BulkDeleteWorkflowsResponse {
deletedWorkflowCount: number;
}
export interface ScriptingApi {
getJavaScriptTypeDefinitions(workflowDefinitionId: string, context?: string): Promise<string>
}
export interface ScriptingApi {
getJavaScriptTypeDefinitions(workflowDefinitionId: string, context?: string): Promise<string>
}
export interface DesignerApi {
runtimeSelectItemsApi: RuntimeSelectItemsApi;
}
export interface DesignerApi {
runtimeSelectItemsApi: RuntimeSelectItemsApi;
}
export interface RuntimeSelectItemsApi {
get(providerTypeName: string, context?: any): Promise<Array<SelectListItem>>
}
export interface RuntimeSelectItemsApi {
get(providerTypeName: string, context?: any): Promise<Array<SelectListItem>>
}
export interface ActivityStatsApi {
get(workflowInstanceId: string, activityId: string): Promise<ActivityStats>;
}
export interface ActivityStatsApi {
get(workflowInstanceId: string, activityId: string): Promise<ActivityStats>;
}
export interface SaveWorkflowDefinitionRequest {
workflowDefinitionId?: string;
name?: string;
displayName?: string;
description?: string;
tag?: string;
variables?: Variables;
contextOptions?: WorkflowContextOptions;
isSingleton?: boolean;
persistenceBehavior?: WorkflowPersistenceBehavior;
deleteCompletedInstances?: boolean;
publish?: boolean;
activities: Array<ActivityDefinition>;
connections: Array<ConnectionDefinition>;
}
export interface WorkflowStorageProvidersApi {
list(): Promise<Array<WorkflowStorageDescriptor>>;
}
export interface ExportWorkflowResponse {
fileName: string;
data: Blob;
}
export interface SaveWorkflowDefinitionRequest {
workflowDefinitionId?: string;
name?: string;
displayName?: string;
description?: string;
tag?: string;
variables?: Variables;
contextOptions?: WorkflowContextOptions;
isSingleton?: boolean;
persistenceBehavior?: WorkflowPersistenceBehavior;
deleteCompletedInstances?: boolean;
publish?: boolean;
activities: Array<ActivityDefinition>;
connections: Array<ConnectionDefinition>;
}
export interface ActivityStats {
fault?: ActivityFault;
averageExecutionTime: string;
fastestExecutionTime: string;
slowestExecutionTime: string;
lastExecutedAt: Date;
eventCounts: Array<ActivityEventCount>;
}
export interface ExportWorkflowResponse {
fileName: string;
data: Blob;
}
interface ActivityEventCount {
eventName: string;
count: number;
}
export interface ActivityStats {
fault?: ActivityFault;
averageExecutionTime: string;
fastestExecutionTime: string;
slowestExecutionTime: string;
lastExecutedAt: Date;
eventCounts: Array<ActivityEventCount>;
}
interface ActivityFault{
message: string;
}
interface ActivityEventCount {
eventName: string;
count: number;
}
interface ActivityFault {
message: string;
}

View file

@ -25,7 +25,8 @@ export function textInput(context: FormContext, fieldName: string, label: string
{label}
</label>
<div class="elsa-mt-1">
<input type="text" id={fieldId} name={fieldName} value={value} onChange={e => onTextInputChange(e, context)} class="focus:elsa-ring-blue-500 focus:elsa-border-blue-500 block elsa-w-full elsa-min-w-0 elsa-rounded-md sm:elsa-text-sm elsa-border-gray-300"/>
<input type="text" id={fieldId} name={fieldName} value={value} onChange={e => onTextInputChange(e, context)}
class="focus:elsa-ring-blue-500 focus:elsa-border-blue-500 block elsa-w-full elsa-min-w-0 elsa-rounded-md sm:elsa-text-sm elsa-border-gray-300"/>
</div>
{hint && hint.length > 0 ? <p class="elsa-mt-2 elsa-text-sm elsa-text-gray-500">{hint}</p> : undefined}
</div>);
@ -53,7 +54,8 @@ export function textArea(context: FormContext, fieldName: string, label: string,
{label}
</label>
<div class="elsa-mt-1">
<textarea id={fieldId} name={fieldName} value={value} onChange={e => onTextAreaChange(e, context)} rows={3} class="focus:elsa-ring-blue-500 focus:elsa-border-blue-500 block elsa-w-full elsa-min-w-0 elsa-rounded-md sm:elsa-text-sm elsa-border-gray-300"/>
<textarea id={fieldId} name={fieldName} value={value} onChange={e => onTextAreaChange(e, context)} rows={3}
class="focus:elsa-ring-blue-500 focus:elsa-border-blue-500 block elsa-w-full elsa-min-w-0 elsa-rounded-md sm:elsa-text-sm elsa-border-gray-300"/>
</div>
{hint && hint.length > 0 ? <p class="elsa-mt-2 elsa-text-sm elsa-text-gray-500">{hint}</p> : undefined}
</div>);
@ -78,6 +80,19 @@ export function selectField(context: FormContext, fieldName: string, label: stri
</div>);
}
export function section(title: string, subTitle?: string) {
return <div>
<h3 class="elsa-text-lg elsa-leading-6 elsa-font-medium elsa-text-gray-900">
{title}
</h3>
{!!subTitle ? (
<p class="elsa-mt-1 elsa-max-w-2xl elsa-text-sm elsa-text-gray-500">
{subTitle}
</p>)
: undefined}
</div>
}
export function updateField<T>(context: FormContext, fieldName: string, value: T) {
const fieldNameHierarchy = fieldName.split('.');
let clone = {...context.model};

View file

@ -2,6 +2,7 @@
const { state, onChange } = createStore({
activityDescriptors: [],
workflowStorageDescriptors: [],
monacoLibPath: ''
});

View file

@ -15,7 +15,6 @@ namespace Elsa.Persistence.EntityFramework.Core.Configuration
builder.Property(x => x.FaultedAt).HasConversion(ValueConverters.InstantConverter);
builder.Ignore(x => x.Output);
builder.Ignore(x => x.ActivityData);
builder.Ignore(x => x.ActivityOutput);
builder.Ignore(x => x.BlockingActivities);
builder.Ignore(x => x.Fault);
builder.Ignore(x => x.ScheduledActivities);

View file

@ -61,7 +61,6 @@ namespace Elsa.Persistence.EntityFramework.Core.Stores
entity.Output,
entity.Variables,
entity.ActivityData,
entity.ActivityOutput,
entity.BlockingActivities,
entity.ScheduledActivities,
entity.Scopes,
@ -81,7 +80,6 @@ namespace Elsa.Persistence.EntityFramework.Core.Stores
entity.Output,
entity.Variables,
entity.ActivityData,
entity.ActivityOutput,
entity.BlockingActivities,
entity.ScheduledActivities,
entity.Scopes,
@ -97,7 +95,6 @@ namespace Elsa.Persistence.EntityFramework.Core.Stores
entity.Output = data.Output;
entity.Variables = data.Variables;
entity.ActivityData = data.ActivityData;
entity.ActivityOutput = data.ActivityOutput;
entity.BlockingActivities = data.BlockingActivities;
entity.ScheduledActivities = data.ScheduledActivities;
entity.Scopes = data.Scopes;

View file

@ -27,7 +27,6 @@ namespace Elsa.Persistence.YesSql.Documents
public Variables Variables { get; set; } = new();
public object? Output { get; set; }
public IDictionary<string, object> ActivityData { get; set; } = new Dictionary<string, object>();
public IDictionary<string, object> ActivityOutput { get; set; } = new Dictionary<string, object>();
public HashSet<BlockingActivity> BlockingActivities
{

View file

@ -14,7 +14,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-1914" />
<PackageReference Include="Jint" Version="3.0.0-beta-2032" />
</ItemGroup>
<ItemGroup>

View file

@ -2,22 +2,19 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper.Internal;
using Elsa.Attributes;
using Elsa.Providers.WorkflowStorage;
using Elsa.Scripting.JavaScript.Events;
using Elsa.Scripting.JavaScript.Extensions;
using Elsa.Scripting.JavaScript.Messages;
using Elsa.Services;
using Elsa.Services.Models;
using Elsa.Services.WorkflowStorage;
using Jint;
using Jint.Runtime.Interop;
using MediatR;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json.Linq;
using NodaTime;
namespace Elsa.Scripting.JavaScript.Handlers
@ -52,7 +49,9 @@ namespace Elsa.Scripting.JavaScript.Handlers
engine.SetValue("getWorkflowDefinitionIdByName", (Func<string, string?>) (name => GetWorkflowDefinitionIdByName(activityExecutionContext, name)));
engine.SetValue("getWorkflowDefinitionIdByTag", (Func<string, string?>) (tag => GetWorkflowDefinitionIdByTag(activityExecutionContext, tag)));
engine.SetValue("getActivity", (Func<string, object?>) (idOrName => GetActivityModel(activityExecutionContext, idOrName)));
engine.SetValue("getInboundActivity", (Func<object?>) (() => GetInboundActivityModel(activityExecutionContext)));
// Using .Result because Jint doesn't support Task-based functions yet.
engine.SetValue("getActivityOutput", (Func<string, string?, object?>) ((activityId, propertyName) => GetActivityOutputAsync(activityId, propertyName, activityExecutionContext).Result));
// Global variables.
engine.SetValue("activityExecutionContext", activityExecutionContext);
@ -83,19 +82,20 @@ namespace Elsa.Scripting.JavaScript.Handlers
foreach (var variable in variables.Data)
engine.SetValue(variable.Key, variable.Value);
// DEPRECATED: The following only works when activity state is stored as part of the workflow instance itself.
// With the introduction of pluggable workflow state storage providers, which are asynchronous, we don't want to load all states of all activities upfront (e.g. we don't want to download files from blob storage).
// Named activities.
foreach (var activity in workflowBlueprint.Activities.Where(x => !string.IsNullOrWhiteSpace(x.Name)))
{
var state = new Dictionary<string, object>(activityExecutionContext.GetActivityData(activity.Id));
// Output.
if (workflowInstance.ActivityOutput.ContainsKey(activity.Id))
if (state.ContainsKey(ActivityOutput.PropertyName))
{
var output = workflowInstance.ActivityOutput[activity.Id];
//state["Output"] = output != null ? JObjectExtensions.SerializeState(output) : null;
state["Output"] = output;
var output = state.GetItem(ActivityOutput.PropertyName);
state["Output"] = output ?? new { };
}
engine.SetValue(activity.Name, state);
}
@ -116,7 +116,7 @@ namespace Elsa.Scripting.JavaScript.Handlers
output.AppendLine("declare function getWorkflowDefinitionIdByName(name: string): string;");
output.AppendLine("declare function getWorkflowDefinitionIdByTag(tag: string): string;");
output.AppendLine("declare function getActivity(idOrName: string): any;");
output.AppendLine("declare function getInboundActivity(): any;");
output.AppendLine("declare function getActivityOutput(activityIdOrName: string, propertyName?: string): any;");
output.AppendLine("declare const activityExecutionContext: ActivityExecutionContext;");
output.AppendLine("declare const workflowExecutionContext: WorkflowExecutionContext;");
@ -136,7 +136,6 @@ namespace Elsa.Scripting.JavaScript.Handlers
if (contextType != null)
{
var workflowContextTypeScriptType = notification.GetTypeScriptType(contextType);
output.AppendLine($"declare const workflowContext: {workflowContextTypeScriptType}");
}
@ -154,8 +153,8 @@ namespace Elsa.Scripting.JavaScript.Handlers
var activityTypeNames = namedActivities.Select(x => x.Type).Distinct().ToList();
var activityTypes = await Task.WhenAll(activityTypeNames.Select(async activityTypeName => (activityTypeName, await _activityTypeService.GetActivityTypeAsync(activityTypeName, cancellationToken))));
var activityTypeDictionary = activityTypes.ToDictionary(x => x.activityTypeName, x => x.Item2);
foreach (var activityType in activityTypeDictionary.Values)
foreach (var activityType in activityTypeDictionary.Values)
RenderActivityTypeDeclaration(activityType, output);
foreach (var activity in namedActivities)
@ -181,7 +180,7 @@ namespace Elsa.Scripting.JavaScript.Handlers
var propertyName = property.Name;
writer.AppendLine($"{propertyName}: {typeScriptType};");
}
foreach (var property in outputProperties)
{
var typeScriptType = notification.GetTypeScriptType(property.Type);
@ -215,6 +214,17 @@ namespace Elsa.Scripting.JavaScript.Handlers
var inboundActivityId = context.WorkflowExecutionContext.GetInboundActivityPath(context.ActivityId).FirstOrDefault();
return inboundActivityId == null ? null : GetActivityModel(context, inboundActivityId);
}
private async Task<object?> GetActivityOutputAsync(string activityIdOrName, string? propertyName, ActivityExecutionContext context)
{
var workflowExecutionContext = context.WorkflowExecutionContext;
var activityBlueprint = workflowExecutionContext.GetActivityBlueprintByName(activityIdOrName) ?? workflowExecutionContext.GetActivityBlueprintById(activityIdOrName)!;
var storageService = context.GetService<IWorkflowStorageService>();
var propName = propertyName ?? ActivityOutput.PropertyName;
var providerName = propertyName == null ? activityBlueprint.OutputStorageProviderName : activityBlueprint.PropertyStorageProviders.GetItem(propertyName);
var storageContext = new WorkflowStorageContext(context.WorkflowInstance, activityBlueprint.Id);
return await storageService.LoadAsync(providerName, storageContext, propName, context.CancellationToken);
}
private void RegisterType<T>(Engine engine) => engine.SetValue(typeof(T).Name, TypeReference.CreateTypeReference(engine, typeof(T)));
private void RegisterType(Type type, Engine engine) => engine.SetValue(type.Name, TypeReference.CreateTypeReference(engine, type));

View file

@ -59,8 +59,7 @@ namespace Elsa.Scripting.JavaScript.Services
private static object? ExecuteExpressionAndGetResult(Engine engine, string expression)
{
engine.Execute(expression);
return engine.GetCompletionValue().ToObject();
return engine.Evaluate(expression).ToObject();
}
}
}

View file

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
using Elsa.Server.Api.Services;
using Elsa.Services.WorkflowStorage;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
namespace Elsa.Server.Api.Endpoints.WorkflowStorageProviders
{
[ApiController]
[ApiVersion("1")]
[Route("v{version:apiVersion}/workflow-storage-providers")]
[Produces("application/json")]
public class List : Controller
{
private readonly IWorkflowStorageService _workflowStorageService;
private readonly IEndpointContentSerializerSettingsProvider _serializerSettingsProvider;
public List(IWorkflowStorageService workflowStorageService, IEndpointContentSerializerSettingsProvider serializerSettingsProvider)
{
_workflowStorageService = workflowStorageService;
_serializerSettingsProvider = serializerSettingsProvider;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(List<WorkflowStorageDescriptor>))]
[SwaggerOperation(
Summary = "Returns all available workflow storage providers.",
Description = "Returns all available workflow storage providers.",
OperationId = "WorkflowStorageProviders.List",
Tags = new[] { "WorkflowStorageProviders" })
]
public IActionResult Handle()
{
var providers = _workflowStorageService.ListProviders();
var descriptors = providers.Select(x => new WorkflowStorageDescriptor(x.Name, x.DisplayName)).ToList();
return Json(descriptors, _serializerSettingsProvider.GetSettings());
}
}
}

View file

@ -0,0 +1,4 @@
namespace Elsa.Server.Api.Endpoints.WorkflowStorageProviders
{
public record WorkflowStorageDescriptor(string Name, string? DisplayName);
}