From 6f8990def29f7958c2fb1823b5ad9d2a602c3003 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 29 Jun 2021 09:53:27 +0200 Subject: [PATCH] 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 --- .../ManyWorkflowInstanceIdSpecification.cs | 15 +++++ .../elsa-workflow-definitions-list-screen.tsx | 17 ++--- .../src/models/domain.ts | 4 ++ .../src/services/elsa-client.ts | 9 ++- .../RequiredFromQueryActionConstraint.cs | 12 ++++ .../Attributes/RequiredFromQueryAttribute.cs | 18 ++++++ .../Endpoints/WorkflowDefinitions/GetMany.cs | 63 +++++++++++++++++++ .../Elsa.Server.Api/Models/ListModel.cs | 11 ++++ 8 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 src/core/Elsa.Core/Persistence/Specifications/WorkflowDefinitions/ManyWorkflowInstanceIdSpecification.cs create mode 100644 src/server/Elsa.Server.Api/ActionConstraints/RequiredFromQueryActionConstraint.cs create mode 100644 src/server/Elsa.Server.Api/Attributes/RequiredFromQueryAttribute.cs create mode 100644 src/server/Elsa.Server.Api/Endpoints/WorkflowDefinitions/GetMany.cs create mode 100644 src/server/Elsa.Server.Api/Models/ListModel.cs diff --git a/src/core/Elsa.Core/Persistence/Specifications/WorkflowDefinitions/ManyWorkflowInstanceIdSpecification.cs b/src/core/Elsa.Core/Persistence/Specifications/WorkflowDefinitions/ManyWorkflowInstanceIdSpecification.cs new file mode 100644 index 000000000..337f2e9e2 --- /dev/null +++ b/src/core/Elsa.Core/Persistence/Specifications/WorkflowDefinitions/ManyWorkflowInstanceIdSpecification.cs @@ -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 + { + public IEnumerable Ids { get; set; } + public ManyWorkflowDefinitionIdsSpecification(IEnumerable ids) => Ids = ids; + public override Expression> ToExpression() => x => Ids.Contains(x.DefinitionId); + } +} \ No newline at end of file diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-list/elsa-workflow-definitions-screen/elsa-workflow-definitions-list-screen.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-list/elsa-workflow-definitions-screen/elsa-workflow-definitions-list-screen.tsx index 1885c7699..78edfaf35 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-list/elsa-workflow-definitions-screen/elsa-workflow-definitions-list-screen.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-list/elsa-workflow-definitions-screen/elsa-workflow-definitions-list-screen.tsx @@ -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 = {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 (
@@ -66,11 +69,9 @@ export class ElsaWorkflowDefinitionsListScreen { - {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; diff --git a/src/designer/elsa-workflows-studio/src/models/domain.ts b/src/designer/elsa-workflows-studio/src/models/domain.ts index 4210d4c34..a8652944a 100644 --- a/src/designer/elsa-workflows-studio/src/models/domain.ts +++ b/src/designer/elsa-workflows-studio/src/models/domain.ts @@ -291,6 +291,10 @@ export interface PagedList { totalCount: number; } +export interface ListModel { + items: Array; +} + export enum ActivityTraits { Action = 1, Trigger = 2, diff --git a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts index 154da7642..9a5885020 100644 --- a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts +++ b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts @@ -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>(`v1/workflow-definitions?version=${versionOptionsString}`); return response.data; }, + getMany: async (ids: Array, versionOptions?: VersionOptions) => { + const versionOptionsString = getVersionOptionsString(versionOptions); + const response = await httpClient.get>(`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(`v1/workflow-definitions/${definitionId}/${versionOptionsString}`); @@ -251,6 +256,8 @@ export interface WorkflowDefinitionsApi { list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise>; + getMany(ids: Array, versionOptions?: VersionOptions): Promise>; + getByDefinitionAndVersion(definitionId: string, versionOptions: VersionOptions): Promise; save(request: SaveWorkflowDefinitionRequest): Promise; diff --git a/src/server/Elsa.Server.Api/ActionConstraints/RequiredFromQueryActionConstraint.cs b/src/server/Elsa.Server.Api/ActionConstraints/RequiredFromQueryActionConstraint.cs new file mode 100644 index 000000000..aec9a5016 --- /dev/null +++ b/src/server/Elsa.Server.Api/ActionConstraints/RequiredFromQueryActionConstraint.cs @@ -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); + } +} \ No newline at end of file diff --git a/src/server/Elsa.Server.Api/Attributes/RequiredFromQueryAttribute.cs b/src/server/Elsa.Server.Api/Attributes/RequiredFromQueryAttribute.cs new file mode 100644 index 000000000..3a9009509 --- /dev/null +++ b/src/server/Elsa.Server.Api/Attributes/RequiredFromQueryAttribute.cs @@ -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)); + } + } + } +} \ No newline at end of file diff --git a/src/server/Elsa.Server.Api/Endpoints/WorkflowDefinitions/GetMany.cs b/src/server/Elsa.Server.Api/Endpoints/WorkflowDefinitions/GetMany.cs new file mode 100644 index 000000000..7e9f0c408 --- /dev/null +++ b/src/server/Elsa.Server.Api/Endpoints/WorkflowDefinitions/GetMany.cs @@ -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))] + [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> Handle([RequiredFromQuery] string? ids, VersionOptions? version = default, CancellationToken cancellationToken = default) + { + IList summaries = new List(); + + 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>(items); + } + + return Json(new ListModel(summaries), _serializer.GetSettings()); + } + } +} \ No newline at end of file diff --git a/src/server/Elsa.Server.Api/Models/ListModel.cs b/src/server/Elsa.Server.Api/Models/ListModel.cs new file mode 100644 index 000000000..18c2aa43a --- /dev/null +++ b/src/server/Elsa.Server.Api/Models/ListModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Elsa.Server.Api.Models +{ + public record ListModel + { + public ListModel(IEnumerable items) => Items = items.ToArray(); + public T[] Items { get; set; } + } +} \ No newline at end of file