Incremental work on journal
This commit is contained in:
parent
bde6bc20cb
commit
2cfd9abd15
|
|
@ -45,6 +45,7 @@ services
|
|||
.AddActivity<ForEach>()
|
||||
.AddActivity<Switch>()
|
||||
.AddActivity<RunJavaScript>()
|
||||
.AddActivity<Event>()
|
||||
)
|
||||
.UseWorkflowPersistence(p => p.UseEntityFrameworkCore(ef => ef.UseSqlite()))
|
||||
.UseWorkflowApiEndpoints()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ export interface ActivityExecutionContextState {
|
|||
|
||||
export interface WorkflowExecutionLogRecord {
|
||||
id: string;
|
||||
activityInstanceId: string;
|
||||
activityId: string;
|
||||
activityType: string;
|
||||
timestamp: Date;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
@ -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",
|
||||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}");
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue