Add API endpoint for querying workflow execution log records

This commit is contained in:
Sipke Schoorstra 2021-04-20 14:19:50 +02:00
parent 91bda57411
commit 61ccbceb5f
9 changed files with 244 additions and 8 deletions

View file

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

View file

@ -5,7 +5,7 @@
* It contains typing information for all components that exist in this project.
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { ActivityDefinitionProperty, ActivityDesignDisplayContext, ActivityModel, ActivityPropertyDescriptor, VersionOptions, WorkflowDefinition, WorkflowModel } from "./models";
import { ActivityDefinitionProperty, ActivityDescriptor, ActivityDesignDisplayContext, ActivityModel, ActivityPropertyDescriptor, VersionOptions, WorkflowDefinition, WorkflowModel } from "./models";
import { MatchResults, RouterHistory } from "@stencil/router";
import { MenuItem } from "./components/controls/elsa-context-menu/models";
import { DropdownButtonItem, DropdownButtonOrigin } from "./components/controls/elsa-dropdown-button/models";
@ -172,6 +172,12 @@ export namespace Components {
"history"?: RouterHistory;
"serverUrl": string;
}
interface ElsaWorkflowInstanceJournal {
"activityDescriptors": Array<ActivityDescriptor>;
"getServerUrl": () => Promise<string>;
"serverUrl": string;
"workflowInstanceId": string;
}
interface ElsaWorkflowInstanceViewerScreen {
"getServerUrl": () => Promise<string>;
"serverUrl": string;
@ -382,6 +388,12 @@ declare global {
prototype: HTMLElsaWorkflowDefinitionsListScreenElement;
new (): HTMLElsaWorkflowDefinitionsListScreenElement;
};
interface HTMLElsaWorkflowInstanceJournalElement extends Components.ElsaWorkflowInstanceJournal, HTMLStencilElement {
}
var HTMLElsaWorkflowInstanceJournalElement: {
prototype: HTMLElsaWorkflowInstanceJournalElement;
new (): HTMLElsaWorkflowInstanceJournalElement;
};
interface HTMLElsaWorkflowInstanceViewerScreenElement extends Components.ElsaWorkflowInstanceViewerScreen, HTMLStencilElement {
}
var HTMLElsaWorkflowInstanceViewerScreenElement: {
@ -439,6 +451,7 @@ declare global {
"elsa-workflow-definition-editor-notifications": HTMLElsaWorkflowDefinitionEditorNotificationsElement;
"elsa-workflow-definition-editor-screen": HTMLElsaWorkflowDefinitionEditorScreenElement;
"elsa-workflow-definitions-list-screen": HTMLElsaWorkflowDefinitionsListScreenElement;
"elsa-workflow-instance-journal": HTMLElsaWorkflowInstanceJournalElement;
"elsa-workflow-instance-viewer-screen": HTMLElsaWorkflowInstanceViewerScreenElement;
"elsa-workflow-instances-list-screen": HTMLElsaWorkflowInstancesListScreenElement;
"elsa-workflow-publish-button": HTMLElsaWorkflowPublishButtonElement;
@ -602,6 +615,11 @@ declare namespace LocalJSX {
"history"?: RouterHistory;
"serverUrl"?: string;
}
interface ElsaWorkflowInstanceJournal {
"activityDescriptors"?: Array<ActivityDescriptor>;
"serverUrl"?: string;
"workflowInstanceId"?: string;
}
interface ElsaWorkflowInstanceViewerScreen {
"serverUrl"?: string;
"workflowInstanceId"?: string;
@ -654,6 +672,7 @@ declare namespace LocalJSX {
"elsa-workflow-definition-editor-notifications": ElsaWorkflowDefinitionEditorNotifications;
"elsa-workflow-definition-editor-screen": ElsaWorkflowDefinitionEditorScreen;
"elsa-workflow-definitions-list-screen": ElsaWorkflowDefinitionsListScreen;
"elsa-workflow-instance-journal": ElsaWorkflowInstanceJournal;
"elsa-workflow-instance-viewer-screen": ElsaWorkflowInstanceViewerScreen;
"elsa-workflow-instances-list-screen": ElsaWorkflowInstancesListScreen;
"elsa-workflow-publish-button": ElsaWorkflowPublishButton;
@ -696,6 +715,7 @@ declare module "@stencil/core" {
"elsa-workflow-definition-editor-notifications": LocalJSX.ElsaWorkflowDefinitionEditorNotifications & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionEditorNotificationsElement>;
"elsa-workflow-definition-editor-screen": LocalJSX.ElsaWorkflowDefinitionEditorScreen & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionEditorScreenElement>;
"elsa-workflow-definitions-list-screen": LocalJSX.ElsaWorkflowDefinitionsListScreen & JSXBase.HTMLAttributes<HTMLElsaWorkflowDefinitionsListScreenElement>;
"elsa-workflow-instance-journal": LocalJSX.ElsaWorkflowInstanceJournal & JSXBase.HTMLAttributes<HTMLElsaWorkflowInstanceJournalElement>;
"elsa-workflow-instance-viewer-screen": LocalJSX.ElsaWorkflowInstanceViewerScreen & JSXBase.HTMLAttributes<HTMLElsaWorkflowInstanceViewerScreenElement>;
"elsa-workflow-instances-list-screen": LocalJSX.ElsaWorkflowInstancesListScreen & JSXBase.HTMLAttributes<HTMLElsaWorkflowInstancesListScreenElement>;
"elsa-workflow-publish-button": LocalJSX.ElsaWorkflowPublishButton & JSXBase.HTMLAttributes<HTMLElsaWorkflowPublishButtonElement>;

View file

@ -0,0 +1,120 @@
import {Component, h, Host, Method, Prop, State, Watch} from '@stencil/core';
import {
ActivityDescriptor, PagedList, WorkflowExecutionLogRecord,
} from "../../../../models";
import {createElsaClient} from "../../../../services/elsa-client";
@Component({
tag: 'elsa-workflow-instance-journal',
shadow: false,
})
export class ElsaWorkflowInstanceJournal {
@Prop() workflowInstanceId: string;
@Prop() serverUrl: string;
@Prop() activityDescriptors: Array<ActivityDescriptor> = [];
@State() records: PagedList<WorkflowExecutionLogRecord> = {items: [], totalCount: 0};
@Method()
async getServerUrl(): Promise<string> {
return this.serverUrl;
}
@Watch('workflowInstanceId')
async workflowInstanceIdChangedHandler(newValue: string) {
const workflowInstanceId = newValue;
const client = createElsaClient(this.serverUrl);
if (workflowInstanceId && workflowInstanceId.length > 0) {
try {
this.records = await client.workflowExecutionLogApi.get(workflowInstanceId);
} catch {
console.warn(`The specified workflow definition does not exist. Creating a new one.`);
}
}
}
async componentWillLoad() {
await this.workflowInstanceIdChangedHandler(this.workflowInstanceId);
}
render() {
const records = this.records;
const items = records.items;
const renderRecord = (record: WorkflowExecutionLogRecord, index: number) => {
const isLastItem = index == items.length - 1;
return (
<li>
<div class="relative pb-8">
{isLastItem ? 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" 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">Completed phone screening with <a href="#" class="font-medium text-gray-900">Martha Gardner</a></p>
</div>
<div class="text-right text-sm whitespace-nowrap text-gray-500">
<time dateTime="2020-09-28">Sep 28</time>
</div>
</div>
</div>
</div>
</li>
);
};
return (
<section class="fixed inset-0 overflow-hidden" aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
<div class="absolute inset-0 overflow-hidden">
<div class="absolute inset-0" aria-hidden="true"/>
<div class="fixed inset-y-0 right-0 pl-10 max-w-full flex sm:pl-16">
<div class="w-screen max-w-2xl">
<div class="h-full flex flex-col py-6 bg-white shadow-xl overflow-y-scroll">
<div class="px-4 sm:px-6">
<div class="flex items-start justify-between">
<h2 class="text-lg font-medium text-gray-900" id="slide-over-title">
Workflow Journal
</h2>
<div class="ml-3 h-7 flex items-center">
<button class="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<span class="sr-only">Close panel</span>
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
</div>
<div class="mt-6 relative flex-1 px-4 sm:px-6">
<div class="absolute inset-0 px-4 sm:px-6">
<div class="flow-root">
<ul class="-mb-8">
{items.map(renderRecord)}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
}

View file

@ -158,16 +158,18 @@ export class ElsaWorkflowInstanceViewerScreen {
}
render() {
const descriptors: Array<ActivityDescriptor> = state.activityDescriptors;
return (
<Host class="flex flex-col w-full" ref={el => this.el = el}>
<Host class="flex flex-col w-full relative" ref={el => this.el = el}>
{this.renderCanvas()}
<elsa-workflow-instance-journal workflowInstanceId={this.workflowInstanceId} serverUrl={this.serverUrl} activityDescriptors={descriptors}/>
</Host>
);
}
renderCanvas() {
return (
<div class="flex-1 flex relative">
<div class="flex-1 flex">
<elsa-designer-tree model={this.workflowModel} class="flex-1" ref={el => this.designer = el}/>
</div>
);

View file

@ -138,6 +138,18 @@ export interface ActivityDefinition {
properties: Array<ActivityDefinitionProperty>;
}
export interface WorkflowExecutionLogRecord {
id: string;
workflowInstanceId: string;
activityId: string;
activityType: string;
timestamp: Date;
eventName: string;
message?: string;
source?: string;
data?: any;
}
export interface ConnectionDefinition {
sourceActivityId?: string;
targetActivityId?: string;
@ -259,8 +271,8 @@ export interface ActivityPropertyDescriptor {
export interface PagedList<T> {
items: Array<T>;
page: number;
pageSize: number;
page?: number;
pageSize?: number;
totalCount: number;
}

View file

@ -10,7 +10,7 @@ import {
VersionOptions, WorkflowBlueprint, WorkflowBlueprintSummary,
WorkflowContextOptions,
WorkflowDefinition,
WorkflowDefinitionSummary, WorkflowInstance, WorkflowInstanceSummary,
WorkflowDefinitionSummary, WorkflowExecutionLogRecord, WorkflowInstance, WorkflowInstanceSummary,
WorkflowPersistenceBehavior, WorkflowStatus
} from "../models";
@ -130,6 +130,22 @@ export const createElsaClient = function (serverUrl: string): ElsaClient {
return response.data;
}
},
workflowExecutionLogApi: {
get: async (workflowInstanceId: string, page?: number, pageSize?: number): Promise<PagedList<WorkflowExecutionLogRecord>> => {
const queryString = {};
if (!!page)
queryString['page'] = page;
if (!!pageSize)
queryString['pageSize'] = pageSize;
const queryStringItems = collection.map(queryString, (v, k) => `${k}=${v}`);
const queryStringText = queryStringItems.length > 0 ? `?${queryStringItems.join('&')}` : '';
const response = await httpClient.get(`v1/workflow-instances/${workflowInstanceId}/execution-log${queryStringText}`);
return response.data;
}
},
scriptingApi: {
getJavaScriptTypeDefinitions: async (workflowDefinitionId: string, context?: string): Promise<string> => {
context = context || '';
@ -145,6 +161,7 @@ export interface ElsaClient {
workflowDefinitionsApi: WorkflowDefinitionsApi;
workflowRegistryApi: WorkflowRegistryApi;
workflowInstancesApi: WorkflowInstancesApi;
workflowExecutionLogApi: WorkflowExecutionLogApi;
scriptingApi: ScriptingApi;
}
@ -185,6 +202,12 @@ export interface WorkflowInstancesApi {
bulkDelete(request: BulkDeleteWorkflowsRequest): Promise<BulkDeleteWorkflowsResponse>;
}
export interface WorkflowExecutionLogApi {
get(workflowInstanceId: string, page?: number, pageSize?: number): Promise<PagedList<WorkflowExecutionLogRecord>>;
}
export interface BulkDeleteWorkflowsRequest {
workflowInstanceIds: Array<string>;
}

View file

@ -42,7 +42,7 @@ namespace Elsa.Server.Api.Endpoints.WebhookDefinitions
]
public async Task<ActionResult<PagedList<WorkflowDefinition>>> Handle(CancellationToken cancellationToken = default)
{
var specification = Specification<WebhookDefinition>.All;
var specification = Specification<WebhookDefinition>.Identity;
var items = await _webhookDefinitionStore.FindManyAsync(specification, cancellationToken: cancellationToken);
return Json(items, _serializer.GetSettings());

View file

@ -0,0 +1,46 @@
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.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Open.Linq.AsyncExtensions;
using Swashbuckle.AspNetCore.Annotations;
namespace Elsa.Server.Api.Endpoints.WorkflowExecutionLog
{
[ApiController]
[ApiVersion("1")]
[Route("v{apiVersion:apiVersion}/workflow-instances/{id}/execution-log")]
[Produces("application/json")]
public class Get : Controller
{
private readonly IWorkflowExecutionLogStore _workflowExecutionLogStore;
public Get(IWorkflowExecutionLogStore workflowExecutionLogStore)
{
_workflowExecutionLogStore = workflowExecutionLogStore;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PagedList<WorkflowExecutionLogRecord>))]
[SwaggerOperation(
Summary = "Returns the workflow's execution log.",
Description = "Returns the workflow's execution log.",
OperationId = "WorkflowExecutionLog.Get",
Tags = new[] { "WorkflowExecutionLog" })
]
public async Task<ActionResult<PagedList<WorkflowExecutionLogRecord>>> Handle(string id, int? page = default, int? pageSize = default, CancellationToken cancellationToken = default)
{
var specification = new WorkflowInstanceIdSpecification(id);
var totalCount = await _workflowExecutionLogStore.CountAsync(specification, cancellationToken);
var paging = page != null ? Paging.Page(page.Value, pageSize ?? 100) : default;
var orderBy = OrderBySpecification.OrderBy<WorkflowExecutionLogRecord>(x => x.Timestamp);
var records = await _workflowExecutionLogStore.FindManyAsync(specification, orderBy, paging, cancellationToken).ToList();
return new PagedList<WorkflowExecutionLogRecord>(records, page, pageSize, totalCount);
}
}
}

View file

@ -53,7 +53,7 @@ namespace Elsa.Server.Api.Endpoints.WorkflowInstances
CancellationToken cancellationToken = default)
{
_stopwatch.Restart();
var specification = Specification<WorkflowInstance>.All;
var specification = Specification<WorkflowInstance>.Identity;
if (!string.IsNullOrWhiteSpace(workflowDefinitionId))
specification = specification.WithWorkflowDefinition(workflowDefinitionId);