Incremental work on journal

This commit is contained in:
Sipke Schoorstra 2022-06-09 18:48:43 +02:00
parent bde6bc20cb
commit 2cfd9abd15
13 changed files with 119 additions and 52 deletions

View file

@ -45,6 +45,7 @@ services
.AddActivity<ForEach>()
.AddActivity<Switch>()
.AddActivity<RunJavaScript>()
.AddActivity<Event>()
)
.UseWorkflowPersistence(p => p.UseEntityFrameworkCore(ef => ef.UseSqlite()))
.UseWorkflowApiEndpoints()

View file

@ -30,11 +30,11 @@ export function flatten(root: ActivityNode): Array<ActivityNode> {
}
export function flattenList(activities: Array<ActivityNode>): Array<ActivityNode> {
const list: Array<ActivityNode> = [...activities];
let list: Array<ActivityNode> = [...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;

View file

@ -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<TabModel>;
}
export interface ActivityExecutionEventBlock {
activityId: string;
completed: boolean;
timestamp: Date;
duration?: moment.Duration;
startedRecord: WorkflowExecutionLogRecord;
completedRecord?: WorkflowExecutionLogRecord;
faultedRecord?: WorkflowExecutionLogRecord;
}

View file

@ -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<ActivityNode> = [];
@State() workflowExecutionLogRecords: Array<WorkflowExecutionLogRecord> = [];
@State() activityExecutionEventBlocks: Array<ActivityExecutionEventBlock> = [];
@Watch('workflowInstance')
async onWorkflowInstanceChanged(value: string) {
@ -46,7 +49,7 @@ export class WorkflowJournal {
const workflowDefinition = this.workflowDefinition;
const activityNodes = this.activityNodes;
const activityDescriptors: Array<ActivityDescriptor> = descriptorsStore.activityDescriptors;
const records = this.workflowExecutionLogRecords;
const eventBlocks = this.activityExecutionEventBlocks;
return (
@ -67,55 +70,42 @@ export class WorkflowJournal {
<div class="flex-1 relative">
<div class="absolute inset-0 overflow-y-scroll">
<ul role="list" class="m-4">
{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';
<table>
<thead>
<tr>
<th>Time</th>
<th>Activity</th>
<th>Status</th>
<th>Duration</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-100">
{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 (
<li>
<div class="relative pb-8">
{isLastRecord ? undefined : <span class="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200" aria-hidden="true"/>}
<div class="relative flex space-x-3">
<div>
<span class="h-8 w-8 rounded-full bg-green-500 flex items-center justify-center ring-8 ring-white">
<svg class="h-5 w-5 text-white" x-description="Heroicon name: solid/check" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
</span>
</div>
<div class="min-w-0 flex-1 pt-1.5 flex justify-between space-x-4">
<div>
<p class="text-sm text-gray-500">{record.activityId}</p>
<div class="mt-2 text-sm text-gray-700">
<p>
{activityDisplayText}
</p>
</div>
</div>
<div class="justify-self-end">
<a href="#" class="relative inline-flex items-center rounded-full 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 bg-rose-500" aria-hidden="true"/>
</span>
<span class="ml-3.5 font-medium text-gray-900">{record.eventName}</span>
</a>
</div>
<div class="text-right text-sm whitespace-nowrap text-gray-500">
<time dateTime="2020-09-20">{formatTime(record.timestamp)}</time>
</div>
</div>
</div>
</div>
</li>
<tr>
<td>{formatTime(eventBlock.timestamp)}</td>
<td>
<div>{activityDisplayText}</div>
<div class="font-bold">{activityDescriptor.activityType}</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>
</td>
<td>{duration}</td>
</tr>
);
})}
</ul>
</tbody>
</table>
</div>
</div>
</div>
@ -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<WorkflowExecutionLogRecord>): Array<ActivityExecutionEventBlock> => {
const startedEvents = records.filter(x => x.eventName == 'Started');
const completedEvents = records.filter(x => x.eventName == 'Completed');
const blocks: Array<ActivityExecutionEventBlock> = 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;
};
}

View file

@ -83,6 +83,7 @@ export interface ActivityExecutionContextState {
export interface WorkflowExecutionLogRecord {
id: string;
activityInstanceId: string;
activityId: string;
activityType: string;
timestamp: Date;

View file

@ -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;
}

View file

@ -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,

View file

@ -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<string>("ActivityInstanceId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("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");

View file

@ -59,6 +59,7 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations
Id = table.Column<string>(type: "TEXT", nullable: false),
WorkflowDefinitionId = table.Column<string>(type: "TEXT", nullable: false),
WorkflowInstanceId = table.Column<string>(type: "TEXT", nullable: false),
ActivityInstanceId = table.Column<string>(type: "TEXT", nullable: false),
ActivityId = table.Column<string>(type: "TEXT", nullable: false),
ActivityType = table.Column<string>(type: "TEXT", nullable: false),
Timestamp = table.Column<string>(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",

View file

@ -151,6 +151,10 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite.Migrations
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ActivityInstanceId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("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");

View file

@ -12,6 +12,7 @@ namespace Elsa.Workflows.Persistence.EntityFrameworkCore.EntityConfiguration
builder.Property<string>("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)}");

View file

@ -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; }

View file

@ -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,