Implement activity stats
This commit is contained in:
parent
9b5bad71c8
commit
08f639b9e0
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
87
src/server/Elsa.Server.Api/Endpoints/ActivityStats/Get.cs
Normal file
87
src/server/Elsa.Server.Api/Endpoints/ActivityStats/Get.cs
Normal 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/server/Elsa.Server.Api/Endpoints/ActivityStats/Models.cs
Normal file
19
src/server/Elsa.Server.Api/Endpoints/ActivityStats/Models.cs
Normal 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);
|
||||
}
|
||||
Loading…
Reference in a new issue