diff --git a/src/bundles/Elsa.WorkflowServer.Web/Program.cs b/src/bundles/Elsa.WorkflowServer.Web/Program.cs index 1616ff450..19b7d5f35 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/Program.cs +++ b/src/bundles/Elsa.WorkflowServer.Web/Program.cs @@ -45,6 +45,7 @@ services .AddActivity() .AddActivity() .AddActivity() + .AddActivity() ) .UseWorkflowPersistence(p => p.UseEntityFrameworkCore(ef => ef.UseSqlite())) .UseWorkflowApiEndpoints() diff --git a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/activity-walker.ts b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/activity-walker.ts index 1da18bd5a..5b856782b 100644 --- a/src/designer/elsa-workflows-designer/src/components/activities/flowchart/activity-walker.ts +++ b/src/designer/elsa-workflows-designer/src/components/activities/flowchart/activity-walker.ts @@ -30,11 +30,11 @@ export function flatten(root: ActivityNode): Array { } export function flattenList(activities: Array): Array { - const list: Array = [...activities]; + let list: Array = [...activities]; for (const activity of activities) { - for (const child of activity.children) - list.push(child); + const childList = flattenList(activity.children); + list = [...list, ...childList]; } return list; diff --git a/src/designer/elsa-workflows-designer/src/components/designer/workflow-instance-viewer/models.ts b/src/designer/elsa-workflows-designer/src/components/designer/workflow-instance-viewer/models.ts index a17422a4e..64f564de1 100644 --- a/src/designer/elsa-workflows-designer/src/components/designer/workflow-instance-viewer/models.ts +++ b/src/designer/elsa-workflows-designer/src/components/designer/workflow-instance-viewer/models.ts @@ -1,4 +1,5 @@ -import {Activity, ActivityDescriptor, InputDescriptor, TabDefinition, WorkflowDefinition} from "../../../models"; +import {Activity, ActivityDescriptor, InputDescriptor, TabDefinition, WorkflowDefinition, WorkflowExecutionLogRecord} from "../../../models"; +import moment from "moment"; export const WorkflowInstanceViewerEventTypes = { WorkflowDefinition: { @@ -35,3 +36,13 @@ export interface PropertiesTabModel extends TabModel { export interface WorkflowInstancePropertiesViewerModel { tabModels: Array; } + +export interface ActivityExecutionEventBlock { + activityId: string; + completed: boolean; + timestamp: Date; + duration?: moment.Duration; + startedRecord: WorkflowExecutionLogRecord; + completedRecord?: WorkflowExecutionLogRecord; + faultedRecord?: WorkflowExecutionLogRecord; +} diff --git a/src/designer/elsa-workflows-designer/src/components/designer/workflow-instance-viewer/workflow-journal.tsx b/src/designer/elsa-workflows-designer/src/components/designer/workflow-instance-viewer/workflow-journal.tsx index b01951a12..b41002c8e 100644 --- a/src/designer/elsa-workflows-designer/src/components/designer/workflow-instance-viewer/workflow-journal.tsx +++ b/src/designer/elsa-workflows-designer/src/components/designer/workflow-instance-viewer/workflow-journal.tsx @@ -2,9 +2,11 @@ import {Component, h, Prop, State, Watch} from "@stencil/core"; import {ActionDefinition, ActionType, ActivityDescriptor, ActivityMetadata, WorkflowDefinition, WorkflowExecutionLogRecord, WorkflowInstance} from "../../../models"; import {Container} from "typedi"; import {ElsaApiClientProvider} from "../../../services"; -import {formatTime, formatTimestamp, isNullOrWhitespace} from "../../../utils"; +import {durationToString, formatTime, formatTimestamp, getDuration, isNullOrWhitespace} from "../../../utils"; import {ActivityNode, flatten, walkActivities} from "../../activities/flowchart/activity-walker"; import descriptorsStore from '../../../data/descriptors-store'; +import {ActivityExecutionEventBlock} from "./models"; +import {start} from "@stencil/core/dev-server"; const PAGE_SIZE: number = 20; @@ -23,6 +25,7 @@ export class WorkflowJournal { @Prop() workflowDefinition: WorkflowDefinition; @State() activityNodes: Array = []; @State() workflowExecutionLogRecords: Array = []; + @State() activityExecutionEventBlocks: Array = []; @Watch('workflowInstance') async onWorkflowInstanceChanged(value: string) { @@ -46,7 +49,7 @@ export class WorkflowJournal { const workflowDefinition = this.workflowDefinition; const activityNodes = this.activityNodes; const activityDescriptors: Array = descriptorsStore.activityDescriptors; - const records = this.workflowExecutionLogRecords; + const eventBlocks = this.activityExecutionEventBlocks; return ( @@ -67,55 +70,42 @@ export class WorkflowJournal {
-
    - {records.map((record, index) => { - const isLastRecord = index == records.length - 1; - // const activityNode = activityNodes.find(x => x.activity.id == record.activityId); - // const activity = activityNode.activity; - // const activityDescriptor = activityDescriptors.find(x => x.activityType == activityNode.activity.typeName); - // const activityMetadata = activity.metadata; - // const activityDisplayText = isNullOrWhitespace(activityMetadata.displayText) ? activity.typeName : activityMetadata.displayText; - const activityDisplayText = 'Test'; + + + + + + + + + + + + {eventBlocks.map((eventBlock, index) => { + const activityNode = activityNodes.find(x => x.activity.id == eventBlock.activityId); + const activity = activityNode.activity; + const activityDescriptor = activityDescriptors.find(x => x.activityType == activityNode.activity.typeName); + const activityMetadata = activity.metadata; + const activityDisplayText = isNullOrWhitespace(activityMetadata.displayText) ? activity.typeName : activityMetadata.displayText; + const duration = durationToString(eventBlock.duration); + const status = eventBlock.completed ? 'Completed' : 'Started'; return ( -
  • -
    - {isLastRecord ? undefined :
    -
  • + + + + + + ); })} - + +
    TimeActivityStatusDuration
    {formatTime(eventBlock.timestamp)} +
    {activityDisplayText}
    +
    {activityDescriptor.activityType}
    +
    + {status} + {duration}
@@ -141,6 +131,29 @@ export class WorkflowJournal { const client = await this.elsaApiClientProvider.getElsaClient(); const workflowInstanceId = this.workflowInstance.id; const pageOfRecords = await client.workflowInstances.getJournal({page, pageSize: PAGE_SIZE, workflowInstanceId: workflowInstanceId}) + const blocks = this.groupRecords(pageOfRecords.items); this.workflowExecutionLogRecords = [...this.workflowExecutionLogRecords, ...pageOfRecords.items]; + this.activityExecutionEventBlocks = [...this.activityExecutionEventBlocks, ...blocks]; } + + private groupRecords = (records: Array): Array => { + const startedEvents = records.filter(x => x.eventName == 'Started'); + const completedEvents = records.filter(x => x.eventName == 'Completed'); + + const blocks: Array = startedEvents.map(startedRecord => { + const completedRecord = completedEvents.find(x => x.activityInstanceId == startedRecord.activityInstanceId); + const duration = !!completedRecord ? getDuration(completedRecord.timestamp, startedRecord.timestamp) : null; + + return { + activityId: startedRecord.activityId, + completed: !!completedRecord, + timestamp: startedRecord.timestamp, + duration: duration, + startedRecord: startedRecord, + completedRecord: completedRecord + } + }); + + return blocks; + }; } diff --git a/src/designer/elsa-workflows-designer/src/models/core.ts b/src/designer/elsa-workflows-designer/src/models/core.ts index 003f454e4..239db89d3 100644 --- a/src/designer/elsa-workflows-designer/src/models/core.ts +++ b/src/designer/elsa-workflows-designer/src/models/core.ts @@ -83,6 +83,7 @@ export interface ActivityExecutionContextState { export interface WorkflowExecutionLogRecord { id: string; + activityInstanceId: string; activityId: string; activityType: string; timestamp: Date; diff --git a/src/designer/elsa-workflows-designer/src/utils/utils.ts b/src/designer/elsa-workflows-designer/src/utils/utils.ts index 7f41ace5c..05d7179cc 100644 --- a/src/designer/elsa-workflows-designer/src/utils/utils.ts +++ b/src/designer/elsa-workflows-designer/src/utils/utils.ts @@ -11,6 +11,11 @@ export function formatTime(timestamp?: Date, defaultText?: string): string { return !!timestamp ? moment(timestamp).format('HH:mm:ss') : defaultText; } +export function getDuration(a: Date, b: Date): moment.Duration { + const diff = moment(a).diff(moment(b)); + return moment.duration(diff); +} + export function parseJson(json: string): any { if (!json) return null; @@ -82,3 +87,14 @@ export const serializeQueryString = (queryString: object): string => { }); return queryStringItems.length > 0 ? `?${queryStringItems.join('&')}` : ''; }; + +export function durationToString(duration: moment.Duration) { + return !!duration ? duration.asHours() > 1 + ? `${duration.asHours().toFixed(3)} h` + : duration.asMinutes() > 1 + ? `${duration.asMinutes().toFixed(3)} m` + : duration.asSeconds() > 1 + ? `${duration.asSeconds().toFixed(3)} s` + : `${duration.asMilliseconds()} ms` + : null; +} diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Journal/Get.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Journal/Get.cs index 183e6e45b..def6f5694 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Journal/Get.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Journal/Get.cs @@ -39,6 +39,7 @@ public class Get : Controller var models = pageOfRecords.Items.Select(x => new WorkflowExecutionLogRecordModel( x.Id, + x.ActivityInstanceId, x.ActivityId, x.ActivityType, x.Timestamp, @@ -55,6 +56,7 @@ public class Get : Controller public record WorkflowExecutionLogRecordModel( string Id, + string ActivityInstanceId, string ActivityId, string ActivityType, DateTimeOffset Timestamp, diff --git a/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220525182000_Initial.Designer.cs b/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220609162941_Initial.Designer.cs similarity index 97% rename from src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220525182000_Initial.Designer.cs rename to src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220609162941_Initial.Designer.cs index 5b1297e40..ae81f2228 100644 --- a/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220525182000_Initial.Designer.cs +++ b/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220609162941_Initial.Designer.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations { [DbContext(typeof(WorkflowsDbContext))] - [Migration("20220525182000_Initial")] + [Migration("20220609162941_Initial")] partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -153,6 +153,10 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("ActivityInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("ActivityType") .IsRequired() .HasColumnType("TEXT"); @@ -186,6 +190,9 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations b.HasIndex("ActivityId") .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityId"); + b.HasIndex("ActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityInstanceId"); + b.HasIndex("ActivityType") .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType"); diff --git a/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220525182000_Initial.cs b/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220609162941_Initial.cs similarity index 97% rename from src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220525182000_Initial.cs rename to src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220609162941_Initial.cs index 5e68289ed..386d9ad47 100644 --- a/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220525182000_Initial.cs +++ b/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/20220609162941_Initial.cs @@ -59,6 +59,7 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations Id = table.Column(type: "TEXT", nullable: false), WorkflowDefinitionId = table.Column(type: "TEXT", nullable: false), WorkflowInstanceId = table.Column(type: "TEXT", nullable: false), + ActivityInstanceId = table.Column(type: "TEXT", nullable: false), ActivityId = table.Column(type: "TEXT", nullable: false), ActivityType = table.Column(type: "TEXT", nullable: false), Timestamp = table.Column(type: "TEXT", nullable: false), @@ -172,6 +173,11 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations table: "WorkflowExecutionLogRecords", column: "ActivityId"); + migrationBuilder.CreateIndex( + name: "IX_WorkflowExecutionLogRecord_ActivityInstanceId", + table: "WorkflowExecutionLogRecords", + column: "ActivityInstanceId"); + migrationBuilder.CreateIndex( name: "IX_WorkflowExecutionLogRecord_ActivityType", table: "WorkflowExecutionLogRecords", diff --git a/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/WorkflowsDbContextModelSnapshot.cs b/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/WorkflowsDbContextModelSnapshot.cs index f6f1d3185..1042d019e 100644 --- a/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/WorkflowsDbContextModelSnapshot.cs +++ b/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite/Migrations/WorkflowsDbContextModelSnapshot.cs @@ -151,6 +151,10 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("ActivityInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("ActivityType") .IsRequired() .HasColumnType("TEXT"); @@ -184,6 +188,9 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations b.HasIndex("ActivityId") .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityId"); + b.HasIndex("ActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityInstanceId"); + b.HasIndex("ActivityType") .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType"); diff --git a/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore/EntityConfiguration/WorkflowExecutionLogRecordConfiguration.cs b/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore/EntityConfiguration/WorkflowExecutionLogRecordConfiguration.cs index 8e9f27699..575e45f9b 100644 --- a/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore/EntityConfiguration/WorkflowExecutionLogRecordConfiguration.cs +++ b/src/modules/Elsa.Workflows.Persistence.EntityFrameworkCore/EntityConfiguration/WorkflowExecutionLogRecordConfiguration.cs @@ -12,6 +12,7 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.EntityConfiguration builder.Property("PayloadData"); builder.HasIndex(x => x.Timestamp).HasDatabaseName($"IX_{nameof(WorkflowExecutionLogRecord)}_{nameof(WorkflowExecutionLogRecord.Timestamp)}"); + builder.HasIndex(x => x.ActivityInstanceId).HasDatabaseName($"IX_{nameof(WorkflowExecutionLogRecord)}_{nameof(WorkflowExecutionLogRecord.ActivityInstanceId)}"); builder.HasIndex(x => x.ActivityId).HasDatabaseName($"IX_{nameof(WorkflowExecutionLogRecord)}_{nameof(WorkflowExecutionLogRecord.ActivityId)}"); builder.HasIndex(x => x.ActivityType).HasDatabaseName($"IX_{nameof(WorkflowExecutionLogRecord)}_{nameof(WorkflowExecutionLogRecord.ActivityType)}"); builder.HasIndex(x => x.EventName).HasDatabaseName($"IX_{nameof(WorkflowExecutionLogRecord)}_{nameof(WorkflowExecutionLogRecord.EventName)}"); diff --git a/src/modules/Elsa.Workflows.Persistence/Entities/WorkflowExecutionLogRecord.cs b/src/modules/Elsa.Workflows.Persistence/Entities/WorkflowExecutionLogRecord.cs index b288934e0..eac20cd22 100644 --- a/src/modules/Elsa.Workflows.Persistence/Entities/WorkflowExecutionLogRecord.cs +++ b/src/modules/Elsa.Workflows.Persistence/Entities/WorkflowExecutionLogRecord.cs @@ -6,6 +6,7 @@ namespace Elsa.Workflows.Persistence.Entities { public string WorkflowDefinitionId { get; set; } = default!; public string WorkflowInstanceId { get; set; } = default!; + public string ActivityInstanceId { get; set; } = default!; public string ActivityId { get; set; } = default!; public string ActivityType { get; set; } = default!; public DateTimeOffset Timestamp { get; set; } diff --git a/src/modules/Elsa.Workflows.Runtime/Middleware/PersistWorkflowExecutionLogMiddleware.cs b/src/modules/Elsa.Workflows.Runtime/Middleware/PersistWorkflowExecutionLogMiddleware.cs index 824a921c2..8ee914116 100644 --- a/src/modules/Elsa.Workflows.Runtime/Middleware/PersistWorkflowExecutionLogMiddleware.cs +++ b/src/modules/Elsa.Workflows.Runtime/Middleware/PersistWorkflowExecutionLogMiddleware.cs @@ -30,6 +30,7 @@ public class PersistWorkflowExecutionLogMiddleware : WorkflowExecutionMiddleware var entries = context.ExecutionLog.Select(x => new WorkflowExecutionLogRecord { Id = _identityGenerator.GenerateId(), + ActivityInstanceId = x.ActivityInstanceId, ActivityId = x.ActivityId, ActivityType = x.ActivityType, Message = x.Message,