Optimize workflow definitions loading

Improves the Workflow Definition list view by instead of loading ALL versions of a page of workflow definitions, it instead only loads the LATEST and their PUBLISHED versions only.
Over time, a workflow definition can have many versions, which would decrease page load performance over time
This commit is contained in:
Sipke Schoorstra 2021-06-29 09:53:27 +02:00
parent 40b53b24a6
commit 6f8990def2
8 changed files with 140 additions and 9 deletions

View file

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Elsa.Models;
namespace Elsa.Persistence.Specifications.WorkflowDefinitions
{
public class ManyWorkflowDefinitionIdsSpecification : Specification<WorkflowDefinition>
{
public IEnumerable<string> Ids { get; set; }
public ManyWorkflowDefinitionIdsSpecification(IEnumerable<string> ids) => Ids = ids;
public override Expression<Func<WorkflowDefinition, bool>> ToExpression() => x => Ids.Contains(x.DefinitionId);
}
}

View file

@ -1,5 +1,4 @@
import {Component, h, Prop, State} from '@stencil/core';
import * as collection from 'lodash/collection';
import {createElsaClient} from "../../../../services/elsa-client";
import {PagedList, VersionOptions, WorkflowDefinitionSummary} from "../../../../models";
import {RouterHistory} from "@stencil/router";
@ -12,6 +11,7 @@ export class ElsaWorkflowDefinitionsListScreen {
@Prop() history?: RouterHistory;
@Prop() serverUrl: string;
@State() workflowDefinitions: PagedList<WorkflowDefinitionSummary> = {items: [], page: 1, pageSize: 50, totalCount: 0};
@State() publishedWorkflowDefinitions: WorkflowDefinitionSummary[] = [];
confirmDialog: HTMLElsaConfirmDialogElement;
@ -34,8 +34,12 @@ export class ElsaWorkflowDefinitionsListScreen {
const elsaClient = this.createClient();
const page = 0;
const pageSize = 50;
const versionOptions: VersionOptions = {allVersions: true};
this.workflowDefinitions = await elsaClient.workflowDefinitionsApi.list(page, pageSize, versionOptions);
const latestVersionOptions: VersionOptions = {isLatest: true};
const publishedVersionOptions: VersionOptions = {isPublished: true};
const latestWorkflowDefinitions = await elsaClient.workflowDefinitionsApi.list(page, pageSize, latestVersionOptions);
const unpublishedWorkflowDefinitionIds = latestWorkflowDefinitions.items.filter(x => !x.isPublished).map(x => x.definitionId);
this.publishedWorkflowDefinitions = await elsaClient.workflowDefinitionsApi.getMany(unpublishedWorkflowDefinitionIds, publishedVersionOptions);
this.workflowDefinitions = latestWorkflowDefinitions;
}
createClient() {
@ -44,7 +48,6 @@ export class ElsaWorkflowDefinitionsListScreen {
render() {
const workflowDefinitions = this.workflowDefinitions.items;
const groupings = collection.groupBy(workflowDefinitions, 'definitionId');
return (
<div>
@ -66,11 +69,9 @@ export class ElsaWorkflowDefinitionsListScreen {
</tr>
</thead>
<tbody class="elsa-bg-white elsa-divide-y elsa-divide-gray-100">
{collection.map(groupings, group => {
const versions = collection.orderBy(group, 'version', 'desc');
const workflowDefinition: WorkflowDefinitionSummary = versions[0];
{workflowDefinitions.map(workflowDefinition => {
const latestVersionNumber = workflowDefinition.version;
const publishedVersion: WorkflowDefinitionSummary = versions.find(x => x.isPublished);
const publishedVersion: WorkflowDefinitionSummary = workflowDefinition.isPublished ? workflowDefinition : this.publishedWorkflowDefinitions.find(x => x.definitionId == workflowDefinition.definitionId);
const publishedVersionNumber = !!publishedVersion ? publishedVersion.version : '-';
let workflowDisplayName = workflowDefinition.displayName;

View file

@ -291,6 +291,10 @@ export interface PagedList<T> {
totalCount: number;
}
export interface ListModel<T> {
items: Array<T>;
}
export enum ActivityTraits {
Action = 1,
Trigger = 2,

View file

@ -7,7 +7,7 @@ import {
ActivityDescriptor,
ConnectionDefinition,
EventTypes,
getVersionOptionsString,
getVersionOptionsString, ListModel,
OrderBy,
PagedList,
SelectListItem,
@ -58,6 +58,11 @@ export const createElsaClient = function (serverUrl: string): ElsaClient {
const response = await httpClient.get<PagedList<WorkflowDefinitionSummary>>(`v1/workflow-definitions?version=${versionOptionsString}`);
return response.data;
},
getMany: async (ids: Array<string>, versionOptions?: VersionOptions) => {
const versionOptionsString = getVersionOptionsString(versionOptions);
const response = await httpClient.get<ListModel<WorkflowDefinitionSummary>>(`v1/workflow-definitions?ids=${ids.join(',')}&version=${versionOptionsString}`);
return response.data.items;
},
getByDefinitionAndVersion: async (definitionId: string, versionOptions: VersionOptions) => {
const versionOptionsString = getVersionOptionsString(versionOptions);
const response = await httpClient.get<WorkflowDefinition>(`v1/workflow-definitions/${definitionId}/${versionOptionsString}`);
@ -251,6 +256,8 @@ export interface WorkflowDefinitionsApi {
list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise<PagedList<WorkflowDefinitionSummary>>;
getMany(ids: Array<string>, versionOptions?: VersionOptions): Promise<Array<WorkflowDefinitionSummary>>;
getByDefinitionAndVersion(definitionId: string, versionOptions: VersionOptions): Promise<WorkflowDefinition>;
save(request: SaveWorkflowDefinitionRequest): Promise<WorkflowDefinition>;

View file

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc.ActionConstraints;
namespace Elsa.Server.Api.ActionConstraints
{
public class RequiredFromQueryActionConstraint : IActionConstraint
{
private readonly string _parameter;
public RequiredFromQueryActionConstraint(string parameter) => _parameter = parameter;
public int Order => 999;
public bool Accept(ActionConstraintContext context) => context.RouteContext.HttpContext.Request.Query.ContainsKey(_parameter);
}
}

View file

@ -0,0 +1,18 @@
using System.Linq;
using Elsa.Server.Api.ActionConstraints;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
namespace Elsa.Server.Api.Attributes
{
public class RequiredFromQueryAttribute : FromQueryAttribute, IParameterModelConvention
{
public void Apply(ParameterModel parameter)
{
if (parameter.Action.Selectors != null && parameter.Action.Selectors.Any())
{
parameter.Action.Selectors.Last().ActionConstraints.Add(new RequiredFromQueryActionConstraint(parameter.BindingInfo?.BinderModelName ?? parameter.ParameterName));
}
}
}
}

View file

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using Elsa.Models;
using Elsa.Persistence;
using Elsa.Persistence.Specifications;
using Elsa.Persistence.Specifications.WorkflowDefinitions;
using Elsa.Serialization;
using Elsa.Server.Api.Attributes;
using Elsa.Server.Api.Models;
using Elsa.Server.Api.Swagger.Examples;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
using Swashbuckle.AspNetCore.Filters;
namespace Elsa.Server.Api.Endpoints.WorkflowDefinitions
{
[ApiController]
[ApiVersion("1")]
[Route("v{apiVersion:apiVersion}/workflow-definitions")]
[Produces("application/json")]
public class GetMany : Controller
{
private readonly IWorkflowDefinitionStore _workflowDefinitionStore;
private readonly IContentSerializer _serializer;
private readonly IMapper _mapper;
public GetMany(IWorkflowDefinitionStore workflowDefinitionStore, IContentSerializer serializer, IMapper mapper)
{
_workflowDefinitionStore = workflowDefinitionStore;
_serializer = serializer;
_mapper = mapper;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ListModel<WorkflowDefinitionSummaryModel>))]
[SwaggerResponseExample(StatusCodes.Status200OK, typeof(WorkflowDefinitionPagedListExample))]
[SwaggerOperation(
Summary = "Returns a list of workflow definitions for the specified ids.",
Description = "Returns a list of workflow definition summaries. When no version options are specified, the latest versions are returned.",
OperationId = "WorkflowDefinitions.GetMany",
Tags = new[] { "WorkflowDefinitions" })
]
public async Task<ActionResult<WorkflowDefinitionSummaryModel[]>> Handle([RequiredFromQuery] string? ids, VersionOptions? version = default, CancellationToken cancellationToken = default)
{
IList<WorkflowDefinitionSummaryModel> summaries = new List<WorkflowDefinitionSummaryModel>();
if (!string.IsNullOrWhiteSpace(ids))
{
version ??= VersionOptions.Latest;
var splitIds = ids.Split(',', StringSplitOptions.RemoveEmptyEntries);
var specification = new VersionOptionsSpecification(version.Value).And(new ManyWorkflowDefinitionIdsSpecification(splitIds));
var items = await _workflowDefinitionStore.FindManyAsync(specification, cancellationToken: cancellationToken);
summaries = _mapper.Map<IList<WorkflowDefinitionSummaryModel>>(items);
}
return Json(new ListModel<WorkflowDefinitionSummaryModel>(summaries), _serializer.GetSettings());
}
}
}

View file

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Linq;
namespace Elsa.Server.Api.Models
{
public record ListModel<T>
{
public ListModel(IEnumerable<T> items) => Items = items.ToArray();
public T[] Items { get; set; }
}
}