Implement variables panel on workflow instance viewer (#3589)

* Small visual improvement for embedded ports

* Implement variables view for workflow instance viewer
This commit is contained in:
Sipke Schoorstra 2023-01-03 19:15:05 +01:00 committed by GitHub
parent 32b7f85142
commit cbbac57e6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 162 additions and 74 deletions

View file

@ -40,7 +40,7 @@ services
.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
.UseWorkflowRuntime(runtime =>
{
runtime.UseProtoActor(proto => proto.PersistenceProvider = _ => new SqliteProvider(new SqliteConnectionStringBuilder(sqliteConnectionString)));
//runtime.UseProtoActor(proto => proto.PersistenceProvider = _ => new SqliteProvider(new SqliteConnectionStringBuilder(sqliteConnectionString)));
runtime.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString));
runtime.UseAsyncWorkflowStateExporter();
})

View file

@ -243,6 +243,11 @@ export namespace Components {
interface ElsaVariablesEditor {
"variables"?: Array<Variable>;
}
interface ElsaVariablesViewer {
"variables"?: Array<Variable>;
"workflowDefinition": WorkflowDefinition;
"workflowInstance": WorkflowInstance;
}
interface ElsaWidgets {
"widgets": Array<Widget>;
}
@ -692,6 +697,12 @@ declare global {
prototype: HTMLElsaVariablesEditorElement;
new (): HTMLElsaVariablesEditorElement;
};
interface HTMLElsaVariablesViewerElement extends Components.ElsaVariablesViewer, HTMLStencilElement {
}
var HTMLElsaVariablesViewerElement: {
prototype: HTMLElsaVariablesViewerElement;
new (): HTMLElsaVariablesViewerElement;
};
interface HTMLElsaWidgetsElement extends Components.ElsaWidgets, HTMLStencilElement {
}
var HTMLElsaWidgetsElement: {
@ -832,6 +843,7 @@ declare global {
"elsa-variable-editor-dialog-content": HTMLElsaVariableEditorDialogContentElement;
"elsa-variable-picker-input": HTMLElsaVariablePickerInputElement;
"elsa-variables-editor": HTMLElsaVariablesEditorElement;
"elsa-variables-viewer": HTMLElsaVariablesViewerElement;
"elsa-widgets": HTMLElsaWidgetsElement;
"elsa-workflow-definition-browser": HTMLElsaWorkflowDefinitionBrowserElement;
"elsa-workflow-definition-editor": HTMLElsaWorkflowDefinitionEditorElement;
@ -1070,6 +1082,11 @@ declare namespace LocalJSX {
"onVariablesChanged"?: (event: ElsaVariablesEditorCustomEvent<Array<Variable>>) => void;
"variables"?: Array<Variable>;
}
interface ElsaVariablesViewer {
"variables"?: Array<Variable>;
"workflowDefinition"?: WorkflowDefinition;
"workflowInstance"?: WorkflowInstance;
}
interface ElsaWidgets {
"widgets"?: Array<Widget>;
}
@ -1185,6 +1202,7 @@ declare namespace LocalJSX {
"elsa-variable-editor-dialog-content": ElsaVariableEditorDialogContent;
"elsa-variable-picker-input": ElsaVariablePickerInput;
"elsa-variables-editor": ElsaVariablesEditor;
"elsa-variables-viewer": ElsaVariablesViewer;
"elsa-widgets": ElsaWidgets;
"elsa-workflow-definition-browser": ElsaWorkflowDefinitionBrowser;
"elsa-workflow-definition-editor": ElsaWorkflowDefinitionEditor;
@ -1250,6 +1268,7 @@ declare module "@stencil/core" {
"elsa-variable-editor-dialog-content": LocalJSX.ElsaVariableEditorDialogContent & JSXBase.HTMLAttributes<HTMLElsaVariableEditorDialogContentElement>;
"elsa-variable-picker-input": LocalJSX.ElsaVariablePickerInput & JSXBase.HTMLAttributes<HTMLElsaVariablePickerInputElement>;
"elsa-variables-editor": LocalJSX.ElsaVariablesEditor & JSXBase.HTMLAttributes<HTMLElsaVariablesEditorElement>;
"elsa-variables-viewer": LocalJSX.ElsaVariablesViewer & JSXBase.HTMLAttributes<HTMLElsaVariablesViewerElement>;
"elsa-widgets": LocalJSX.ElsaWidgets & JSXBase.HTMLAttributes<HTMLElsaWidgetsElement>;
"elsa-workflow-definition-browser": LocalJSX.ElsaWorkflowDefinitionBrowser & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionBrowserElement>;
"elsa-workflow-definition-editor": LocalJSX.ElsaWorkflowDefinitionEditor & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionEditorElement>;

View file

@ -0,0 +1,68 @@
import {Component, Event, EventEmitter, h, Prop, State, Watch} from "@stencil/core";
import {DeleteIcon, EditIcon} from "../../icons/tooling";
import {StorageDriverDescriptor, Variable, WorkflowInstance} from "../../../models";
import descriptorsStore from "../../../data/descriptors-store";
import {ModalActionClickArgs, ModalActionDefinition, ModalActionType, ModalDialogInstance, ModalDialogService} from "../../shared/modal-dialog";
import {Container} from "typedi";
import {WorkflowDefinition} from "../../../modules/workflow-definitions/models/entities";
@Component({
tag: 'elsa-variables-viewer',
shadow: false
})
export class VariablesViewer {
@Prop() variables?: Array<Variable>;
@Prop() workflowDefinition: WorkflowDefinition;
@Prop() workflowInstance: WorkflowInstance;
render() {
const variables = this.variables;
const storageDrivers: Array<StorageDriverDescriptor> = descriptorsStore.storageDrivers;
return (
<div>
<div class="align-middle inline-block min-w-full border-b border-gray-200">
<table class="default-table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Type</th>
<th scope="col">Storage</th>
<th scope="col">Value</th>
</tr>
</thead>
<tbody>
{variables.map(variable => {
const storage = storageDrivers.find(x => x.typeName == variable.storageDriverTypeName);
const storageName = storage?.displayName ?? '-';
const descriptor = descriptorsStore.variableDescriptors.find(x => x.typeName == variable.typeName);
const typeDisplayName = descriptor?.displayName ?? variable.typeName;
const variableValue = this.getVariableValue(variable, storage);
return (
<tr>
<td class="whitespace-nowrap">{variable.name}</td>
<td class="whitespace-nowrap">{typeDisplayName}</td>
<td>{storageName}</td>
<td class="pr-6">{variableValue}</td>
</tr>);
}
)}
</tbody>
</table>
</div>
</div>
);
}
private getVariableValue(variable: Variable, storage: StorageDriverDescriptor) : any {
if(storage.typeName !== 'Elsa.Workflows.Core.Implementations.WorkflowStorageDriver, Elsa.Workflows.Core')
return null;
const workflowInstance = this.workflowInstance;
const persistentVariables = workflowInstance.properties.PersistentVariablesDictionary;
const key = `${workflowInstance.id}:Workflow1:${variable.name}`;
return persistentVariables[key];
}
}

View file

@ -17,12 +17,12 @@ export const InfoList: FunctionalComponent<InfoListProps> = ({ title, dictionary
<div class="mx-auto">
<div>
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">{title}</h3>
<h3 class="text-md leading-6 font-medium text-gray-900">{title}</h3>
</div>
<div class="mt-5 border-t border-gray-200">
<div class="mt-3 border-t border-gray-200">
<dl class="sm:divide-y sm:divide-gray-200">
{entries.map(([k, v]) => (
<div class="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4">
<div class="py-3 sm:grid sm:grid-cols-3 sm:gap-4">
<dt class="text-sm font-medium text-gray-500">{k}</dt>
<dd class="flex justify-between items-center mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
{v}

View file

@ -42,7 +42,7 @@ export interface WorkflowInstanceSummary {
}
export interface WorkflowInstance extends WorkflowInstanceSummary {
workflowState: WorkflowState;
properties: any;
}
export interface PagedList<T> {

View file

@ -56,6 +56,7 @@ export class DefaultActivityTemplate {
const hasEmbeddedPorts = embeddedPorts.length > 0;
const canStartWorkflow = activity?.canStartWorkflow;
const icon = this.icon;
const hasIcon = !!icon;
const textColor = canStartWorkflow ? 'text-white' : 'text-gray-700';
const isTrigger = activityDescriptor?.kind == ActivityKind.Trigger;
const backgroundColor = canStartWorkflow ? isTrigger ? 'bg-green-400' : 'bg-blue-400' : 'bg-white';
@ -95,7 +96,7 @@ export class DefaultActivityTemplate {
<div>
<div class={`activity-wrapper border ${borderColor} ${backgroundColor} ${containerCssClass} rounded overflow-hidden`}>
<div class="text-white">
<div class={`flex flex-shrink items-center py-3 pr-3 ${iconBackgroundColor}`}>
<div class={`flex flex-shrink items-center py-3 ${ hasIcon ? 'pr-3' : 'px-3' } ${iconBackgroundColor}`}>
{this.renderIcon(icon)}
<span>{displayText}</span>
</div>
@ -146,16 +147,11 @@ export class DefaultActivityTemplate {
private renderPort = (activity: Activity, port: Port) => {
const canStartWorkflow = activity?.canStartWorkflow == true;
const textColor = canStartWorkflow ? 'text-white' : 'text-gray-700';
const displayTextClass = canStartWorkflow ? 'text-white' : 'text-gray-600';
const borderColor = port.name == this.selectedPortName ? 'border-blue-600' : 'border-gray-300';
const activityDescriptor = this.activityDescriptor;
const portProvider = this.portProviderRegistry.get(activityDescriptor.typeName);
const activityProperty = portProvider.resolvePort(port.name, {activity, activityDescriptor}) as Activity;
const childActivityDescriptor: ActivityDescriptor = activityProperty != null ? descriptorsStore.activityDescriptors.find(x => x.typeName == activityProperty.type) : null;
let childActivityDisplayText = activityProperty?.metadata?.displayText;
if (isNullOrWhitespace(childActivityDisplayText))
childActivityDisplayText = childActivityDescriptor?.displayName;
const renderActivityProperty = () => {
@ -167,7 +163,7 @@ export class DefaultActivityTemplate {
onMouseDown={e => e.stopPropagation()}
class="text-gray-400 hover:text-gray-600">
<div class="flex-grow">
<span class={textColor}>{port.displayName}</span>
<span class={`text-sm ${displayTextClass}`}>{port.displayName}</span>
</div>
</a>
</div>
@ -184,7 +180,7 @@ export class DefaultActivityTemplate {
onClick={e => this.onEditChildActivityClick(e, activity, port)}
onMouseDown={e => e.stopPropagation()}>
<div class="flex-grow">
<span class={textColor}>{port.displayName}</span>
<span class={`text-sm ${displayTextClass}`}>{port.displayName}</span>
</div>
</a>
</div>
@ -199,7 +195,7 @@ export class DefaultActivityTemplate {
onClick={e => this.onEditChildActivityClick(e, activity, port)}
onMouseDown={e => e.stopPropagation()}>
<div class="flex-grow">
<span class={textColor}>{port.displayName}</span>
<span class={`text-sm ${displayTextClass}`}>{port.displayName}</span>
</div>
</a>
</div>

View file

@ -1,5 +1,5 @@
import {Component, Event, EventEmitter, h, Method, Prop, State, Watch} from '@stencil/core';
import {TabChangedArgs, WorkflowInstance} from '../../../models';
import {TabChangedArgs, Variable, WorkflowInstance} from '../../../models';
import {InfoList} from "../../../components/shared/forms/info-list";
import {formatTimestamp, isNullOrWhitespace} from "../../../utils";
import {PropertiesTabModel, TabModel, WorkflowInstancePropertiesDisplayingArgs, WorkflowInstancePropertiesEventTypes, WorkflowInstancePropertiesViewerModel} from "../models";
@ -147,8 +147,10 @@ export class WorkflowDefinitionPropertiesEditor {
};
private renderVariablesTab = () => {
const variables: Array<Variable> = this.workflowDefinition?.variables ?? [];
return <div>
TODO: Variables editor
<elsa-variables-viewer variables={variables} workflowDefinition={this.workflowDefinition} workflowInstance={this.workflowInstance} />
</div>
};

View file

@ -12,10 +12,11 @@
</PropertyGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.12">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.8" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net7.0'">
@ -23,14 +24,11 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.EntityFrameworkCore\Elsa.EntityFrameworkCore.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.1" />
</ItemGroup>
</Project>

View file

@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.12">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -13,8 +13,8 @@
<ItemGroup Condition=" '$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.12">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -19,9 +19,9 @@
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.12">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -25,7 +25,6 @@ namespace Elsa.EntityFrameworkCore.Modules.Management
public void Configure(EntityTypeBuilder<WorkflowInstance> builder)
{
builder.Ignore(x => x.WorkflowState);
builder.Ignore(x => x.Fault);
builder.Property<string>("Data");
builder.Property(x => x.Status).HasConversion<EnumToStringConverter<WorkflowStatus>>();
builder.Property(x => x.SubStatus).HasConversion<EnumToStringConverter<WorkflowSubStatus>>();

View file

@ -33,24 +33,30 @@ public class EFCoreWorkflowInstanceStore : IWorkflowInstanceStore
public async Task<WorkflowInstance?> FindByIdAsync(string id, CancellationToken cancellationToken = default) =>
await _store.FindAsync(x => x.Id == id, Load, cancellationToken);
/// <inheritdoc />
public async Task SaveAsync(WorkflowInstance record, CancellationToken cancellationToken = default) =>
await _store.SaveAsync(record, Save, cancellationToken);
/// <inheritdoc />
public async Task SaveManyAsync(IEnumerable<WorkflowInstance> records, CancellationToken cancellationToken = default) =>
await _store.SaveManyAsync(records, Save, cancellationToken);
/// <inheritdoc />
public async Task<bool> DeleteAsync(string id, CancellationToken cancellationToken = default) =>
await _store.DeleteWhereAsync(x => x.Id == id, cancellationToken) > 0;
/// <inheritdoc />
public async Task<int> DeleteManyAsync(IEnumerable<string> ids, CancellationToken cancellationToken = default)
{
var idList = ids.ToList();
return await _store.DeleteWhereAsync(x => idList.Contains(x.Id), cancellationToken);
}
/// <inheritdoc />
public async Task DeleteManyByDefinitionIdAsync(string definitionId, CancellationToken cancellationToken = default) =>
await _store.DeleteWhereAsync(x => x.DefinitionId == definitionId, cancellationToken);
/// <inheritdoc />
public async Task<Page<WorkflowInstanceSummary>> FindManyAsync(FindWorkflowInstancesArgs args, CancellationToken cancellationToken = default)
{
var dbContext = await _store.CreateDbContextAsync(cancellationToken);
@ -94,9 +100,9 @@ public class EFCoreWorkflowInstanceStore : IWorkflowInstanceStore
return await query.PaginateAsync(x => WorkflowInstanceSummary.FromInstance(x), pageArgs);
}
public WorkflowInstance Save(ManagementElsaDbContext managementElsaDbContext, WorkflowInstance entity)
private WorkflowInstance Save(ManagementElsaDbContext managementElsaDbContext, WorkflowInstance entity)
{
var data = new WorkflowInstanceState(entity.WorkflowState, entity.Fault);
var data = entity.WorkflowState;
var options = _serializerOptionsProvider.CreatePersistenceOptions(ReferenceHandler.Preserve);
var json = JsonSerializer.Serialize(data, options);
@ -104,39 +110,22 @@ public class EFCoreWorkflowInstanceStore : IWorkflowInstanceStore
return entity;
}
public WorkflowInstance? Load(ManagementElsaDbContext managementElsaDbContext, WorkflowInstance? entity)
private WorkflowInstance? Load(ManagementElsaDbContext managementElsaDbContext, WorkflowInstance? entity)
{
if (entity == null)
return null;
var data = new WorkflowInstanceState(entity.WorkflowState, entity.Fault);
var data = entity.WorkflowState;
var json = (string?)managementElsaDbContext.Entry(entity).Property("Data").CurrentValue;
if (!string.IsNullOrWhiteSpace(json))
{
var options = _serializerOptionsProvider.CreatePersistenceOptions(ReferenceHandler.Preserve);
data = JsonSerializer.Deserialize<WorkflowInstanceState>(json, options)!;
data = JsonSerializer.Deserialize<WorkflowState>(json, options)!;
}
entity.WorkflowState = data.WorkflowState;
entity.Fault = data.Fault;
entity.WorkflowState = data;
return entity;
}
private class WorkflowInstanceState
{
public WorkflowInstanceState()
{
}
public WorkflowInstanceState(WorkflowState workflowState, WorkflowFaultState? fault)
{
WorkflowState = workflowState;
Fault = fault;
}
public WorkflowState WorkflowState { get; init; } = default!;
public WorkflowFaultState? Fault { get; set; }
}
}

View file

@ -7,20 +7,25 @@ using Microsoft.AspNetCore.Http;
namespace Elsa.Http.Handlers;
// ReSharper disable once ClassWithVirtualMembersNeverInherited.Global
/// <summary>
/// A default fault handler that writes information about the fault to the <see cref="HttpResponse"/>.
/// </summary>
public class DefaultHttpEndpointWorkflowFaultHandler : IHttpEndpointWorkflowFaultHandler
{
/// <inheritdoc />
public virtual async ValueTask HandleAsync(HttpEndpointFaultedWorkflowContext context)
{
var httpContext = context.HttpContext;
var workflowInstance = context.WorkflowInstance;
var fault = workflowInstance.WorkflowState.Fault!;
httpContext.Response.ContentType = MediaTypeNames.Application.Json;
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
var faultedResponse = JsonSerializer.Serialize(new
{
errorMessage = $"Workflow faulted at {workflowInstance.FaultedAt!} with error: {workflowInstance.Fault!.Message}",
exception = workflowInstance.Fault?.Exception,
errorMessage = $"Workflow faulted at {workflowInstance.FaultedAt!} with error: {fault.Message}",
exception = fault.Exception,
workflow = new
{
name = workflowInstance.Name,

View file

@ -20,7 +20,8 @@ public class WorkflowInstanceMapper : ResponseMapper<Response, WorkflowInstance>
SubStatus = e.SubStatus,
CorrelationId = e.CorrelationId,
Name = e.Name,
Fault = e.Fault,
Properties = e.WorkflowState.Properties,
Fault = e.WorkflowState.Fault,
CancelledAt = e.CancelledAt,
CreatedAt = e.CreatedAt,
FaultedAt = e.FaultedAt,

View file

@ -19,6 +19,7 @@ public class Response
public WorkflowSubStatus SubStatus { get; set; }
public string? CorrelationId { get; set; }
public string? Name { get; set; }
public PropertyBag Properties { get; set; }
public WorkflowFaultState? Fault { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset? LastExecutedAt { get; set; }

View file

@ -6,19 +6,23 @@ using Elsa.Workflows.Core.Models;
namespace Elsa.Workflows.Core.Activities;
/// <summary>
/// Sets a transient property on the workflow execution context the specified name value.
/// This value is used by the <see cref="PersistWorkflowInstanceMiddleware"/> component to update the name of the workflow instance.
/// Sets a property on the workflow execution context with the specified name value.
/// </summary>
[Activity("Elsa", "Workflows", "Set the name of the workflow instance to a specified value.")]
public class SetName : Activity
{
internal static readonly object WorkflowInstanceNameKey = new();
/// <summary>
/// The property key name used to store the workflow instance name.
/// </summary>
public const string WorkflowInstanceNameKey = "WorkflowInstanceName";
/// <inheritdoc />
[JsonConstructor]
public SetName([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <inheritdoc />
public SetName(Input<string> value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line)
{
Value = value;
@ -29,9 +33,10 @@ public class SetName : Activity
/// </summary>
public Input<string> Value { get; set; } = new("");
/// <inheritdoc />
protected override void Execute(ActivityExecutionContext context)
{
var value = context.Get(Value);
context.WorkflowExecutionContext.TransientProperties[WorkflowInstanceNameKey] = value!;
context.WorkflowExecutionContext.SetProperty(WorkflowInstanceNameKey, value!);
}
}

View file

@ -49,7 +49,7 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer
private void DeserializeProperties(WorkflowState state, WorkflowExecutionContext workflowExecutionContext)
{
workflowExecutionContext.Properties = state.Properties.Properties;
workflowExecutionContext.Properties = state.Properties.Dictionary;
}
private static void DeserializeCompletionCallbacks(WorkflowState state, WorkflowExecutionContext workflowExecutionContext)

View file

@ -44,7 +44,7 @@ public class ActivityExecutionContext
/// <summary>
/// A dictionary of values that can be associated with this activity execution context.
/// </summary>
public IDictionary<string, object> Properties { get; set; } = new Dictionary<string, object>();
public PropertyBag Properties { get; set; } = new();
/// <summary>
/// A transient dictionary of values that can be associated with this activity execution context.
@ -162,18 +162,18 @@ public class ActivityExecutionContext
/// <summary>
/// Returns a property value associated with the current activity context.
/// </summary>
public T? GetProperty<T>(string key) => Properties.TryGetValue<T?>(key, out var value) ? value : default;
public T? GetProperty<T>(string key) => Properties.Dictionary.TryGetValue<T?>(key, out var value) ? value : default;
/// <summary>
/// Returns a property value associated with the current activity context.
/// </summary>
public T GetProperty<T>(string key, Func<T> defaultValue)
{
if (Properties.TryGetValue<T?>(key, out var value))
if (Properties.Dictionary.TryGetValue<T?>(key, out var value))
return value!;
value = defaultValue();
Properties[key] = value!;
Properties.Dictionary[key] = value!;
return value!;
}
@ -181,7 +181,7 @@ public class ActivityExecutionContext
/// <summary>
/// Stores a property associated with the current activity context.
/// </summary>
public void SetProperty<T>(string key, T? value) => Properties[key] = value!;
public void SetProperty<T>(string key, T? value) => Properties.Dictionary[key] = value!;
/// <summary>
/// Updates a property associated with the current activity context.
@ -190,7 +190,7 @@ public class ActivityExecutionContext
{
var value = GetProperty<T?>(key);
value = updater(value);
Properties[key] = value;
Properties.Dictionary[key] = value;
return value;
}

View file

@ -11,10 +11,10 @@ public class PropertyBag
{
}
public PropertyBag(IDictionary<string, object> properties)
public PropertyBag(IDictionary<string, object> dictionary)
{
Properties = properties;
Dictionary = dictionary;
}
public IDictionary<string, object> Properties { get; init; }
public IDictionary<string, object> Dictionary { get; init; }
}

View file

@ -132,12 +132,12 @@ public class WorkflowExecutionContext
public IDictionary<string, object> Output { get; } = new Dictionary<string, object>();
/// <summary>
/// A dictionary that can be used by application code and activities to store information. Values need to be serializable, since this dictionary will be persisted alongside the workflow instance.
/// A dictionary that can be used by application code and activities to store information. Values need to be serializable.
/// </summary>
public IDictionary<string, object> Properties { get; set; } = new Dictionary<string, object>();
/// <summary>
/// A dictionary that can be used by application code and middleware to store information and even services. Values do not need to be serializable, since this dictionary will not be persisted.
/// A dictionary that can be used by application code and middleware to store information and even services. Values do not need to be serializable.
/// All data will be gone once workflow execution completes.
/// </summary>
public IDictionary<object, object> TransientProperties { get; set; } = new Dictionary<object, object>();

View file

@ -14,6 +14,6 @@ public class PropertyBagConverter : JsonConverter<PropertyBag>
public override void Write(Utf8JsonWriter writer, PropertyBag value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value.Properties);
JsonSerializer.Serialize(writer, value.Dictionary);
}
}

View file

@ -1,3 +1,5 @@
using Elsa.Workflows.Core.Models;
namespace Elsa.Workflows.Core.State;
public class ActivityExecutionContextState
@ -15,6 +17,6 @@ public class ActivityExecutionContextState
public string? ParentContextId { get; set; }
public string ScheduledActivityId { get; set; } = default!;
public string? OwnerActivityId { get; set; }
public IDictionary<string, object> Properties { get; set; } = new Dictionary<string, object>();
public PropertyBag Properties { get; set; } = new();
//public RegisterState Register { get; set; } = default!;
}

View file

@ -15,7 +15,6 @@ public class WorkflowInstance : Entity
public WorkflowSubStatus SubStatus { get; set; }
public string? CorrelationId { get; set; }
public string? Name { get; set; }
public WorkflowFaultState? Fault { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset? LastExecutedAt { get; set; }
public DateTimeOffset? FinishedAt { get; set; }

View file

@ -1,7 +1,9 @@
using Elsa.Common.Models;
using Elsa.Common.Services;
using Elsa.Extensions;
using Elsa.Mediator.Models;
using Elsa.Mediator.Services;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.State;
using Elsa.Workflows.Management.Entities;
@ -70,6 +72,8 @@ public class AsyncWorkflowStateExporter : IWorkflowStateExporter, ICommandHandle
workflowInstance.SubStatus = workflowState.SubStatus;
workflowInstance.CorrelationId = workflowState.CorrelationId;
workflowInstance.LastExecutedAt = now;
workflowInstance.WorkflowState = workflowState;
workflowInstance.Name = workflowState.Properties.Dictionary.GetValue(SetName.WorkflowInstanceNameKey) as string;
// TODO: Store timestamps such as CancelledAt, FaultedAt, etc.