Implement activity stats

This commit is contained in:
Sipke Schoorstra 2021-05-03 12:56:52 +02:00
parent 9b5bad71c8
commit 08f639b9e0
12 changed files with 481 additions and 319 deletions

View file

@ -0,0 +1,13 @@
using System;
using System.Linq.Expressions;
using Elsa.Models;
namespace Elsa.Persistence.Specifications.WorkflowExecutionLogRecords
{
public class ActivityIdSpecification : Specification<WorkflowExecutionLogRecord>
{
public string ActivityId { get; set; }
public ActivityIdSpecification(string activityId) => ActivityId = activityId;
public override Expression<Func<WorkflowExecutionLogRecord, bool>> ToExpression() => x => x.ActivityId == ActivityId;
}
}

View file

@ -497,7 +497,7 @@ export class ElsaWorkflowDesigner {
<p>${activity.displayName}</p>
</div>
<div class="context-menu-button-container">
${this.activityContextMenuButton}
${!!this.activityContextMenuButton ? this.activityContextMenuButton : ''}
</div>
</div>
</div>

View file

@ -6,20 +6,6 @@ export enum WorkflowDesignerMode {
Blueprint
}
interface AggregatedActivityEvent {
eventName: string;
count: number;
}
export interface ActivityStats {
fault?: WorkflowFault;
averageExecutionTime: number;
fastestExecutionTime: number;
slowestExecutionTime: number;
lastExecutedAt: Date;
events: Array<AggregatedActivityEvent>;
}
export interface ActivityContextMenuState {
shown: boolean;
x: number;

View file

@ -97,12 +97,11 @@ export class ElsaWorkflowBlueprintViewerScreen {
updateModels(workflowBlueprint: WorkflowBlueprint) {
this.workflowBlueprint = workflowBlueprint;
this.workflowModel = this.mapWorkflowModel(workflowBlueprint);
debugger;
}
mapWorkflowModel(workflowBlueprint: WorkflowBlueprint): WorkflowModel {
return {
activities: workflowBlueprint.activities.filter(x => x.parentId == null).map(this.mapActivityModel),
activities: workflowBlueprint.activities.filter(x => x.parentId == workflowBlueprint.id).map(this.mapActivityModel),
connections: workflowBlueprint.connections.map(this.mapConnectionModel),
persistenceBehavior: workflowBlueprint.persistenceBehavior,
};

View file

@ -8,6 +8,7 @@ import {
} from "../../../../models";
import {activityIconProvider} from "../../../../services/activity-icon-provider";
import {createElsaClient} from "../../../../services/elsa-client";
import {durationToString} from "../../../../utils/utils";
interface Tab {
id: string;
@ -95,7 +96,7 @@ export class ElsaWorkflowInstanceJournal {
selectActivityRecordInternal(record?: WorkflowExecutionLogRecord) {
const activity = !!record ? this.workflowBlueprint.activities.find(x => x.id === record.activityId) : null;
this.selectedRecordId = !!record ? record.id : null;
this.selectedActivityId = activity != null ? activity.parentId != null ? activity.parentId : activity.id : null;
this.selectedActivityId = activity != null ? activity.parentId != this.workflowBlueprint.id ? activity.parentId : activity.id : null;
}
getEventColor(eventName: string) {
@ -264,14 +265,7 @@ export class ElsaWorkflowInstanceJournal {
filteredRecordData[key] = valueText;
}
const deltaTimeText = !!deltaTime ? deltaTime.asHours() > 1
? `${deltaTime.asHours()} h`
: deltaTime.asMinutes() > 1
? `${deltaTime.asMinutes()} m`
: deltaTime.asSeconds() > 1
? `${deltaTime.asSeconds()} s`
: `${deltaTime.asMilliseconds()} ms`
: null;
const deltaTimeText = durationToString(deltaTime);
return (
<li>

View file

@ -13,11 +13,15 @@ import {
WorkflowPersistenceBehavior,
WorkflowStatus
} from "../../../../models";
import {createElsaClient} from "../../../../services/elsa-client";
import {ActivityStats, createElsaClient} from "../../../../services/elsa-client";
import {pluginManager} from '../../../../services/plugin-manager';
import state from '../../../../utils/store';
import {ActivityContextMenuState, WorkflowDesignerMode} from "../../../designers/tree/elsa-designer-tree/models";
import {registerClickOutside} from "stencil-click-outside";
import {editor} from "monaco-editor";
import create = editor.create;
import moment from "moment";
import {durationToString} from "../../../../utils/utils";
@Component({
tag: 'elsa-workflow-instance-viewer-screen',
@ -35,6 +39,7 @@ export class ElsaWorkflowInstanceViewerScreen {
@State() workflowBlueprint: WorkflowBlueprint;
@State() workflowModel: WorkflowModel;
@State() selectedActivityId?: string;
@State() activityStats?: ActivityStats;
@State() activityContextMenuState: ActivityContextMenuState = {
shown: false,
@ -131,7 +136,7 @@ export class ElsaWorkflowInstanceViewerScreen {
mapWorkflowModel(workflowBlueprint: WorkflowBlueprint): WorkflowModel {
return {
activities: workflowBlueprint.activities.filter(x => x.parentId == null).map(this.mapActivityModel),
activities: workflowBlueprint.activities.filter(x => x.parentId == workflowBlueprint.id).map(this.mapActivityModel),
connections: workflowBlueprint.connections.map(this.mapConnectionModel),
persistenceBehavior: workflowBlueprint.persistenceBehavior,
};
@ -184,20 +189,28 @@ export class ElsaWorkflowInstanceViewerScreen {
this.selectedActivityId = activity != null ? activity.parentId != null ? activity.parentId : activity.id : null;
}
onActivitySelected(e: CustomEvent<ActivityModel>) {
async onActivitySelected(e: CustomEvent<ActivityModel>) {
this.selectedActivityId = e.detail.activityId;
this.journal.selectActivityRecord(this.selectedActivityId);
await this.journal.selectActivityRecord(this.selectedActivityId);
}
onActivityDeselected(e: CustomEvent<ActivityModel>) {
async onActivityDeselected(e: CustomEvent<ActivityModel>) {
if (this.selectedActivityId == e.detail.activityId)
this.selectedActivityId = null;
this.journal.selectActivityRecord(this.selectedActivityId);
await this.journal.selectActivityRecord(this.selectedActivityId);
}
onActivityContextMenuButtonClicked(e: CustomEvent<ActivityContextMenuState>) {
async onActivityContextMenuButtonClicked(e: CustomEvent<ActivityContextMenuState>) {
this.activityContextMenuState = e.detail;
this.activityStats = null;
if (!e.detail.shown) {
return;
}
const elsaClient = createElsaClient(this.serverUrl);
this.activityStats = await elsaClient.activityStatsApi.get(this.workflowInstanceId, e.detail.activity.activityId);
}
render() {
@ -247,6 +260,132 @@ export class ElsaWorkflowInstanceViewerScreen {
}
renderActivityPerformanceMenu() {
const activityStats: ActivityStats = this.activityStats;
const renderStats = function () {
return (
<div>
<div>
<table class="min-w-full divide-y divide-gray-200 border-b border-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Event
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Count
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{activityStats.eventCounts.map(eventCount => (
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{eventCount.eventName}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{eventCount.count}
</td>
</tr>))}
</tbody>
</table>
</div>
<div class="relative grid gap-6 bg-white px-5 py-6 sm:gap-8 sm:p-8">
{!!activityStats.fault ? (
<a href="#" class="-m-3 p-3 flex items-start rounded-lg hover:bg-gray-50 transition ease-in-out duration-150">
<svg class="flex-shrink-0 h-6 w-6 text-red-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
<div class="ml-4">
<p class="text-base font-medium text-gray-900">
Fault
</p>
<p class="mt-1 text-sm text-gray-500">
{activityStats.fault.message}
</p>
</div>
</a>) : undefined}
<a href="#" class="-m-3 p-3 flex items-start rounded-lg hover:bg-gray-50 transition ease-in-out duration-150">
<svg class="flex-shrink-0 h-6 w-6 text-indigo-500" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="12" r="9"/>
<polyline points="12 7 12 12 15 15"/>
</svg>
<div class="ml-4">
<p class="text-base font-medium text-gray-900">
Average Execution Time
</p>
<p class="mt-1 text-sm text-gray-500">
{durationToString(moment.duration(activityStats.averageExecutionTime))}
</p>
</div>
</a>
<a href="#" class="-m-3 p-3 flex items-start rounded-lg hover:bg-gray-50 transition ease-in-out duration-150">
<svg class="flex-shrink-0 h-6 w-6 text-green-500" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="12" r="9"/>
<polyline points="12 7 12 12 15 15"/>
</svg>
<div class="ml-4">
<p class="text-base font-medium text-gray-900">
Fastest Execution Time
</p>
<p class="mt-1 text-sm text-gray-500">
{durationToString(moment.duration(activityStats.fastestExecutionTime))}
</p>
</div>
</a>
<a href="#" class="-m-3 p-3 flex items-start rounded-lg hover:bg-gray-50 transition ease-in-out duration-150">
<svg class="flex-shrink-0 h-6 w-6 text-yellow-500" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="12" r="9"/>
<polyline points="12 7 12 12 15 15"/>
</svg>
<div class="ml-4">
<p class="text-base font-medium text-gray-900">
Slowest Execution Time
</p>
<p class="mt-1 text-sm text-gray-500">
{durationToString(moment.duration(activityStats.slowestExecutionTime))}
</p>
</div>
</a>
<a href="#" class="-m-3 p-3 flex items-start rounded-lg hover:bg-gray-50 transition ease-in-out duration-150">
<svg class="flex-shrink-0 h-6 w-6 text-blue-600" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<rect x="4" y="5" width="16" height="16" rx="2"/>
<line x1="16" y1="3" x2="16" y2="7"/>
<line x1="8" y1="3" x2="8" y2="7"/>
<line x1="4" y1="11" x2="20" y2="11"/>
<line x1="11" y1="15" x2="12" y2="15"/>
<line x1="12" y1="15" x2="12" y2="18"/>
</svg>
<div class="ml-4">
<p class="text-base font-medium text-gray-900">
Last Executed At
</p>
<p class="mt-1 text-sm text-gray-500">
{moment(activityStats.lastExecutedAt).format('DD-MM-YYYY HH:mm:ss')}
</p>
</div>
</a>
</div>
</div>
)
};
const renderLoader = function () {
return <div>Loading...</div>;
};
return <div
data-transition-enter="transition ease-out duration-100"
data-transition-enter-start="transform opacity-0 scale-95"
@ -263,125 +402,8 @@ export class ElsaWorkflowInstanceViewerScreen {
}
>
<div class="rounded-lg shadow-lg ring-1 ring-black ring-opacity-5 overflow-hidden">
{!!activityStats ? renderStats() : renderLoader()}
<div>
<table class="min-w-full divide-y divide-gray-200 border-b border-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Event
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Count
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
Executing
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
1
</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
Executed
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
1
</td>
</tr>
</tbody>
</table>
</div>
<div class="relative grid gap-6 bg-white px-5 py-6 sm:gap-8 sm:p-8">
<a href="#" class="-m-3 p-3 flex items-start rounded-lg hover:bg-gray-50 transition ease-in-out duration-150">
<svg class="flex-shrink-0 h-6 w-6 text-red-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
<div class="ml-4">
<p class="text-base font-medium text-gray-900">
Fault
</p>
<p class="mt-1 text-sm text-gray-500">
Get all of your questions answered in our forums or contact support.
</p>
</div>
</a>
<a href="#" class="-m-3 p-3 flex items-start rounded-lg hover:bg-gray-50 transition ease-in-out duration-150">
<svg class="flex-shrink-0 h-6 w-6 text-indigo-500" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="12" r="9"/>
<polyline points="12 7 12 12 15 15"/>
</svg>
<div class="ml-4">
<p class="text-base font-medium text-gray-900">
Average Execution Time
</p>
<p class="mt-1 text-sm text-gray-500">
100ms
</p>
</div>
</a>
<a href="#" class="-m-3 p-3 flex items-start rounded-lg hover:bg-gray-50 transition ease-in-out duration-150">
<svg class="flex-shrink-0 h-6 w-6 text-green-500" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="12" r="9"/>
<polyline points="12 7 12 12 15 15"/>
</svg>
<div class="ml-4">
<p class="text-base font-medium text-gray-900">
Fastest Execution Time
</p>
<p class="mt-1 text-sm text-gray-500">
10ms
</p>
</div>
</a>
<a href="#" class="-m-3 p-3 flex items-start rounded-lg hover:bg-gray-50 transition ease-in-out duration-150">
<svg class="flex-shrink-0 h-6 w-6 text-yellow-500" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<circle cx="12" cy="12" r="9"/>
<polyline points="12 7 12 12 15 15"/>
</svg>
<div class="ml-4">
<p class="text-base font-medium text-gray-900">
Slowest Execution Time
</p>
<p class="mt-1 text-sm text-gray-500">
200ms
</p>
</div>
</a>
<a href="#" class="-m-3 p-3 flex items-start rounded-lg hover:bg-gray-50 transition ease-in-out duration-150">
<svg class="flex-shrink-0 h-6 w-6 text-blue-600" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<rect x="4" y="5" width="16" height="16" rx="2"/>
<line x1="16" y1="3" x2="16" y2="7"/>
<line x1="8" y1="3" x2="8" y2="7"/>
<line x1="4" y1="11" x2="20" y2="11"/>
<line x1="11" y1="15" x2="12" y2="15"/>
<line x1="12" y1="15" x2="12" y2="18"/>
</svg>
<div class="ml-4">
<p class="text-base font-medium text-gray-900">
Last Executed At
</p>
<p class="mt-1 text-sm text-gray-500">
10-05-2021 15:09:10
</p>
</div>
</a>
</div>
</div>
</div>
}

View file

@ -10,7 +10,7 @@ import {
VersionOptions, WorkflowBlueprint, WorkflowBlueprintSummary,
WorkflowContextOptions,
WorkflowDefinition,
WorkflowDefinitionSummary, WorkflowExecutionLogRecord, WorkflowInstance, WorkflowInstanceSummary,
WorkflowDefinitionSummary, WorkflowExecutionLogRecord, WorkflowFault, WorkflowInstance, WorkflowInstanceSummary,
WorkflowPersistenceBehavior, WorkflowStatus
} from "../models";
@ -153,13 +153,19 @@ export const createElsaClient = function (serverUrl: string): ElsaClient {
return response.data;
}
},
designerApi:{
runtimeSelectItemsApi:{
designerApi: {
runtimeSelectItemsApi: {
get: async (providerTypeName: string, context?: any): Promise<Array<SelectListItem>> => {
const response = await httpClient.post('v1/designer/runtime-select-list-items', { providerTypeName: providerTypeName, context: context });
return response.data;
const response = await httpClient.post('v1/designer/runtime-select-list-items', {providerTypeName: providerTypeName, context: context});
return response.data;
}
}
},
activityStatsApi: {
get: async (workflowInstanceId: string, activityId?: any): Promise<ActivityStats> => {
const response = await httpClient.get(`v1/workflow-instances/${workflowInstanceId}/activity-stats/${activityId}`);
return response.data;
}
}
}
}
@ -172,6 +178,7 @@ export interface ElsaClient {
workflowExecutionLogApi: WorkflowExecutionLogApi;
scriptingApi: ScriptingApi;
designerApi: DesignerApi;
activityStatsApi: ActivityStatsApi;
}
export interface ActivitiesApi {
@ -214,7 +221,7 @@ export interface WorkflowInstancesApi {
export interface WorkflowExecutionLogApi {
get(workflowInstanceId: string, page?: number, pageSize?: number): Promise<PagedList<WorkflowExecutionLogRecord>>;
}
export interface BulkDeleteWorkflowsRequest {
@ -237,6 +244,10 @@ export interface RuntimeSelectItemsApi {
get(providerTypeName: string, context?: any): Promise<Array<SelectListItem>>
}
export interface ActivityStatsApi {
get(workflowInstanceId: string, activityId: string): Promise<ActivityStats>;
}
export interface SaveWorkflowDefinitionRequest {
workflowDefinitionId?: string;
name?: string;
@ -257,3 +268,21 @@ export interface ExportWorkflowResponse {
fileName: string;
data: Blob;
}
export interface ActivityStats {
fault?: ActivityFault;
averageExecutionTime: string;
fastestExecutionTime: string;
slowestExecutionTime: string;
lastExecutedAt: Date;
eventCounts: Array<ActivityEventCount>;
}
interface ActivityEventCount {
eventName: string;
count: number;
}
interface ActivityFault{
message: string;
}

View file

@ -1,246 +1,259 @@
import {ActivityDefinition, ActivityDefinitionProperty, ActivityModel, ConnectionModel, WorkflowModel} from "../models";
import * as collection from 'lodash/collection';
import {Duration} from "moment";
declare global {
interface Array<T> {
distinct(): Array<T>;
last(): T;
}
}
export type Map<T> = {
[key: string]: T
};
export function format(first: string, middle: string, last: string): string {
return (first || '') + (middle ? ` ${middle}` : '') + (last ? ` ${last}` : '');
}
export interface Array<T> {
interface Array<T> {
distinct(): Array<T>;
last(): T;
}
}
find<S extends T>(predicate: (this: void, value: T, index: number, obj: T[]) => value is S, thisArg?: any): S | undefined;
export type Map<T> = {
[key: string]: T
};
find(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T | undefined;
export function format(first: string, middle: string, last: string): string {
return (first || '') + (middle ? ` ${middle}` : '') + (last ? ` ${last}` : '');
}
push(...items: T[]): number;
export interface Array<T> {
distinct(): Array<T>;
last(): T;
find<S extends T>(predicate: (this: void, value: T, index: number, obj: T[]) => value is S, thisArg?: any): S | undefined;
find(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T | undefined;
push(...items: T[]): number;
}
Array.prototype.distinct = function () {
return [...new Set(this)];
return [...new Set(this)];
}
if (!Array.prototype.last) {
Array.prototype.last = function () {
return this[this.length - 1];
};
Array.prototype.last = function () {
return this[this.length - 1];
};
}
export function isNumeric(str: string): boolean {
return !isNaN(str as any) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
!isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail
return !isNaN(str as any) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
!isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail
}
export function getChildActivities(workflowModel: WorkflowModel, parentId?: string) {
if (parentId == null) {
const targetIds = new Set(workflowModel.connections.map(x => x.targetId));
return workflowModel.activities.filter(x => !targetIds.has(x.activityId));
} else {
const targetIds = new Set(workflowModel.connections.filter(x => x.sourceId === parentId).map(x => x.targetId));
return workflowModel.activities.filter(x => targetIds.has(x.activityId));
}
if (parentId == null) {
const targetIds = new Set(workflowModel.connections.map(x => x.targetId));
return workflowModel.activities.filter(x => !targetIds.has(x.activityId));
} else {
const targetIds = new Set(workflowModel.connections.filter(x => x.sourceId === parentId).map(x => x.targetId));
return workflowModel.activities.filter(x => targetIds.has(x.activityId));
}
}
export function getInboundConnections(workflowModel: WorkflowModel, activityId: string) {
return workflowModel.connections.filter(x => x.targetId === activityId);
return workflowModel.connections.filter(x => x.targetId === activityId);
}
export function getOutboundConnections(workflowModel: WorkflowModel, activityId: string) {
return workflowModel.connections.filter(x => x.sourceId === activityId);
return workflowModel.connections.filter(x => x.sourceId === activityId);
}
export function removeActivity(workflowModel: WorkflowModel, activityId: string): WorkflowModel {
const inboundConnections = getInboundConnections(workflowModel, activityId);
const outboundConnections = getOutboundConnections(workflowModel, activityId);
const connectionsToRemove = [...inboundConnections, ...outboundConnections];
const inboundConnections = getInboundConnections(workflowModel, activityId);
const outboundConnections = getOutboundConnections(workflowModel, activityId);
const connectionsToRemove = [...inboundConnections, ...outboundConnections];
return {
...workflowModel,
activities: workflowModel.activities.filter(x => x.activityId != activityId),
connections: workflowModel.connections.filter(x => connectionsToRemove.indexOf(x) < 0)
};
return {
...workflowModel,
activities: workflowModel.activities.filter(x => x.activityId != activityId),
connections: workflowModel.connections.filter(x => connectionsToRemove.indexOf(x) < 0)
};
}
export function removeConnection(workflowModel: WorkflowModel, sourceId: string, outcome: string): WorkflowModel {
return {
...workflowModel,
connections: workflowModel.connections.filter(x => !(x.sourceId === sourceId && x.outcome === outcome))
};
return {
...workflowModel,
connections: workflowModel.connections.filter(x => !(x.sourceId === sourceId && x.outcome === outcome))
};
}
export function findActivity(workflowModel: WorkflowModel, activityId: string) {
return workflowModel.activities.find(x => x.activityId === activityId);
return workflowModel.activities.find(x => x.activityId === activityId);
}
export function addConnection(workflowModel: WorkflowModel, connection: ConnectionModel);
export function addConnection(workflowModel: WorkflowModel, sourceId: string, targetId: string, outcome: string);
export function addConnection(workflowModel: WorkflowModel, ...args: any) {
const connection = typeof (args) == 'object' ? args as ConnectionModel : {sourceId: args[0], targetId: args[1], outcome: args[3]};
const connection = typeof (args) == 'object' ? args as ConnectionModel : {sourceId: args[0], targetId: args[1], outcome: args[3]};
return {
...workflowModel,
connections: [...workflowModel.connections, connection]
};
return {
...workflowModel,
connections: [...workflowModel.connections, connection]
};
}
export function setActivityDefinitionProperty(activityDefinition: ActivityDefinition, name: string, expression: string, syntax: string) {
setProperty(activityDefinition.properties, name, expression, syntax);
setProperty(activityDefinition.properties, name, expression, syntax);
}
export function setActivityModelProperty(activityModel: ActivityModel, name: string, expression: string, syntax: string) {
setProperty(activityModel.properties, name, expression, syntax);
setProperty(activityModel.properties, name, expression, syntax);
}
export function setProperty(properties: Array<ActivityDefinitionProperty>, name: string, expression: string, syntax?: string) {
let property: ActivityDefinitionProperty = properties.find(x => x.name == name);
let property: ActivityDefinitionProperty = properties.find(x => x.name == name);
if (!syntax)
syntax = 'Literal';
if (!syntax)
syntax = 'Literal';
if (!property) {
const expressions = {};
expressions[syntax] = expression;
property = {name: name, expressions: expressions, syntax: syntax};
properties.push(property);
} else {
property.expressions[syntax] = expression;
property.syntax = syntax;
}
if (!property) {
const expressions = {};
expressions[syntax] = expression;
property = {name: name, expressions: expressions, syntax: syntax};
properties.push(property);
} else {
property.expressions[syntax] = expression;
property.syntax = syntax;
}
}
export function getOrCreateProperty(activity: ActivityModel, name: string, defaultExpression?: () => string, defaultSyntax?: () => string): ActivityDefinitionProperty {
let property: ActivityDefinitionProperty = activity.properties.find(x => x.name == name);
let property: ActivityDefinitionProperty = activity.properties.find(x => x.name == name);
if (!property) {
const expressions = {};
let syntax = defaultSyntax ? defaultSyntax() : undefined;
if (!property) {
const expressions = {};
let syntax = defaultSyntax ? defaultSyntax() : undefined;
if (!syntax)
syntax = 'Literal';
if (!syntax)
syntax = 'Literal';
expressions[syntax] = defaultExpression ? defaultExpression() : undefined;
property = {name: name, expressions: expressions, syntax: null};
activity.properties.push(property);
}
expressions[syntax] = defaultExpression ? defaultExpression() : undefined;
property = {name: name, expressions: expressions, syntax: null};
activity.properties.push(property);
}
return property;
return property;
}
export function parseJson(json: string): any {
if(!json)
return null;
try {
return JSON.parse(json);
} catch (e) {
console.warn(`Error parsing JSON: ${e}`);
}
return undefined;
if (!json)
return null;
try {
return JSON.parse(json);
} catch (e) {
console.warn(`Error parsing JSON: ${e}`);
}
return undefined;
}
export function parseQuery(queryString?: string): any {
if (!queryString)
return {};
if (!queryString)
return {};
const query = {};
const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i].split('=');
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
}
return query;
const query = {};
const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i].split('=');
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
}
return query;
}
export function queryToString(query: any): string {
const q = query || {};
return collection.map(q, (v, k) => `${k}=${v}`).join('&');
const q = query || {};
return collection.map(q, (v, k) => `${k}=${v}`).join('&');
}
export function mapSyntaxToLanguage(syntax: string): any {
switch (syntax) {
case 'Json':
return 'json';
case 'JavaScript':
return 'javascript';
case 'Liquid':
return 'handlebars';
case 'Literal':
default:
return 'plaintext';
}
switch (syntax) {
case 'Json':
return 'json';
case 'JavaScript':
return 'javascript';
case 'Liquid':
return 'handlebars';
case 'Literal':
default:
return 'plaintext';
}
}
export function durationToString(duration: Duration) {
return !!duration ? duration.asHours() > 1
? `${duration.asHours()} h`
: duration.asMinutes() > 1
? `${duration.asMinutes()} m`
: duration.asSeconds() > 1
? `${duration.asSeconds()} s`
: `${duration.asMilliseconds()} ms`
: null;
}
// TODO: Replace with momentJS.
export function timeSince(time) {
switch (typeof time) {
case 'number':
break;
case 'string':
time = +new Date(time);
break;
case 'object':
if (time.constructor === Date) time = time.getTime();
break;
default:
time = +new Date();
}
const time_formats = [
[60, 'seconds', 1], // 60
[120, '1 minute ago', '1 minute from now'], // 60*2
[3600, 'minutes', 60], // 60*60, 60
[7200, '1 hour ago', '1 hour from now'], // 60*60*2
[86400, 'hours', 3600], // 60*60*24, 60*60
[172800, 'Yesterday', 'Tomorrow'], // 60*60*24*2
[604800, 'days', 86400], // 60*60*24*7, 60*60*24
[1209600, 'Last week', 'Next week'], // 60*60*24*7*4*2
[2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7
[4838400, 'Last month', 'Next month'], // 60*60*24*7*4*2
[29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4
[58060800, 'Last year', 'Next year'], // 60*60*24*7*4*12*2
[2903040000, 'years', 29030400], // 60*60*24*7*4*12*100, 60*60*24*7*4*12
[5806080000, 'Last century', 'Next century'], // 60*60*24*7*4*12*100*2
[58060800000, 'centuries', 2903040000] // 60*60*24*7*4*12*100*20, 60*60*24*7*4*12*100
];
let seconds = (+new Date() - time) / 1000,
token = 'ago',
list_choice = 1;
switch (typeof time) {
case 'number':
break;
case 'string':
time = +new Date(time);
break;
case 'object':
if (time.constructor === Date) time = time.getTime();
break;
default:
time = +new Date();
}
const time_formats = [
[60, 'seconds', 1], // 60
[120, '1 minute ago', '1 minute from now'], // 60*2
[3600, 'minutes', 60], // 60*60, 60
[7200, '1 hour ago', '1 hour from now'], // 60*60*2
[86400, 'hours', 3600], // 60*60*24, 60*60
[172800, 'Yesterday', 'Tomorrow'], // 60*60*24*2
[604800, 'days', 86400], // 60*60*24*7, 60*60*24
[1209600, 'Last week', 'Next week'], // 60*60*24*7*4*2
[2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7
[4838400, 'Last month', 'Next month'], // 60*60*24*7*4*2
[29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4
[58060800, 'Last year', 'Next year'], // 60*60*24*7*4*12*2
[2903040000, 'years', 29030400], // 60*60*24*7*4*12*100, 60*60*24*7*4*12
[5806080000, 'Last century', 'Next century'], // 60*60*24*7*4*12*100*2
[58060800000, 'centuries', 2903040000] // 60*60*24*7*4*12*100*20, 60*60*24*7*4*12*100
];
let seconds = (+new Date() - time) / 1000,
token = 'ago',
list_choice = 1;
if (seconds === 0) {
return 'Just now';
}
if (seconds < 0) {
seconds = Math.abs(seconds);
token = 'from now';
list_choice = 2;
}
let i = 0,
format;
if (seconds === 0) {
return 'Just now';
}
if (seconds < 0) {
seconds = Math.abs(seconds);
token = 'from now';
list_choice = 2;
}
let i = 0,
format;
// tslint:disable-next-line: no-conditional-assignment
while (format = time_formats[i++]) {
if (seconds < format[0]) {
if (typeof format[2] === 'string') {
return format[list_choice];
} else {
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token;
}
}
// tslint:disable-next-line: no-conditional-assignment
while (format = time_formats[i++]) {
if (seconds < format[0]) {
if (typeof format[2] === 'string') {
return format[list_choice];
} else {
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token;
}
}
return time;
}
return time;
}

View file

@ -10,7 +10,7 @@ namespace Elsa.Samples.Server.Host.Workflows
{
builder
.WithWorkflowDefinitionId("HelloWorld")
.WithVersion(1)
.WithVersion(1, false, false)
.WithDisplayName("Hello World!")
.HttpEndpoint("/hello-world")
.WriteHttpResponse(HttpStatusCode.OK, "Hello World!", "text/plain");

View file

@ -10,7 +10,7 @@ namespace Elsa.Samples.Server.Host.Workflows
{
builder
.WithWorkflowDefinitionId("HelloWorld")
.WithVersion(2)
.WithVersion(2, true, true)
.WithDisplayName("Hello World!")
.HttpEndpoint("/hello-world/v2")
.WriteHttpResponse(HttpStatusCode.OK, "Hello World V2!", "text/plain");

View file

@ -0,0 +1,87 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Models;
using Elsa.Persistence;
using Elsa.Persistence.Specifications;
using Elsa.Persistence.Specifications.WorkflowExecutionLogRecords;
using Elsa.Server.Api.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using NodaTime;
using Open.Linq.AsyncExtensions;
using Swashbuckle.AspNetCore.Annotations;
namespace Elsa.Server.Api.Endpoints.ActivityStats
{
[ApiController]
[ApiVersion("1")]
[Route("v{apiVersion:apiVersion}/workflow-instances/{workflowInstanceId}/activity-stats/{activityId}")]
[Produces("application/json")]
public class Get : Controller
{
private readonly IWorkflowExecutionLogStore _workflowExecutionLogStore;
private readonly IEndpointContentSerializerSettingsProvider _serializerSettingsProvider;
public Get(IWorkflowExecutionLogStore workflowExecutionLogStore, IEndpointContentSerializerSettingsProvider serializerSettingsProvider)
{
_workflowExecutionLogStore = workflowExecutionLogStore;
_serializerSettingsProvider = serializerSettingsProvider;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ActivityStats))]
[SwaggerOperation(
Summary = "Returns an aggregated view of activity events and execution statistics.",
Description = "Returns an aggregated view of activity events and execution statistics.",
OperationId = "ActivityStats.Get",
Tags = new[] { "ActivityStats" })
]
public async Task<ActionResult<ActivityStats>> Handle(string workflowInstanceId, string activityId, CancellationToken cancellationToken = default)
{
var specification = new WorkflowInstanceIdSpecification(workflowInstanceId).And(new ActivityIdSpecification(activityId));
var orderBy = OrderBySpecification.OrderBy<WorkflowExecutionLogRecord>(x => x.Timestamp);
var records = await _workflowExecutionLogStore.FindManyAsync(specification, orderBy, null, cancellationToken).ToList();
var eventCounts = records.GroupBy(x => x.EventName);
var executions = GetExecutions(records).ToList();
var executionTimes = executions.Select(x => x.Duration).OrderBy(x => x).ToList();
var faultRecord = records.FirstOrDefault(x => x.EventName == "Faulted");
var activityFault = faultRecord != null ? new ActivityFault(faultRecord.Message!) : null;
var model = new ActivityStats
{
EventCounts = eventCounts.Select(x => new ActivityEventCount(x.Key!, x.Count())).ToList(),
LastExecutedAt = executions.Select(x => x.Timestamp).OrderByDescending(x => x).FirstOrDefault(),
SlowestExecutionTime = executionTimes.FirstOrDefault(),
FastestExecutionTime = executionTimes.LastOrDefault(),
AverageExecutionTime = Duration.FromTicks(executionTimes.Average(x => x.TotalTicks)),
Fault = activityFault
};
return Json(model, _serializerSettingsProvider.GetSettings());
}
private IEnumerable<ActivityExecutionStat> GetExecutions(List<WorkflowExecutionLogRecord> records)
{
var filteredRecords = records.Where(x => x.EventName is "Executing" or "Executed").OrderBy(x => x.Timestamp).ToList();
if(!filteredRecords.Any())
yield break;
var index = 0;
foreach (var @record in filteredRecords)
{
if (@record.EventName == "Executed")
{
var previousRecord = filteredRecords.ElementAt(index - 1);
var duration = @record.Timestamp - previousRecord.Timestamp;
yield return new ActivityExecutionStat(@record.Timestamp, duration);
}
index++;
}
}
}
}

View file

@ -0,0 +1,19 @@
using System.Collections.Generic;
using NodaTime;
namespace Elsa.Server.Api.Endpoints.ActivityStats
{
public record ActivityStats
{
public IList<ActivityEventCount> EventCounts { get; set; } = default!;
public ActivityFault? Fault { get; set; }
public Duration? AverageExecutionTime { get; set; }
public Duration? FastestExecutionTime { get; set; }
public Duration? SlowestExecutionTime { get; set; }
public Instant? LastExecutedAt { get; set; }
}
public record ActivityFault(string Message);
public record ActivityEventCount(string EventName, int Count);
internal record ActivityExecutionStat(Instant Timestamp, Duration Duration);
}