From 56f70ea9e23ca886b40f1aa026431786e5d4e799 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Mon, 20 Dec 2021 16:33:07 +0100 Subject: [PATCH] Add server features API Closes #2588 --- .../elsa-workflows-studio/src/components.d.ts | 2 + .../elsa-studio-root/elsa-studio-root.tsx | 6 ++ .../elsa-designer-tree/elsa-designer-tree.tsx | 1 - ...elsa-workflow-definition-editor-screen.tsx | 73 ++++++++++++------- .../src/data/dashboard.tsx | 4 +- .../src/data/workflow-editor.tsx | 4 +- .../src/models/services.ts | 1 + .../src/services/elsa-client.ts | 17 +++++ .../Extensions/ServiceCollectionExtensions.cs | 3 +- .../WorkflowTestingServerFeatureProvider.cs | 11 +++ .../Elsa.Samples.Server.Host/Startup.cs | 6 +- .../Endpoints/Features/List.cs | 57 +++++++++++++++ .../Services/IServerFeatureProvider.cs | 10 +++ 13 files changed, 160 insertions(+), 35 deletions(-) create mode 100644 src/modules/workflowtesting/Elsa.WorkflowTesting.Api/Services/WorkflowTestingServerFeatureProvider.cs create mode 100644 src/server/Elsa.Server.Api/Endpoints/Features/List.cs create mode 100644 src/server/Elsa.Server.Api/Services/IServerFeatureProvider.cs diff --git a/src/designer/elsa-workflows-studio/src/components.d.ts b/src/designer/elsa-workflows-studio/src/components.d.ts index a59e4ba7f..d0f71320a 100644 --- a/src/designer/elsa-workflows-studio/src/components.d.ts +++ b/src/designer/elsa-workflows-studio/src/components.d.ts @@ -290,6 +290,7 @@ export namespace Components { "history": RouterHistory; "importWorkflow": (file: File) => Promise; "monacoLibPath": string; + "serverFeatures": Array; "serverUrl": string; "workflowDefinitionId": string; } @@ -1055,6 +1056,7 @@ declare namespace LocalJSX { "history"?: RouterHistory; "monacoLibPath"?: string; "onWorkflowSaved"?: (event: CustomEvent) => void; + "serverFeatures"?: Array; "serverUrl"?: string; "workflowDefinitionId"?: string; } diff --git a/src/designer/elsa-workflows-studio/src/components/dashboard/pages/elsa-studio-root/elsa-studio-root.tsx b/src/designer/elsa-workflows-studio/src/components/dashboard/pages/elsa-studio-root/elsa-studio-root.tsx index f7b4d11ed..6ec80fe31 100644 --- a/src/designer/elsa-workflows-studio/src/components/dashboard/pages/elsa-studio-root/elsa-studio-root.tsx +++ b/src/designer/elsa-workflows-studio/src/components/dashboard/pages/elsa-studio-root/elsa-studio-root.tsx @@ -68,10 +68,13 @@ export class ElsaStudioRoot { const elsaClientFactory: () => Promise = () => createElsaClient(this.serverUrl); const httpClientFactory: () => Promise = () => createHttpClient(this.serverUrl); + const elsaClient = await elsaClientFactory(); + const elsaStudio: ElsaStudio = this.elsaStudio = { serverUrl: this.serverUrl, basePath: this.basePath, featuresString: this.featuresString, + serverFeatures: [], eventBus, pluginManager, propertyDisplayManager, @@ -88,6 +91,8 @@ export class ElsaStudioRoot { await eventBus.emit(EventTypes.Root.Initializing); pluginManager.initialize(elsaStudio); propertyDisplayManager.initialize(elsaStudio); + + elsaStudio.serverFeatures = await elsaClient.featuresApi.list(); } async componentDidLoad() { @@ -107,6 +112,7 @@ export class ElsaStudioRoot { const tunnelState: DashboardState = { serverUrl: this.serverUrl, basePath: this.basePath, + serverFeatures: this.elsaStudio.serverFeatures, culture, monacoLibPath: this.monacoLibPath }; diff --git a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx index a15638a38..43cbcbbe0 100644 --- a/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx +++ b/src/designer/elsa-workflows-studio/src/components/designers/tree/elsa-designer-tree/elsa-designer-tree.tsx @@ -61,7 +61,6 @@ export class ElsaWorkflowDesigner { @Event() activityContextMenuButtonTestClicked: EventEmitter; @State() workflowModel: WorkflowModel; - @State() activityContextMenuState: ActivityContextMenuState = { shown: false, x: 0, diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx index 90464e639..2f587f4cc 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-definition-editor/elsa-workflow-definition-editor-screen/elsa-workflow-definition-editor-screen.tsx @@ -46,6 +46,7 @@ export class ElsaWorkflowDefinitionEditorScreen { @Prop({attribute: 'monaco-lib-path', reflect: true}) monacoLibPath: string; @Prop() culture: string; @Prop() basePath: string; + @Prop() serverFeatures: Array = []; @Prop() history: RouterHistory; @State() workflowDefinition: WorkflowDefinition; @State() workflowModel: WorkflowModel; @@ -93,6 +94,7 @@ export class ElsaWorkflowDefinitionEditorScreen { helpDialog: HTMLElsaModalDialogElement; activityContextMenu: HTMLDivElement; componentCustomButton: HTMLDivElement; + //connectionContextMenu: HTMLDivElement; @Method() @@ -179,7 +181,7 @@ export class ElsaWorkflowDefinitionEditorScreen { } @Listen('click', {target: 'window'}) - onWindowClicked(event: Event){ + onWindowClicked(event: Event) { const target = event.target as HTMLElement; if (!this.componentCustomButton.contains(target)) @@ -243,7 +245,7 @@ export class ElsaWorkflowDefinitionEditorScreen { state.workflowStorageDescriptors = await client.workflowStorageProvidersApi.list(); } - async tryUpdateActivityInformation(activityId: string){ + async tryUpdateActivityInformation(activityId: string) { if (!this.workflowInstanceId) { this.activityStats = null; this.workflowInstance = null; @@ -511,8 +513,7 @@ export class ElsaWorkflowDefinitionEditorScreen { this.workflowInstanceId = message.workflowInstanceId; this.workflowTestActivityMessages = this.workflowTestActivityMessages.filter(x => x.activityId !== message.activityId); this.workflowTestActivityMessages = [...this.workflowTestActivityMessages, message]; - } - else{ + } else { this.workflowTestActivityMessages = []; this.workflowInstanceId = null; } @@ -520,7 +521,7 @@ export class ElsaWorkflowDefinitionEditorScreen { this.render(); }; - async onRestartActivityButtonClick(){ + async onRestartActivityButtonClick() { await eventBus.emit(EventTypes.WorkflowRestarted, this, this.selectedActivityId); this.handleContextMenuTestChange(0, 0, false, null); @@ -543,13 +544,13 @@ export class ElsaWorkflowDefinitionEditorScreen { onUpdateActivity = (activity: ActivityModel) => { const message = this.workflowTestActivityMessages.find(x => x.activityId === activity.activityId); - if (message){ + if (message) { message.status = WorkflowTestActivityMessageStatus.Modified; this.clearSubsequentWorkflowTestMessages(activity.activityId); } } - private clearSubsequentWorkflowTestMessages(activityId: string){ + private clearSubsequentWorkflowTestMessages(activityId: string) { const targetActivityId = this.workflowDefinition.connections.find(x => x.sourceActivityId === activityId)?.targetActivityId; if (!targetActivityId) return; @@ -567,8 +568,7 @@ export class ElsaWorkflowDefinitionEditorScreen { let icon: string; - switch (testActivityMessage.status) - { + switch (testActivityMessage.status) { case WorkflowTestActivityMessageStatus.Done: icon = ` @@ -612,7 +612,8 @@ export class ElsaWorkflowDefinitionEditorScreen { render() { const tunnelState: WorkflowEditorState = { serverUrl: this.serverUrl, - workflowDefinitionId: this.workflowDefinition.definitionId + workflowDefinitionId: this.workflowDefinition.definitionId, + serverFeatures: this.serverFeatures }; return ( @@ -643,7 +644,7 @@ export class ElsaWorkflowDefinitionEditorScreen { `; return ( -
+
- +
); } - const renderPerformanceStats = () =>{ + const renderPerformanceStats = () => { if (!!message.error) return; return (
- +
); }; @@ -737,7 +739,7 @@ export class ElsaWorkflowDefinitionEditorScreen { return ( @@ -819,7 +821,7 @@ export class ElsaWorkflowDefinitionEditorScreen { const renderComponentCustomButton = () => { if (this.configureComponentCustomButtonContext.data == null) - return; + return; const label = this.configureComponentCustomButtonContext.data.label; @@ -846,7 +848,10 @@ export class ElsaWorkflowDefinitionEditorScreen { data-transition-leave-start="elsa-transform elsa-opacity-100 elsa-scale-100" data-transition-leave-end="elsa-transform elsa-opacity-0 elsa-scale-95" class={`${this.activityContextMenuTestState.shown ? '' : 'hidden'} elsa-absolute elsa-z-10 elsa-mt-3 elsa-px-2 elsa-w-screen elsa-max-w-xl sm:elsa-px-0`} - style={{left: `${this.activityContextMenuTestState.x + 64}px`, top: `${this.activityContextMenuTestState.y - 256}px`}} + style={{ + left: `${this.activityContextMenuTestState.x + 64}px`, + top: `${this.activityContextMenuTestState.y - 256}px` + }} ref={el => this.componentCustomButton = el} >
@@ -869,7 +874,7 @@ export class ElsaWorkflowDefinitionEditorScreen { data-transition-leave-end="elsa-transform elsa-opacity-0 elsa-scale-95" class={`${this.activityContextMenuState.shown ? '' : 'hidden'} context-menu elsa-z-10 elsa-mx-3 elsa-w-48 elsa-mt-1 elsa-rounded-md elsa-shadow-lg elsa-fixed`} style={{left: `${this.activityContextMenuState.x}px`, top: `${this.activityContextMenuState.y}px`}} - ref={el => this.activityContextMenu = el } + ref={el => this.activityContextMenu = el} >
Connect outcomes to existing activity
-
Press and hold SHIFT while LEFT-clicking the outcome to connect. Release SHIFT and LEFT-click the target activity.
+
Press and hold SHIFT while LEFT-clicking the outcome to connect. Release SHIFT and LEFT-click the target activity.
Pan
@@ -1054,17 +1060,28 @@ export class ElsaWorkflowDefinitionEditorScreen { workflowDefinition={this.workflowDefinition} /> - Test - - - + {this.renderTestPanel()} ); } + + private renderTestPanel() { + const testingEnabled = this.serverFeatures.find(x => x == 'WorkflowTesting'); + + if (!testingEnabled) + return; + + return [ + Test, + + + + ]; + } } injectHistory(ElsaWorkflowDefinitionEditorScreen); -DashboardTunnel.injectProps(ElsaWorkflowDefinitionEditorScreen, ['serverUrl', 'culture', 'monacoLibPath', 'basePath']); +DashboardTunnel.injectProps(ElsaWorkflowDefinitionEditorScreen, ['serverUrl', 'culture', 'monacoLibPath', 'basePath', 'serverFeatures']); diff --git a/src/designer/elsa-workflows-studio/src/data/dashboard.tsx b/src/designer/elsa-workflows-studio/src/data/dashboard.tsx index dcf629397..65f068f2e 100644 --- a/src/designer/elsa-workflows-studio/src/data/dashboard.tsx +++ b/src/designer/elsa-workflows-studio/src/data/dashboard.tsx @@ -6,6 +6,7 @@ export interface DashboardState { basePath: string; culture: string; monacoLibPath: string; + serverFeatures: Array; } export default createProviderConsumer( @@ -13,7 +14,8 @@ export default createProviderConsumer( serverUrl: null, basePath: null, culture: null, - monacoLibPath: null + monacoLibPath: null, + serverFeatures: [] }, (subscribe, child) => () ); diff --git a/src/designer/elsa-workflows-studio/src/data/workflow-editor.tsx b/src/designer/elsa-workflows-studio/src/data/workflow-editor.tsx index cca87c00c..6ecd055b8 100644 --- a/src/designer/elsa-workflows-studio/src/data/workflow-editor.tsx +++ b/src/designer/elsa-workflows-studio/src/data/workflow-editor.tsx @@ -4,12 +4,14 @@ import {h} from "@stencil/core"; export interface WorkflowEditorState { workflowDefinitionId: string; serverUrl: string; + serverFeatures: Array; } export default createProviderConsumer( { workflowDefinitionId: null, - serverUrl: null + serverUrl: null, + serverFeatures: [] }, (subscribe, child) => () ); diff --git a/src/designer/elsa-workflows-studio/src/models/services.ts b/src/designer/elsa-workflows-studio/src/models/services.ts index c3c1ddc37..4d79dfa70 100644 --- a/src/designer/elsa-workflows-studio/src/models/services.ts +++ b/src/designer/elsa-workflows-studio/src/models/services.ts @@ -9,6 +9,7 @@ export interface ElsaStudio { serverUrl: string; basePath: string; featuresString: string; + serverFeatures: Array; pluginManager: PluginManager; propertyDisplayManager: PropertyDisplayManager; elsaClientFactory: () => Promise; 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 1e06239e5..c93f974d2 100644 --- a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts +++ b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts @@ -244,6 +244,12 @@ export const createElsaClient = async function (serverUrl: string): Promise { + const response = await httpClient.get('v1/features'); + return response.data.features; + } + }, } return _elsaClient; @@ -261,12 +267,17 @@ export interface ElsaClient { workflowStorageProvidersApi: WorkflowStorageProvidersApi; workflowChannelsApi: WorkflowChannelsApi; workflowTestApi: WorkflowTestApi; + featuresApi: FeaturesApi; } export interface ActivitiesApi { list(): Promise>; } +export interface FeaturesApi { + list(): Promise>; +} + export interface WorkflowDefinitionsApi { list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise>; @@ -291,7 +302,9 @@ export interface WorkflowDefinitionsApi { export interface WorkflowTestApi { execute(request: WorkflowTestExecuteRequest): Promise; + restartFromActivity(request: WorkflowTestRestartFromActivityRequest): Promise; + stop(request: WorkflowTestStopRequest): Promise; } @@ -418,3 +431,7 @@ interface ActivityEventCount { interface ActivityFault { message: string; } + +interface FeaturesModel { + features: Array; +} diff --git a/src/modules/workflowtesting/Elsa.WorkflowTesting.Api/Extensions/ServiceCollectionExtensions.cs b/src/modules/workflowtesting/Elsa.WorkflowTesting.Api/Extensions/ServiceCollectionExtensions.cs index 6fd72a78b..1675dc547 100644 --- a/src/modules/workflowtesting/Elsa.WorkflowTesting.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/modules/workflowtesting/Elsa.WorkflowTesting.Api/Extensions/ServiceCollectionExtensions.cs @@ -13,7 +13,8 @@ namespace Elsa.WorkflowTesting.Api.Extensions return services .AddScoped() .AddScoped() - .AddNotificationHandlersFrom(); + .AddNotificationHandlersFrom() + .AddTransient(); } } } \ No newline at end of file diff --git a/src/modules/workflowtesting/Elsa.WorkflowTesting.Api/Services/WorkflowTestingServerFeatureProvider.cs b/src/modules/workflowtesting/Elsa.WorkflowTesting.Api/Services/WorkflowTestingServerFeatureProvider.cs new file mode 100644 index 000000000..3d914a79c --- /dev/null +++ b/src/modules/workflowtesting/Elsa.WorkflowTesting.Api/Services/WorkflowTestingServerFeatureProvider.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Server.Api.Services; + +namespace Elsa.WorkflowTesting.Api.Services; + +public class WorkflowTestingServerFeatureProvider : IServerFeatureProvider +{ + public ValueTask> GetFeaturesAsync(CancellationToken cancellationToken) => ValueTask.FromResult>(new[] { "WorkflowTesting" }); +} \ No newline at end of file diff --git a/src/samples/server/Elsa.Samples.Server.Host/Startup.cs b/src/samples/server/Elsa.Samples.Server.Host/Startup.cs index 7380bb84d..3f89dcdd1 100644 --- a/src/samples/server/Elsa.Samples.Server.Host/Startup.cs +++ b/src/samples/server/Elsa.Samples.Server.Host/Startup.cs @@ -95,8 +95,8 @@ namespace Elsa.Samples.Server.Host // In a production environment, make sure to allow only origins you trust. services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("Content-Disposition"))); - //Workflow Testing - //services.AddWorkflowTestingServices(); + // Workflow Testing + services.AddWorkflowTestingServices(); } public void Configure(IApplicationBuilder app) @@ -117,7 +117,7 @@ namespace Elsa.Samples.Server.Host .UseElsaFeatures() .UseRouting() .UseEndpoints(endpoints => { endpoints.MapControllers(); }) - //.MapWorkflowTestHub() + .MapWorkflowTestHub() ; } diff --git a/src/server/Elsa.Server.Api/Endpoints/Features/List.cs b/src/server/Elsa.Server.Api/Endpoints/Features/List.cs new file mode 100644 index 000000000..3e2912945 --- /dev/null +++ b/src/server/Elsa.Server.Api/Endpoints/Features/List.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Server.Api.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Elsa.Server.Api.Endpoints.Features +{ + [ApiController] + [ApiVersion("1")] + [Route("v{apiVersion:apiVersion}/features")] + [Produces("application/json")] + public class List : Controller + { + private readonly IEnumerable _providers; + + public List(IEnumerable providers) + { + _providers = providers; + } + + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [SwaggerOperation( + Summary = "Returns a list of features supported by this server.", + Description = "Returns a list of features supported by this server.", + OperationId = "Features.Get", + Tags = new[] { "Features" }) + ] + public async Task Handle(CancellationToken cancellationToken) + { + var features = await GetFeaturesAsync(cancellationToken).ToListAsync(cancellationToken); + + var model = new + { + Features = features + }; + + return Ok(model); + } + + private async IAsyncEnumerable GetFeaturesAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + foreach (var provider in _providers) + { + var features = await provider.GetFeaturesAsync(cancellationToken); + + foreach (var feature in features) + yield return feature; + } + } + } +} \ No newline at end of file diff --git a/src/server/Elsa.Server.Api/Services/IServerFeatureProvider.cs b/src/server/Elsa.Server.Api/Services/IServerFeatureProvider.cs new file mode 100644 index 000000000..fb00580d8 --- /dev/null +++ b/src/server/Elsa.Server.Api/Services/IServerFeatureProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Elsa.Server.Api.Services; + +public interface IServerFeatureProvider +{ + ValueTask> GetFeaturesAsync(CancellationToken cancellationToken); +} \ No newline at end of file