Merge remote-tracking branch 'origin/v3' into v3
This commit is contained in:
commit
a6dd2d089b
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
|
||||
import { InputDefinition, OutputDefinition, WorkflowDefinition, WorkflowDefinitionSummary } from "./modules/workflow-definitions/models/entities";
|
||||
import { Activity, ActivityDeletedArgs, ActivitySelectedArgs, ChildActivitySelectedArgs, ContainerSelectedArgs, EditChildActivityArgs, GraphUpdatedArgs, IntellisenseContext, SelectListItem, TabChangedArgs, TabDefinition, Variable, WorkflowInstance, WorkflowInstanceSummary, WorkflowUpdatedArgs } from "./models";
|
||||
import { Activity, ActivityDeletedArgs, ActivitySelectedArgs, ChildActivitySelectedArgs, ContainerSelectedArgs, EditChildActivityArgs, GraphUpdatedArgs, IntellisenseContext, SelectListItem, TabChangedArgs, TabDefinition, Variable, WorkflowExecutionLogRecord, WorkflowInstance, WorkflowInstanceSummary, WorkflowUpdatedArgs } from "./models";
|
||||
import { ActivityUpdatedArgs, DeleteActivityRequestedArgs, Widget, WorkflowDefinitionPropsUpdatedArgs, WorkflowDefinitionUpdatedArgs } from "./modules/workflow-definitions/models/ui";
|
||||
import { NotificationType } from "./modules/notifications/models";
|
||||
import { Button } from "./components/shared/button-group/models";
|
||||
|
|
@ -39,8 +39,11 @@ export namespace Components {
|
|||
}
|
||||
interface ElsaActivityProperties {
|
||||
"activity"?: Activity;
|
||||
"activityExecutionLog": WorkflowExecutionLogRecord;
|
||||
"activityPropertyTabIndex"?: number;
|
||||
"hide": () => Promise<void>;
|
||||
"show": () => Promise<void>;
|
||||
"updateSelectedTab": (tabIndex: number) => Promise<void>;
|
||||
}
|
||||
interface ElsaActivityPropertiesEditor {
|
||||
"activity"?: Activity;
|
||||
|
|
@ -317,6 +320,7 @@ export namespace Components {
|
|||
"workflowInstance": WorkflowInstance;
|
||||
}
|
||||
interface ElsaWorkflowJournal {
|
||||
"getExecutionLogByWorkflowInstanceId": (activityId: string) => Promise<WorkflowExecutionLogRecord>;
|
||||
"workflowDefinition": WorkflowDefinition;
|
||||
"workflowInstance": WorkflowInstance;
|
||||
}
|
||||
|
|
@ -436,6 +440,10 @@ export interface ElsaWorkflowInstanceBrowserCustomEvent<T> extends CustomEvent<T
|
|||
detail: T;
|
||||
target: HTMLElsaWorkflowInstanceBrowserElement;
|
||||
}
|
||||
export interface ElsaWorkflowJournalCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLElsaWorkflowJournalElement;
|
||||
}
|
||||
export interface ElsaWorkflowNavigatorCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLElsaWorkflowNavigatorElement;
|
||||
|
|
@ -907,6 +915,8 @@ declare namespace LocalJSX {
|
|||
}
|
||||
interface ElsaActivityProperties {
|
||||
"activity"?: Activity;
|
||||
"activityExecutionLog"?: WorkflowExecutionLogRecord;
|
||||
"activityPropertyTabIndex"?: number;
|
||||
}
|
||||
interface ElsaActivityPropertiesEditor {
|
||||
"activity"?: Activity;
|
||||
|
|
@ -1186,6 +1196,7 @@ declare namespace LocalJSX {
|
|||
"workflowInstance"?: WorkflowInstance;
|
||||
}
|
||||
interface ElsaWorkflowJournal {
|
||||
"onJournalItemStatusSelected"?: (event: ElsaWorkflowJournalCustomEvent<string>) => void;
|
||||
"workflowDefinition"?: WorkflowDefinition;
|
||||
"workflowInstance"?: WorkflowInstance;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ export interface WorkflowExecutionLogRecord {
|
|||
activityType: string;
|
||||
timestamp: Date;
|
||||
eventName: string;
|
||||
payload?: any;
|
||||
}
|
||||
|
||||
export enum SyntaxNames {
|
||||
|
|
|
|||
|
|
@ -1,26 +1,34 @@
|
|||
import {Component, h, Method, Prop, State} from '@stencil/core';
|
||||
import {Component, Event, EventEmitter, h, Listen, Method, Prop, State} from '@stencil/core';
|
||||
import {camelCase} from 'lodash';
|
||||
import {
|
||||
Activity,
|
||||
ActivityDescriptor,
|
||||
Lookup,
|
||||
TabChangedArgs,
|
||||
TabDefinition
|
||||
TabDefinition,
|
||||
WorkflowExecutionLogRecord
|
||||
} from '../../../models';
|
||||
import {InfoList} from "../../../components/shared/forms/info-list";
|
||||
import descriptorsStore from "../../../data/descriptors-store";
|
||||
import moment from 'moment';
|
||||
import Container from 'typedi';
|
||||
import { ActivityIconRegistry } from '../../../services';
|
||||
import { ActivityIconSize } from '../../../components/icons/activities/models';
|
||||
|
||||
@Component({
|
||||
tag: 'elsa-activity-properties',
|
||||
})
|
||||
export class ActivityProperties {
|
||||
private slideOverPanel: HTMLElsaSlideOverPanelElement;
|
||||
private readonly iconRegistry: ActivityIconRegistry;
|
||||
|
||||
constructor() {
|
||||
this.iconRegistry = Container.get(ActivityIconRegistry);
|
||||
}
|
||||
|
||||
@Prop({mutable: true}) public activity?: Activity;
|
||||
|
||||
@Prop() public activityExecutionLog: WorkflowExecutionLogRecord;
|
||||
@Prop() public activityPropertyTabIndex?: number;
|
||||
@State() private selectedTabIndex: number = 0;
|
||||
|
||||
@Method()
|
||||
|
|
@ -33,6 +41,17 @@ export class ActivityProperties {
|
|||
await this.slideOverPanel.hide();
|
||||
}
|
||||
|
||||
@Method()
|
||||
public async updateSelectedTab(tabIndex : number): Promise<void> {
|
||||
this.selectedTabIndex = tabIndex;
|
||||
}
|
||||
|
||||
async componentWillLoad(): Promise<void> {
|
||||
if(this.activityPropertyTabIndex != null) {
|
||||
this.selectedTabIndex = this.activityPropertyTabIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const activity = this.activity;
|
||||
const activityDescriptor = this.findActivityDescriptor();
|
||||
|
|
@ -47,7 +66,12 @@ export class ActivityProperties {
|
|||
content: () => this.renderCommonTab()
|
||||
};
|
||||
|
||||
const tabs = !!activityDescriptor ? [propertiesTab, commonTab] : [];
|
||||
const journalTab: TabDefinition = {
|
||||
displayText: 'Journal',
|
||||
content: () => this.renderJournalTab()
|
||||
};
|
||||
|
||||
const tabs = !!activityDescriptor ? [propertiesTab, commonTab, journalTab] : [];
|
||||
const mainTitle = activity.id;
|
||||
const subTitle = activityDescriptor.displayName;
|
||||
|
||||
|
|
@ -93,4 +117,78 @@ export class ActivityProperties {
|
|||
return <div>
|
||||
</div>
|
||||
};
|
||||
|
||||
private renderJournalTab = () => {
|
||||
const log = this.activityExecutionLog;
|
||||
if(log == null) return;
|
||||
|
||||
const exception = log.payload?.exception;
|
||||
const statusColor = log.eventName == "Completed" ? "bg-blue-100" : log.eventName == "Faulted" ? "bg-red-100" : "bg-green-100";
|
||||
const icon = this.iconRegistry.getOrDefault(log.activityType)({size: ActivityIconSize.Small});
|
||||
return (
|
||||
<div class="border-2 cursor-pointer p-4 rounded">
|
||||
<div class="relative pb-10">
|
||||
<div class="relative flex space-x-3">
|
||||
<div>
|
||||
<span class={`h-8 w-8 rounded p-1 bg-blue-500 flex items-center justify-center ring-8 ring-white mr-1`}>
|
||||
{icon}
|
||||
</span>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4">
|
||||
<div>
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||
{log.activityType}
|
||||
</h3>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class={`relative inline-flex items-center rounded-full ${statusColor} border border-gray-300 px-3 py-0.5 text-sm`}>
|
||||
<span class="absolute flex-shrink-0 flex items-center justify-center">
|
||||
<span class={`h-1.5 w-1.5 rounded-full`} aria-hidden="true"/>
|
||||
</span>
|
||||
<span class="font-medium text-gray-900">{log.eventName}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-right text-sm whitespace-nowrap text-gray-500">
|
||||
<span>{moment(log.timestamp).format('DD-MM-YYYY HH:mm:ss')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-12 mt-2">
|
||||
<dl class="sm:divide-y sm:divide-gray-200">
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-2">
|
||||
<div class="sm:col-span-2">
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<span>Activity ID</span>
|
||||
<copy-button value={log.activityId}/>
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 mb-2">{log.activityId}</dd>
|
||||
</div>
|
||||
{!!exception ? (
|
||||
[<div class="sm:col-span-2">
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<span>Exception</span>
|
||||
<copy-button value={exception.Type + '\n' + exception.Message}/>
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
{exception.message}
|
||||
</dd>
|
||||
</div>,
|
||||
<div class="sm:col-span-2">
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
<span>Exception Details</span>
|
||||
<copy-button value={JSON.stringify(exception, null, 1)}/>
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 overflow-x-auto">
|
||||
<pre>{JSON.stringify(exception, null, 1)}</pre>
|
||||
</dd>
|
||||
</div>]
|
||||
) : undefined}
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import {Component, h, Prop, State, Watch} from "@stencil/core";
|
||||
import {ActivityDescriptor, Workflow, WorkflowExecutionLogRecord, WorkflowInstance} from "../../../models";
|
||||
import {Component, h, Method, Prop, State, Watch, Event, EventEmitter} from "@stencil/core";
|
||||
import {Workflow, WorkflowExecutionLogRecord, WorkflowInstance} from "../../../models";
|
||||
import {Container} from "typedi";
|
||||
import {ActivityIconRegistry, ActivityNode, createActivityNodeMap, flatten, walkActivities} from "../../../services";
|
||||
import {durationToString, formatTime, getDuration, Hash, isNullOrWhitespace} from "../../../utils";
|
||||
import descriptorsStore from '../../../data/descriptors-store';
|
||||
import {ActivityExecutionEventBlock} from "../models";
|
||||
import {ActivityIconSize} from "../../../components/icons/activities";
|
||||
import {WorkflowDefinition} from "../../workflow-definitions/models/entities";
|
||||
|
|
@ -33,6 +32,7 @@ export class Journal {
|
|||
@State() workflowExecutionLogRecords: Array<WorkflowExecutionLogRecord> = [];
|
||||
@State() rootBlocks: Array<ActivityExecutionEventBlock> = [];
|
||||
@State() expandedBlocks: Array<ActivityExecutionEventBlock> = [];
|
||||
@Event() journalItemStatusSelected: EventEmitter<string>;
|
||||
|
||||
@Watch('workflowInstance')
|
||||
async onWorkflowInstanceChanged(value: string) {
|
||||
|
|
@ -47,20 +47,18 @@ export class Journal {
|
|||
await this.loadJournalPage(0);
|
||||
}
|
||||
|
||||
async componentWillLoad() {
|
||||
@Method()
|
||||
public async getExecutionLogByWorkflowInstanceId(activityId: string): Promise<WorkflowExecutionLogRecord> {
|
||||
const logRecords = this.workflowExecutionLogRecords.filter(r => r.activityId === activityId);
|
||||
return logRecords.find(r => r.eventName === "Faulted" || r.eventName === "Completed") ?? logRecords.find(r => r.eventName === "Started");
|
||||
}
|
||||
|
||||
async componentWillLoad(): Promise<void> {
|
||||
this.createGraph();
|
||||
await this.loadJournalPage(0);
|
||||
}
|
||||
|
||||
render() {
|
||||
const workflowInstance = this.workflowInstance;
|
||||
const workflowDefinition = this.workflowDefinition;
|
||||
const nodeMap = this.nodeMap;
|
||||
const activityDescriptors: Array<ActivityDescriptor> = descriptorsStore.activityDescriptors;
|
||||
const blocks = this.rootBlocks;
|
||||
const iconRegistry = this.iconRegistry;
|
||||
const expandedBlocks = this.expandedBlocks;
|
||||
|
||||
return (
|
||||
|
||||
<div class="absolute inset-0 overflow-hidden">
|
||||
|
|
@ -107,16 +105,17 @@ export class Journal {
|
|||
const nodeMap = this.nodeMap;
|
||||
const iconRegistry = this.iconRegistry;
|
||||
const expandedBlocks = this.expandedBlocks;
|
||||
|
||||
return blocks.map((block, index) => {
|
||||
|
||||
const activityNode = nodeMap[block.activityId];
|
||||
const activity = activityNode.activity;
|
||||
const activityMetadata = activity.metadata;
|
||||
const activityDisplayText = isNullOrWhitespace(activityMetadata.displayText) ? activity.id : activityMetadata.displayText;
|
||||
const duration = durationToString(block.duration);
|
||||
const status = block.completed ? 'Completed' : 'Started';
|
||||
const status = block.completed ? 'Completed' : block.faulted ? 'Faulted' : 'Started';
|
||||
const icon = iconRegistry.getOrDefault(activity.type)({size: ActivityIconSize.Small});
|
||||
const expanded = !!expandedBlocks.find(x => x == block);
|
||||
const statusColor = block.completed ? "bg-blue-100" : block.faulted ? "bg-red-100" : "bg-green-100";
|
||||
|
||||
const toggleIcon = expanded
|
||||
? (
|
||||
|
|
@ -154,7 +153,9 @@ export class Journal {
|
|||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 text-green-800">{status}</span>
|
||||
{block.children.length == 0 ?
|
||||
(<a href="#" onClick={e => this.onStatusClick(e, block)} class={`inline-flex rounded-full ${statusColor} px-2 text-xs font-semibold leading-5 text-green-800`}>{status}</a>)
|
||||
: <span class={`inline-flex rounded-full ${statusColor} px-2 text-xs font-semibold leading-5 text-green-800`}>{status}</span>}
|
||||
</td>
|
||||
<td>{duration}</td>
|
||||
</tr>, expanded ? this.renderBlocks(block.children) : undefined]
|
||||
|
|
@ -187,7 +188,7 @@ export class Journal {
|
|||
return;
|
||||
|
||||
const workflowInstanceId = this.workflowInstance.id;
|
||||
const pageOfRecords = await this.workflowInstancesApi.getJournal({page, pageSize: PAGE_SIZE, workflowInstanceId: workflowInstanceId})
|
||||
const pageOfRecords = await this.workflowInstancesApi.getJournal({page, pageSize: PAGE_SIZE, workflowInstanceId: workflowInstanceId});
|
||||
const blocks = this.createBlocks(pageOfRecords.items);
|
||||
const rootBlocks = blocks.filter(x => !x.parentActivityInstanceId);
|
||||
this.workflowExecutionLogRecords = [...this.workflowExecutionLogRecords, ...pageOfRecords.items];
|
||||
|
|
@ -197,9 +198,11 @@ export class Journal {
|
|||
private createBlocks = (records: Array<WorkflowExecutionLogRecord>): Array<ActivityExecutionEventBlock> => {
|
||||
const startedEvents = records.filter(x => x.eventName == 'Started');
|
||||
const completedEvents = records.filter(x => x.eventName == 'Completed');
|
||||
const faultedEvents = records.filter(x => x.eventName == 'Faulted');
|
||||
|
||||
const blocks = startedEvents.map(startedRecord => {
|
||||
const completedRecord = completedEvents.find(x => x.activityInstanceId == startedRecord.activityInstanceId);
|
||||
const faultedRecord = faultedEvents.find(x => x.activityInstanceId == startedRecord.activityInstanceId);
|
||||
const duration = !!completedRecord ? getDuration(completedRecord.timestamp, startedRecord.timestamp) : null;
|
||||
|
||||
return {
|
||||
|
|
@ -207,10 +210,12 @@ export class Journal {
|
|||
activityInstanceId: startedRecord.activityInstanceId,
|
||||
parentActivityInstanceId: startedRecord.parentActivityInstanceId,
|
||||
completed: !!completedRecord,
|
||||
faulted: !!faultedRecord,
|
||||
timestamp: startedRecord.timestamp,
|
||||
duration: duration,
|
||||
startedRecord: startedRecord,
|
||||
completedRecord: completedRecord,
|
||||
faultedRecord: faultedRecord,
|
||||
children: []
|
||||
};
|
||||
});
|
||||
|
|
@ -243,4 +248,9 @@ export class Journal {
|
|||
const existingBlock = this.expandedBlocks.find(x => x == block);
|
||||
this.expandedBlocks = existingBlock ? this.expandedBlocks.filter(x => x != existingBlock) : [...this.expandedBlocks, block];
|
||||
};
|
||||
|
||||
private onStatusClick = async (e: MouseEvent, block: ActivityExecutionEventBlock) => {
|
||||
e.preventDefault();
|
||||
this.journalItemStatusSelected.emit(block.activityId);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import {
|
|||
ActivitySelectedArgs,
|
||||
ContainerSelectedArgs,
|
||||
GraphUpdatedArgs,
|
||||
WorkflowInstance
|
||||
WorkflowInstance,
|
||||
WorkflowExecutionLogRecord
|
||||
} from '../../../models';
|
||||
import {PluginRegistry, ActivityNameFormatter, ActivityDriverRegistry, EventBus} from '../../../services';
|
||||
import {MonacoEditorSettings} from "../../../services/monaco-editor-settings";
|
||||
|
|
@ -23,6 +24,8 @@ export class WorkflowInstanceViewer {
|
|||
private readonly activityNameFormatter: ActivityNameFormatter;
|
||||
private flowchartElement: HTMLElsaFlowchartElement;
|
||||
private container: HTMLDivElement;
|
||||
private workflowJournalElement: HTMLElsaWorkflowJournalElement;
|
||||
private activityPropertiesElement: HTMLElsaActivityPropertiesElement;
|
||||
|
||||
constructor() {
|
||||
this.eventBus = Container.get(EventBus);
|
||||
|
|
@ -37,6 +40,8 @@ export class WorkflowInstanceViewer {
|
|||
@State() private workflowDefinitionState: WorkflowDefinition;
|
||||
@State() private workflowInstanceState: WorkflowInstance;
|
||||
@State() private selectedActivity?: Activity;
|
||||
@State() private selectedActivityExecutionLog?: WorkflowExecutionLogRecord;
|
||||
@State() private activityPropertyTabIndex?: number;
|
||||
|
||||
@Watch('monacoLibPath')
|
||||
private handleMonacoLibPath(value: string) {
|
||||
|
|
@ -73,6 +78,26 @@ export class WorkflowInstanceViewer {
|
|||
@Listen('activitySelected')
|
||||
private async handleActivitySelected(e: CustomEvent<ActivitySelectedArgs>) {
|
||||
this.selectedActivity = e.detail.activity;
|
||||
this.selectedActivityExecutionLog = await this.workflowJournalElement.getExecutionLogByWorkflowInstanceId(this.selectedActivity.id);
|
||||
}
|
||||
|
||||
@Listen('journalItemStatusSelected')
|
||||
private async handleJournalStatusSelected(e: CustomEvent<string>) {
|
||||
const activityId = e.detail;
|
||||
const graph = await this.flowchartElement.getGraph();
|
||||
const node = graph.getNodes().find(n => n.id == activityId)
|
||||
|
||||
if(node != null) {
|
||||
graph.resetSelection(node);
|
||||
this.selectedActivity = node.data;
|
||||
this.selectedActivityExecutionLog = await this.workflowJournalElement.getExecutionLogByWorkflowInstanceId(this.selectedActivity.id);
|
||||
if(this.activityPropertiesElement == null) {
|
||||
this.activityPropertyTabIndex = 2;
|
||||
}
|
||||
else {
|
||||
this.activityPropertiesElement.updateSelectedTab(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Listen('graphUpdated')
|
||||
|
|
@ -127,9 +152,8 @@ export class WorkflowInstanceViewer {
|
|||
|
||||
private renderSelectedObject = () => {
|
||||
const activity = this.selectedActivity;
|
||||
|
||||
if (!!activity)
|
||||
return <elsa-activity-properties activity={activity}/>;
|
||||
return <elsa-activity-properties activity={activity} activityExecutionLog={this.selectedActivityExecutionLog} activityPropertyTabIndex={this.activityPropertyTabIndex} ref={el => this.activityPropertiesElement = el}/>;
|
||||
}
|
||||
|
||||
private getWorkflowInternal = async (): Promise<WorkflowDefinition> => {
|
||||
|
|
@ -159,14 +183,18 @@ export class WorkflowInstanceViewer {
|
|||
public render() {
|
||||
const workflowDefinition = this.workflowDefinitionState;
|
||||
const workflowInstance = this.workflowInstanceState;
|
||||
|
||||
return (
|
||||
|
||||
<div class="absolute inset-0" ref={el => this.container = el}>
|
||||
<elsa-panel
|
||||
class="elsa-activity-picker-container"
|
||||
position={PanelPosition.Left}
|
||||
onExpandedStateChanged={e => this.onActivityPickerPanelStateChanged(e.detail)}>
|
||||
<elsa-workflow-journal workflowDefinition={workflowDefinition} workflowInstance={workflowInstance}/>
|
||||
<elsa-workflow-journal
|
||||
workflowDefinition={workflowDefinition}
|
||||
workflowInstance={workflowInstance}
|
||||
ref={el => this.workflowJournalElement = el}
|
||||
/>
|
||||
</elsa-panel>
|
||||
<elsa-flowchart
|
||||
ref={el => this.flowchartElement = el}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export interface ActivityExecutionEventBlock {
|
|||
activityInstanceId: string;
|
||||
parentActivityInstanceId: string;
|
||||
completed: boolean;
|
||||
faulted: boolean;
|
||||
timestamp: Date;
|
||||
duration?: moment.Duration;
|
||||
startedRecord: WorkflowExecutionLogRecord;
|
||||
|
|
|
|||
|
|
@ -1,27 +1,44 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using Elsa.EntityFrameworkCore.Common;
|
||||
using Elsa.Workflows.Core.Serialization;
|
||||
using Elsa.Common.Entities;
|
||||
using Elsa.Common.Models;
|
||||
using Elsa.EntityFrameworkCore.Common;
|
||||
using Elsa.Workflows.Runtime.Entities;
|
||||
using Elsa.Workflows.Runtime.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace Elsa.EntityFrameworkCore.Modules.Runtime;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class EFCoreWorkflowExecutionLogStore : IWorkflowExecutionLogStore
|
||||
{
|
||||
private readonly SerializerOptionsProvider _serializerOptionsProvider;
|
||||
private readonly IDbContextFactory<RuntimeElsaDbContext> _dbContextFactory;
|
||||
private readonly EntityStore<RuntimeElsaDbContext, WorkflowExecutionLogRecord> _store;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
|
||||
public EFCoreWorkflowExecutionLogStore(EntityStore<RuntimeElsaDbContext, WorkflowExecutionLogRecord> store) => _store = store;
|
||||
|
||||
public EFCoreWorkflowExecutionLogStore(
|
||||
EntityStore<RuntimeElsaDbContext, WorkflowExecutionLogRecord> store,
|
||||
IDbContextFactory<RuntimeElsaDbContext> dbContextFactory,
|
||||
SerializerOptionsProvider serializerOptionsProvider)
|
||||
{
|
||||
_store = store;
|
||||
_serializerOptionsProvider = serializerOptionsProvider;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SaveAsync(WorkflowExecutionLogRecord record, CancellationToken cancellationToken = default) => await _store.SaveAsync(record, cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SaveManyAsync(IEnumerable<WorkflowExecutionLogRecord> records, CancellationToken cancellationToken = default) => await _store.SaveManyAsync(records, cancellationToken);
|
||||
public async Task SaveManyAsync(IEnumerable<WorkflowExecutionLogRecord> records, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _store.SaveManyAsync(records, Save, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Page<WorkflowExecutionLogRecord>> FindManyByWorkflowInstanceIdAsync(string workflowInstanceId, PageArgs? pageArgs = default, CancellationToken cancellationToken = default)
|
||||
|
|
@ -31,8 +48,43 @@ public class EFCoreWorkflowExecutionLogStore : IWorkflowExecutionLogStore
|
|||
x => x.Timestamp,
|
||||
OrderDirection.Ascending,
|
||||
pageArgs,
|
||||
Load,
|
||||
cancellationToken);
|
||||
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
foreach (var record in records.Items)
|
||||
{
|
||||
var entry = dbContext.Entry(record);
|
||||
var json = entry.Property<string>("PayloadData").CurrentValue;
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
record.Payload = JsonSerializer.Deserialize<object>(json);
|
||||
}
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
private WorkflowExecutionLogRecord Save(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord entity)
|
||||
{
|
||||
var options = _serializerOptionsProvider.CreatePersistenceOptions(ReferenceHandler.Preserve);
|
||||
var json = JsonSerializer.Serialize(entity.Payload, options);
|
||||
|
||||
dbContext.Entry(entity).Property("PayloadData").CurrentValue = json;
|
||||
return entity;
|
||||
}
|
||||
|
||||
private WorkflowExecutionLogRecord? Load(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord? entity)
|
||||
{
|
||||
if (entity is not null)
|
||||
{
|
||||
var json = dbContext.Entry(entity).Property<string>("PayloadData").CurrentValue;
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
entity.Payload = JsonSerializer.Deserialize<object>(json);
|
||||
}
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
using Elsa.Workflows.Core.Signals;
|
||||
|
||||
namespace Elsa.Workflows.Core.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Records Start and Completed entries of the owning activity.
|
||||
/// </summary>
|
||||
public class ExecutionLoggingBehavior : Behavior
|
||||
{
|
||||
public ExecutionLoggingBehavior(IActivity owner) : base(owner)
|
||||
{
|
||||
OnSignalReceived<ActivityCompleted>(OnActivityCompleted);
|
||||
}
|
||||
|
||||
protected override void Execute(ActivityExecutionContext context)
|
||||
{
|
||||
context.AddExecutionLogEntry("Started");
|
||||
}
|
||||
|
||||
private void OnActivityCompleted(ActivityCompleted signal, SignalContext context)
|
||||
{
|
||||
if (context.IsSelf)
|
||||
context.SenderActivityExecutionContext.AddExecutionLogEntry("Completed");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Core.Pipelines.ActivityExecution;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
using Elsa.Workflows.Core.Signals;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Workflows.Core.Middleware.Activities;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Core.Models;
|
||||
using Elsa.Workflows.Core.Pipelines.ActivityExecution;
|
||||
using Elsa.Workflows.Core.Services;
|
||||
|
||||
namespace Elsa.Workflows.Core.Middleware.Activities;
|
||||
|
||||
/// <summary>
|
||||
/// Adds extension methods to <see cref="ExecutionLogMiddleware"/>.
|
||||
/// </summary>
|
||||
public static class ExecutionLogMiddlewareExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Installs the <see cref="ExecutionLogMiddleware"/> component in the activity execution pipeline.
|
||||
/// </summary>
|
||||
public static IActivityExecutionPipelineBuilder UseExecutionLogging(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<ExecutionLogMiddleware>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An activity execution middleware component that extracts execution details as <see cref="WorkflowExecutionLogEntry"/>.
|
||||
/// </summary>
|
||||
public class ExecutionLogMiddleware : IActivityExecutionMiddleware
|
||||
{
|
||||
private readonly ActivityMiddlewareDelegate _next;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
public ExecutionLogMiddleware(ActivityMiddlewareDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask InvokeAsync(ActivityExecutionContext context)
|
||||
{
|
||||
context.AddExecutionLogEntry("Started");
|
||||
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
|
||||
var payload = new JsonObject();
|
||||
|
||||
foreach (var entry in context.JournalData)
|
||||
{
|
||||
payload[entry.Key] = entry.Value != null ? JsonSerializer.Deserialize<JsonNode>(JsonSerializer.Serialize(entry.Value)) : JsonNode.Parse("null");
|
||||
}
|
||||
|
||||
context.AddExecutionLogEntry("Completed", payload: payload);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
context.AddExecutionLogEntry("Faulted",
|
||||
payload: new
|
||||
{
|
||||
Exception = new
|
||||
{
|
||||
exception.Message,
|
||||
exception.Source,
|
||||
exception.Data,
|
||||
Type = exception.GetType()
|
||||
}
|
||||
});
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,6 @@ public abstract class Activity : IActivity, ISignalHandler
|
|||
Line = line;
|
||||
Type = ActivityTypeNameHelper.GenerateTypeName(GetType());
|
||||
Version = 1;
|
||||
Behaviors.Add<ExecutionLoggingBehavior>(this);
|
||||
Behaviors.Add<ScheduledChildCallbackBehavior>(this);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ public class ActivityExecutionPipeline : IActivityExecutionPipeline
|
|||
|
||||
private ActivityMiddlewareDelegate CreateDefaultPipeline() => Setup(x => x
|
||||
.UseExceptionHandling()
|
||||
.UseExecutionLogging()
|
||||
.UseDefaultActivityInvoker()
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue