Add server features API

Closes #2588
This commit is contained in:
Sipke Schoorstra 2021-12-20 16:33:07 +01:00
parent 26f309a2b1
commit 56f70ea9e2
13 changed files with 160 additions and 35 deletions

View file

@ -290,6 +290,7 @@ export namespace Components {
"history": RouterHistory;
"importWorkflow": (file: File) => Promise<void>;
"monacoLibPath": string;
"serverFeatures": Array<string>;
"serverUrl": string;
"workflowDefinitionId": string;
}
@ -1055,6 +1056,7 @@ declare namespace LocalJSX {
"history"?: RouterHistory;
"monacoLibPath"?: string;
"onWorkflowSaved"?: (event: CustomEvent<WorkflowDefinition>) => void;
"serverFeatures"?: Array<string>;
"serverUrl"?: string;
"workflowDefinitionId"?: string;
}

View file

@ -68,10 +68,13 @@ export class ElsaStudioRoot {
const elsaClientFactory: () => Promise<ElsaClient> = () => createElsaClient(this.serverUrl);
const httpClientFactory: () => Promise<AxiosInstance> = () => 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
};

View file

@ -61,7 +61,6 @@ export class ElsaWorkflowDesigner {
@Event() activityContextMenuButtonTestClicked: EventEmitter<ActivityContextMenuState>;
@State() workflowModel: WorkflowModel;
@State() activityContextMenuState: ActivityContextMenuState = {
shown: false,
x: 0,

View file

@ -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<string> = [];
@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 = `<svg class="elsa-h-8 elsa-w-8 elsa-text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
@ -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 {
</div>`;
return (
<div class="elsa-flex-1 elsa-flex elsa-relative" >
<div class="elsa-flex-1 elsa-flex elsa-relative">
<elsa-designer-tree model={this.workflowModel}
mode={this.workflowDesignerMode}
activityContextMenuButton={this.workflowDesignerMode == WorkflowDesignerMode.Edit
@ -693,7 +694,7 @@ export class ElsaWorkflowDefinitionEditorScreen {
await eventBus.emit(EventTypes.ComponentCustomButtonClick, this, componentCustomButtonClickContext);
}
private canBeRestartedFromCurrentActivity(){
private canBeRestartedFromCurrentActivity() {
if (!this.selectedActivityId) return false;
if (this.workflowDesignerMode !== WorkflowDesignerMode.Test) return false;
@ -717,17 +718,18 @@ export class ElsaWorkflowDefinitionEditorScreen {
return (
<div class="elsa-ml-4">
<elsa-workflow-fault-information workflowFault={this.workflowInstance?.fault} faultedAt={this.workflowInstance?.faultedAt} />
<elsa-workflow-fault-information workflowFault={this.workflowInstance?.fault}
faultedAt={this.workflowInstance?.faultedAt}/>
</div>
);
}
const renderPerformanceStats = () =>{
const renderPerformanceStats = () => {
if (!!message.error) return;
return (
<div class="elsa-ml-4">
<elsa-workflow-performance-information activityStats={this.activityStats} />
<elsa-workflow-performance-information activityStats={this.activityStats}/>
</div>
);
};
@ -737,7 +739,7 @@ export class ElsaWorkflowDefinitionEditorScreen {
return (
<button type="button"
onClick={() => this.onRestartActivityButtonClick()}
onClick={() => this.onRestartActivityButtonClick()}
class="elsa-ml-0 elsa-w-full elsa-inline-flex elsa-justify-center elsa-rounded-md elsa-border elsa-border-transparent elsa-shadow-sm elsa-px-4 elsa-py-2 elsa-bg-blue-600 elsa-text-base elsa-font-medium elsa-text-white hover:elsa-bg-red-700 focus:elsa-outline-none focus:elsa-ring-2 focus:elsa-ring-offset-2 focus:elsa-ring-red-500 sm:elsa-ml-3 sm:elsa-w-auto sm:elsa-text-sm">
{this.t('Restart')}
</button>
@ -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}
>
<div class="elsa-rounded-lg elsa-shadow-lg elsa-ring-1 elsa-ring-black elsa-ring-opacity-5 elsa-overflow-hidden">
@ -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}
>
<div class="elsa-rounded-md elsa-bg-white elsa-shadow-xs" role="menu" aria-orientation="vertical"
aria-labelledby="pinned-project-options-menu-0">
@ -970,7 +975,8 @@ export class ElsaWorkflowDefinitionEditorScreen {
</div>
<div class="elsa-py-3 elsa-flex elsa-justify-between elsa-text-sm elsa-font-medium">
<dt class="elsa-text-gray-500">Connect outcomes to existing activity</dt>
<dd class="elsa-text-gray-900">Press and hold SHIFT while LEFT-clicking the outcome to connect. Release SHIFT and LEFT-click the target activity.</dd>
<dd
class="elsa-text-gray-900">Press and hold SHIFT while LEFT-clicking the outcome to connect. Release SHIFT and LEFT-click the target activity.</dd>
</div>
<div class="elsa-py-3 elsa-flex elsa-justify-between elsa-text-sm elsa-font-medium">
<dt class="elsa-text-gray-500">Pan</dt>
@ -1054,17 +1060,28 @@ export class ElsaWorkflowDefinitionEditorScreen {
workflowDefinition={this.workflowDefinition}
/>
</elsa-tab-content>
<elsa-tab-header tab="test" slot="header">Test</elsa-tab-header>
<elsa-tab-content tab="test" slot="content">
<elsa-workflow-test-panel
workflowDefinition={this.workflowDefinition}
workflowTestActivityId={this.selectedActivityId}
/>
</elsa-tab-content>
{this.renderTestPanel()}
</elsa-flyout-panel>
);
}
private renderTestPanel() {
const testingEnabled = this.serverFeatures.find(x => x == 'WorkflowTesting');
if (!testingEnabled)
return;
return [
<elsa-tab-header tab="test" slot="header">Test</elsa-tab-header>,
<elsa-tab-content tab="test" slot="content">
<elsa-workflow-test-panel
workflowDefinition={this.workflowDefinition}
workflowTestActivityId={this.selectedActivityId}
/>
</elsa-tab-content>
];
}
}
injectHistory(ElsaWorkflowDefinitionEditorScreen);
DashboardTunnel.injectProps(ElsaWorkflowDefinitionEditorScreen, ['serverUrl', 'culture', 'monacoLibPath', 'basePath']);
DashboardTunnel.injectProps(ElsaWorkflowDefinitionEditorScreen, ['serverUrl', 'culture', 'monacoLibPath', 'basePath', 'serverFeatures']);

View file

@ -6,6 +6,7 @@ export interface DashboardState {
basePath: string;
culture: string;
monacoLibPath: string;
serverFeatures: Array<string>;
}
export default createProviderConsumer<DashboardState>(
@ -13,7 +14,8 @@ export default createProviderConsumer<DashboardState>(
serverUrl: null,
basePath: null,
culture: null,
monacoLibPath: null
monacoLibPath: null,
serverFeatures: []
},
(subscribe, child) => (<context-consumer subscribe={subscribe} renderer={child}/>)
);

View file

@ -4,12 +4,14 @@ import {h} from "@stencil/core";
export interface WorkflowEditorState {
workflowDefinitionId: string;
serverUrl: string;
serverFeatures: Array<string>;
}
export default createProviderConsumer<WorkflowEditorState>(
{
workflowDefinitionId: null,
serverUrl: null
serverUrl: null,
serverFeatures: []
},
(subscribe, child) => (<context-consumer subscribe={subscribe} renderer={child}/>)
);

View file

@ -9,6 +9,7 @@ export interface ElsaStudio {
serverUrl: string;
basePath: string;
featuresString: string;
serverFeatures: Array<string>;
pluginManager: PluginManager;
propertyDisplayManager: PropertyDisplayManager;
elsaClientFactory: () => Promise<ElsaClient>;

View file

@ -244,6 +244,12 @@ export const createElsaClient = async function (serverUrl: string): Promise<Elsa
return response.data;
}
},
featuresApi: {
list: async () => {
const response = await httpClient.get<FeaturesModel>('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<Array<ActivityDescriptor>>;
}
export interface FeaturesApi {
list(): Promise<Array<string>>;
}
export interface WorkflowDefinitionsApi {
list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise<PagedList<WorkflowDefinitionSummary>>;
@ -291,7 +302,9 @@ export interface WorkflowDefinitionsApi {
export interface WorkflowTestApi {
execute(request: WorkflowTestExecuteRequest): Promise<void>;
restartFromActivity(request: WorkflowTestRestartFromActivityRequest): Promise<void>;
stop(request: WorkflowTestStopRequest): Promise<void>;
}
@ -418,3 +431,7 @@ interface ActivityEventCount {
interface ActivityFault {
message: string;
}
interface FeaturesModel {
features: Array<string>;
}

View file

@ -13,7 +13,8 @@ namespace Elsa.WorkflowTesting.Api.Extensions
return services
.AddScoped<IWorkflowTestLaunchpad, WorkflowTestLaunchpad>()
.AddScoped<IWorkflowTestService, WorkflowTestService>()
.AddNotificationHandlersFrom<ActivityExecutionResultExecutedHandler>();
.AddNotificationHandlersFrom<ActivityExecutionResultExecutedHandler>()
.AddTransient<IServerFeatureProvider, WorkflowTestingServerFeatureProvider>();
}
}
}

View file

@ -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<IEnumerable<string>> GetFeaturesAsync(CancellationToken cancellationToken) => ValueTask.FromResult<IEnumerable<string>>(new[] { "WorkflowTesting" });
}

View file

@ -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()
;
}

View file

@ -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<IServerFeatureProvider> _providers;
public List(IEnumerable<IServerFeatureProvider> 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<IActionResult> Handle(CancellationToken cancellationToken)
{
var features = await GetFeaturesAsync(cancellationToken).ToListAsync(cancellationToken);
var model = new
{
Features = features
};
return Ok(model);
}
private async IAsyncEnumerable<string> GetFeaturesAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
foreach (var provider in _providers)
{
var features = await provider.GetFeaturesAsync(cancellationToken);
foreach (var feature in features)
yield return feature;
}
}
}
}

View file

@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Elsa.Server.Api.Services;
public interface IServerFeatureProvider
{
ValueTask<IEnumerable<string>> GetFeaturesAsync(CancellationToken cancellationToken);
}