From 1db05d3b0b4409a636f26f3ca8d16fea3d94fd85 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 13 Jun 2021 16:24:43 +0200 Subject: [PATCH] Update models and UI to support configurable activity output storage --- .../SendHttpRequest/SendHttpRequest.cs | 6 +- .../Elsa.Client/Models/ActivityDefinition.cs | 3 +- .../Elsa.Client/Models/WorkflowInstance.cs | 11 +- .../ActivityResults/OutputResult.cs | 5 +- .../Models/ActivityDefinition.cs | 2 + .../Models/WorkflowInstance.cs | 1 - .../Services/Models/ActivityBlueprint.cs | 4 +- .../IWorkflowStorageService.cs | 4 +- .../WorkflowStorage/WorkflowStorageService.cs | 1 + .../WorkflowBlueprintMaterializer.cs | 8 +- .../elsa-workflows-studio/src/components.d.ts | 4 +- .../elsa-designer-tree/elsa-designer-tree.tsx | 7 +- .../tree/elsa-designer-tree/models.ts | 5 + .../elsa-workflow-blueprint-viewer-screen.tsx | 9 +- .../elsa-activity-editor-modal.tsx | 65 ++++-- .../elsa-activity-picker-modal.tsx | 9 +- .../elsa-activity-picker-modal/readme.md | 0 ...elsa-workflow-definition-editor-screen.tsx | 19 +- .../elsa-workflow-instance-viewer-screen.tsx | 9 +- .../src/models/domain.ts | 12 +- .../elsa-workflows-studio/src/models/view.ts | 4 +- .../src/services/elsa-client.ts | 187 +++++++++--------- .../elsa-workflows-studio/src/utils/forms.tsx | 19 +- .../elsa-workflows-studio/src/utils/store.ts | 1 + .../WorkflowInstanceConfiguration.cs | 1 - .../EntityFrameworkWorkflowInstanceStore.cs | 3 - .../Documents/WorkflowInstanceDocument.cs | 1 - .../Elsa.Scripting.JavaScript.csproj | 2 +- .../Handlers/ConfigureJavaScriptEngine.cs | 42 ++-- .../Services/JintJavaScriptEvaluator.cs | 3 +- .../WorkflowStorageProviders/List.cs | 41 ++++ .../WorkflowStorageProviders/Models.cs | 4 + 32 files changed, 319 insertions(+), 173 deletions(-) rename src/designer/elsa-workflows-studio/src/components/{pickers => screens/workflow-definition-editor}/elsa-activity-picker-modal/elsa-activity-picker-modal.tsx (97%) rename src/designer/elsa-workflows-studio/src/components/{pickers => screens/workflow-definition-editor}/elsa-activity-picker-modal/readme.md (100%) create mode 100644 src/server/Elsa.Server.Api/Endpoints/WorkflowStorageProviders/List.cs create mode 100644 src/server/Elsa.Server.Api/Endpoints/WorkflowStorageProviders/Models.cs diff --git a/src/activities/Elsa.Activities.Http/Activities/SendHttpRequest/SendHttpRequest.cs b/src/activities/Elsa.Activities.Http/Activities/SendHttpRequest/SendHttpRequest.cs index 4a76b09c5..e6e9fda9b 100644 --- a/src/activities/Elsa.Activities.Http/Activities/SendHttpRequest/SendHttpRequest.cs +++ b/src/activities/Elsa.Activities.Http/Activities/SendHttpRequest/SendHttpRequest.cs @@ -102,6 +102,8 @@ namespace Elsa.Activities.Http )] public ICollection? SupportedStatusCodes { get; set; } = new HashSet(new[] { 200 }); + [ActivityOutput] public HttpResponseModel Response { get; set; } + protected override async ValueTask 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 { OutcomeNames.Done, statusOutcome }; if (!isSupportedStatusCode) outcomes.Add("Unsupported Status Code"); + Response = responseModel; return Combine(Output(responseModel), Outcomes(outcomes)); } diff --git a/src/clients/Elsa.Client/Models/ActivityDefinition.cs b/src/clients/Elsa.Client/Models/ActivityDefinition.cs index 9cf1939b9..87a085972 100644 --- a/src/clients/Elsa.Client/Models/ActivityDefinition.cs +++ b/src/clients/Elsa.Client/Models/ActivityDefinition.cs @@ -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 Properties { get; set; } = new List(); + [DataMember(Order = 11)] public string? OutputStorageProviderName { get; set; } + [DataMember(Order = 12)] public ICollection Properties { get; set; } = new List(); } } \ No newline at end of file diff --git a/src/clients/Elsa.Client/Models/WorkflowInstance.cs b/src/clients/Elsa.Client/Models/WorkflowInstance.cs index 5edd00bc9..9557cc85d 100644 --- a/src/clients/Elsa.Client/Models/WorkflowInstance.cs +++ b/src/clients/Elsa.Client/Models/WorkflowInstance.cs @@ -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 ActivityData { get; set; } = new Dictionary(); - [DataMember(Order = 17)] public IDictionary ActivityOutput { get; set; } = new Dictionary(); - [DataMember(Order = 18)] + [DataMember(Order = 17)] public HashSet BlockingActivities { get => _blockingActivities; set => _blockingActivities = new HashSet(value, BlockingActivityEqualityComparer.Instance); } - [DataMember(Order = 19)] public WorkflowFault? Fault { get; set; } - [DataMember(Order = 20)] public SimpleStack ScheduledActivities { get; set; } - [DataMember(Order = 21)] public SimpleStack Scopes { get; set; } - [DataMember(Order = 22)] public ScheduledActivity? CurrentActivity { get; set; } + [DataMember(Order = 18)] public WorkflowFault? Fault { get; set; } + [DataMember(Order = 19)] public SimpleStack ScheduledActivities { get; set; } + [DataMember(Order = 20)] public SimpleStack Scopes { get; set; } + [DataMember(Order = 21)] public ScheduledActivity? CurrentActivity { get; set; } } } \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/ActivityResults/OutputResult.cs b/src/core/Elsa.Abstractions/ActivityResults/OutputResult.cs index de8263352..e503506a5 100644 --- a/src/core/Elsa.Abstractions/ActivityResults/OutputResult.cs +++ b/src/core/Elsa.Abstractions/ActivityResults/OutputResult.cs @@ -21,8 +21,9 @@ namespace Elsa.ActivityResults { var workflowStorageService = activityExecutionContext.GetService(); 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); } } } \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Models/ActivityDefinition.cs b/src/core/Elsa.Abstractions/Models/ActivityDefinition.cs index a20ae3570..9173ffaaa 100644 --- a/src/core/Elsa.Abstractions/Models/ActivityDefinition.cs +++ b/src/core/Elsa.Abstractions/Models/ActivityDefinition.cs @@ -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 Properties { get; set; } = new List(); + public IDictionary PropertyStorageProviders { get; } = new Dictionary(); } } \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Models/WorkflowInstance.cs b/src/core/Elsa.Abstractions/Models/WorkflowInstance.cs index 814cf274f..2ed1ffd98 100644 --- a/src/core/Elsa.Abstractions/Models/WorkflowInstance.cs +++ b/src/core/Elsa.Abstractions/Models/WorkflowInstance.cs @@ -33,7 +33,6 @@ namespace Elsa.Models public Variables Variables { get; set; } public WorkflowOutputReference? Output { get; set; } public IDictionary> ActivityData { get; set; } = new Dictionary>(); - public IDictionary ActivityOutput { get; set; } = new Dictionary(); public HashSet BlockingActivities { diff --git a/src/core/Elsa.Abstractions/Services/Models/ActivityBlueprint.cs b/src/core/Elsa.Abstractions/Services/Models/ActivityBlueprint.cs index 12dd3efa6..992e6b8b7 100644 --- a/src/core/Elsa.Abstractions/Services/Models/ActivityBlueprint.cs +++ b/src/core/Elsa.Abstractions/Services/Models/ActivityBlueprint.cs @@ -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 PropertyStorageProviders { get; } = new Dictionary(); - public string? OutputStorageProviderName { get; } + public IDictionary PropertyStorageProviders { get; set; } = new Dictionary(); + public string? OutputStorageProviderName { get; set; } public string? Source { get; set; } diff --git a/src/core/Elsa.Abstractions/Services/WorkflowStorage/IWorkflowStorageService.cs b/src/core/Elsa.Abstractions/Services/WorkflowStorage/IWorkflowStorageService.cs index f3db69657..982eb9d82 100644 --- a/src/core/Elsa.Abstractions/Services/WorkflowStorage/IWorkflowStorageService.cs +++ b/src/core/Elsa.Abstractions/Services/WorkflowStorage/IWorkflowStorageService.cs @@ -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 ListProviders(); ValueTask SaveAsync(string? providerName, WorkflowStorageContext context, string key, object? value, CancellationToken cancellationToken = default); ValueTask LoadAsync(string? providerName, WorkflowStorageContext context, string key, CancellationToken cancellationToken = default); ValueTask DeleteAsync(string? providerName, WorkflowStorageContext context, string key, CancellationToken cancellationToken = default); diff --git a/src/core/Elsa.Core/Services/WorkflowStorage/WorkflowStorageService.cs b/src/core/Elsa.Core/Services/WorkflowStorage/WorkflowStorageService.cs index a73779f60..ee1a9501a 100644 --- a/src/core/Elsa.Core/Services/WorkflowStorage/WorkflowStorageService.cs +++ b/src/core/Elsa.Core/Services/WorkflowStorage/WorkflowStorageService.cs @@ -20,6 +20,7 @@ namespace Elsa.Services.WorkflowStorage } public IWorkflowStorageProvider GetProviderByNameOrDefault(string? providerName = default) => providerName != null ? _providersLookup.GetItem(providerName) ?? _defaultStorageProvider : _defaultStorageProvider; + public IEnumerable ListProviders() => _providersLookup.Values; public async ValueTask SaveAsync(string? providerName, WorkflowStorageContext context, string key, object? value, CancellationToken cancellationToken = default) { diff --git a/src/core/Elsa.Core/Services/Workflows/WorkflowBlueprintMaterializer.cs b/src/core/Elsa.Core/Services/Workflows/WorkflowBlueprintMaterializer.cs index dbfceef61..36f4923ab 100644 --- a/src/core/Elsa.Core/Services/Workflows/WorkflowBlueprintMaterializer.cs +++ b/src/core/Elsa.Core/Services/Workflows/WorkflowBlueprintMaterializer.cs @@ -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 }); } diff --git a/src/designer/elsa-workflows-studio/src/components.d.ts b/src/designer/elsa-workflows-studio/src/components.d.ts index 2e742e20e..f2bdaf80f 100644 --- a/src/designer/elsa-workflows-studio/src/components.d.ts +++ b/src/designer/elsa-workflows-studio/src/components.d.ts @@ -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; @@ -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) => void; diff --git a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx index 875c64b56..83b18615f 100644 --- a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx +++ b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx @@ -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; @Event() activitySelected: EventEmitter; @Event() activityDeselected: EventEmitter; @@ -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. diff --git a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/models.ts b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/models.ts index 7a0f79c74..e3706e4eb 100644 --- a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/models.ts +++ b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/models.ts @@ -11,4 +11,9 @@ export interface ActivityContextMenuState { x: number; y: number; activity?: ActivityModel | null; +} + +export enum LayoutDirection { + Horizontal, + Vertical } \ No newline at end of file diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-blueprint-viewer/elsa-workflow-blueprint-viewer-screen/elsa-workflow-blueprint-viewer-screen.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-blueprint-viewer/elsa-workflow-blueprint-viewer-screen/elsa-workflow-blueprint-viewer-screen.tsx index e6c4878ec..4575ba2c9 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-blueprint-viewer/elsa-workflow-blueprint-viewer-screen/elsa-workflow-blueprint-viewer-screen.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-blueprint-viewer/elsa-workflow-blueprint-viewer-screen/elsa-workflow-blueprint-viewer-screen.tsx @@ -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 } } diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-editor-modal/elsa-activity-editor-modal.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-editor-modal/elsa-activity-editor-modal.tsx index 2d4fdd6db..a40ae39e3 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-editor-modal/elsa-activity-editor-modal.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-editor-modal/elsa-activity-editor-modal.tsx @@ -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 = []; @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 = activityDescriptor.inputProperties; + const inputProperties: Array = 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 = []; - 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 ( - this.dialog = el} > + this.dialog = el}>
this.onSubmit(e)} ref={el => this.form = el} key={this.timestamp.getTime().toString()}>
@@ -159,23 +162,43 @@ export class ElsaActivityEditorModal { renderSelectedTab(activityModel: ActivityModel, activityDescriptor: ActivityDescriptor, categories: Array) { 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 = 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 ( -
+
+ + {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}
); @@ -198,7 +221,7 @@ export class ElsaActivityEditorModal { renderPropertiesTab(activityModel: ActivityModel, activityDescriptor: ActivityDescriptor) { const propertyDescriptors: Array = this.renderProps.defaultProperties; - if(propertyDescriptors.length == 0) + if (propertyDescriptors.length == 0) return undefined; const key = `activity-settings:${activityModel.activityId}`; diff --git a/src/designer/elsa-workflows-studio/src/components/pickers/elsa-activity-picker-modal/elsa-activity-picker-modal.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-picker-modal/elsa-activity-picker-modal.tsx similarity index 97% rename from src/designer/elsa-workflows-studio/src/components/pickers/elsa-activity-picker-modal/elsa-activity-picker-modal.tsx rename to src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-picker-modal/elsa-activity-picker-modal.tsx index ef5ebd75e..2c090693e 100644 --- a/src/designer/elsa-workflows-studio/src/components/pickers/elsa-activity-picker-modal/elsa-activity-picker-modal.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-picker-modal/elsa-activity-picker-modal.tsx @@ -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', diff --git a/src/designer/elsa-workflows-studio/src/components/pickers/elsa-activity-picker-modal/readme.md b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-picker-modal/readme.md similarity index 100% rename from src/designer/elsa-workflows-studio/src/components/pickers/elsa-activity-picker-modal/readme.md rename to src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-activity-picker-modal/readme.md diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx index 2f19bcc12..3dc896b6f 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx @@ -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(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 } } diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-viewer-screen.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-viewer-screen.tsx index 4bc6f20fb..5e0b0db00 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-viewer-screen.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-viewer/elsa-workflow-instance-viewer-screen/elsa-workflow-instance-viewer-screen.tsx @@ -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 } } diff --git a/src/designer/elsa-workflows-studio/src/models/domain.ts b/src/designer/elsa-workflows-studio/src/models/domain.ts index 976b6644b..16048ac84 100644 --- a/src/designer/elsa-workflows-studio/src/models/domain.ts +++ b/src/designer/elsa-workflows-studio/src/models/domain.ts @@ -46,9 +46,10 @@ export interface ActivityBlueprint { persistWorkflow: boolean; loadWorkflowContext: boolean; saveWorkflowContext: boolean; - persistOutput: boolean; + outputStorageProviderName?: string; source?: string; properties: Variables; + propertyStorageProviders: Map; } export interface Connection { @@ -139,8 +140,9 @@ export interface ActivityDefinition { persistWorkflow: boolean; loadWorkflowContext: boolean; saveWorkflowContext: boolean; - persistOutput: boolean; + outputStorageProviderName: string; properties: Array; + propertyStorageProviders: Map; } export interface WorkflowExecutionLogRecord { @@ -260,6 +262,7 @@ export interface ActivityDescriptor { outcomes: Array; browsable: boolean; inputProperties: Array; + outputProperties: Array; } export interface ActivityPropertyDescriptor { @@ -274,6 +277,11 @@ export interface ActivityPropertyDescriptor { supportedSyntaxes: Array } +export interface WorkflowStorageDescriptor { + name: string; + displayName?: string; +} + export interface PagedList { items: Array; page?: number; diff --git a/src/designer/elsa-workflows-studio/src/models/view.ts b/src/designer/elsa-workflows-studio/src/models/view.ts index fab973465..bdc0877da 100644 --- a/src/designer/elsa-workflows-studio/src/models/view.ts +++ b/src/designer/elsa-workflows-studio/src/models/view.ts @@ -1,4 +1,5 @@ import {ActivityDefinitionProperty, WorkflowPersistenceBehavior} from "./domain"; +import {Map} from '../utils/utils'; export interface WorkflowModel { activities: Array; @@ -15,9 +16,10 @@ export interface ActivityModel { outcomes: Array; properties: Array; persistWorkflow?: boolean; - persistOutput?: boolean; + outputStorageProviderName?: string; loadWorkflowContext?: boolean; saveWorkflowContext?: boolean; + propertyStorageProviders: Map; } export interface ConnectionModel { diff --git a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts index 3948ae4b5..53f4e7c27 100644 --- a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts +++ b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts @@ -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>('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>; -} + export interface ActivitiesApi { + list(): Promise>; + } -export interface WorkflowDefinitionsApi { + export interface WorkflowDefinitionsApi { - list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise>; + list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise>; - getByDefinitionAndVersion(definitionId: string, versionOptions: VersionOptions): Promise; + getByDefinitionAndVersion(definitionId: string, versionOptions: VersionOptions): Promise; - save(request: SaveWorkflowDefinitionRequest): Promise; + save(request: SaveWorkflowDefinitionRequest): Promise; - delete(definitionId: string): Promise; + delete(definitionId: string): Promise; - retract(workflowDefinitionId: string): Promise; + retract(workflowDefinitionId: string): Promise; - export(workflowDefinitionId: string, versionOptions: VersionOptions): Promise; + export(workflowDefinitionId: string, versionOptions: VersionOptions): Promise; - import(workflowDefinitionId: string, file: File): Promise; -} + import(workflowDefinitionId: string, file: File): Promise; + } -export interface WorkflowRegistryApi { - list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise>; + export interface WorkflowRegistryApi { + list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise>; - get(id: string, versionOptions: VersionOptions): Promise; -} + get(id: string, versionOptions: VersionOptions): Promise; + } -export interface WorkflowInstancesApi { - list(page?: number, pageSize?: number, workflowDefinitionId?: string, workflowStatus?: WorkflowStatus, orderBy?: OrderBy, searchTerm?: string): Promise>; + export interface WorkflowInstancesApi { + list(page?: number, pageSize?: number, workflowDefinitionId?: string, workflowStatus?: WorkflowStatus, orderBy?: OrderBy, searchTerm?: string): Promise>; - get(id: string): Promise; + get(id: string): Promise; - delete(id: string): Promise; + delete(id: string): Promise; - bulkDelete(request: BulkDeleteWorkflowsRequest): Promise; -} + bulkDelete(request: BulkDeleteWorkflowsRequest): Promise; + } -export interface WorkflowExecutionLogApi { + export interface WorkflowExecutionLogApi { - get(workflowInstanceId: string, page?: number, pageSize?: number): Promise>; + get(workflowInstanceId: string, page?: number, pageSize?: number): Promise>; -} + } -export interface BulkDeleteWorkflowsRequest { - workflowInstanceIds: Array; -} + export interface BulkDeleteWorkflowsRequest { + workflowInstanceIds: Array; + } -export interface BulkDeleteWorkflowsResponse { - deletedWorkflowCount: number; -} + export interface BulkDeleteWorkflowsResponse { + deletedWorkflowCount: number; + } -export interface ScriptingApi { - getJavaScriptTypeDefinitions(workflowDefinitionId: string, context?: string): Promise -} + export interface ScriptingApi { + getJavaScriptTypeDefinitions(workflowDefinitionId: string, context?: string): Promise + } -export interface DesignerApi { - runtimeSelectItemsApi: RuntimeSelectItemsApi; -} + export interface DesignerApi { + runtimeSelectItemsApi: RuntimeSelectItemsApi; + } -export interface RuntimeSelectItemsApi { - get(providerTypeName: string, context?: any): Promise> -} + export interface RuntimeSelectItemsApi { + get(providerTypeName: string, context?: any): Promise> + } -export interface ActivityStatsApi { - get(workflowInstanceId: string, activityId: string): Promise; -} + export interface ActivityStatsApi { + get(workflowInstanceId: string, activityId: string): Promise; + } -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; - connections: Array; -} + export interface WorkflowStorageProvidersApi { + list(): Promise>; + } -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; + connections: Array; + } -export interface ActivityStats { - fault?: ActivityFault; - averageExecutionTime: string; - fastestExecutionTime: string; - slowestExecutionTime: string; - lastExecutedAt: Date; - eventCounts: Array; -} + 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; + } -interface ActivityFault{ - message: string; -} \ No newline at end of file + interface ActivityEventCount { + eventName: string; + count: number; + } + + interface ActivityFault { + message: string; + } \ No newline at end of file diff --git a/src/designer/elsa-workflows-studio/src/utils/forms.tsx b/src/designer/elsa-workflows-studio/src/utils/forms.tsx index 918b32959..3715e7d0e 100644 --- a/src/designer/elsa-workflows-studio/src/utils/forms.tsx +++ b/src/designer/elsa-workflows-studio/src/utils/forms.tsx @@ -25,7 +25,8 @@ export function textInput(context: FormContext, fieldName: string, label: string {label}
- 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"/> + 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"/>
{hint && hint.length > 0 ?

{hint}

: undefined}
); @@ -53,7 +54,8 @@ export function textArea(context: FormContext, fieldName: string, label: string, {label}
-