Add API endpoint for querying workflow execution log records
This commit is contained in:
parent
91bda57411
commit
61ccbceb5f
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue