parent
26f309a2b1
commit
56f70ea9e2
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@ export class ElsaWorkflowDesigner {
|
|||
@Event() activityContextMenuButtonTestClicked: EventEmitter<ActivityContextMenuState>;
|
||||
@State() workflowModel: WorkflowModel;
|
||||
|
||||
|
||||
@State() activityContextMenuState: ActivityContextMenuState = {
|
||||
shown: false,
|
||||
x: 0,
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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}/>)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}/>)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export interface ElsaStudio {
|
|||
serverUrl: string;
|
||||
basePath: string;
|
||||
featuresString: string;
|
||||
serverFeatures: Array<string>;
|
||||
pluginManager: PluginManager;
|
||||
propertyDisplayManager: PropertyDisplayManager;
|
||||
elsaClientFactory: () => Promise<ElsaClient>;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ namespace Elsa.WorkflowTesting.Api.Extensions
|
|||
return services
|
||||
.AddScoped<IWorkflowTestLaunchpad, WorkflowTestLaunchpad>()
|
||||
.AddScoped<IWorkflowTestService, WorkflowTestService>()
|
||||
.AddNotificationHandlersFrom<ActivityExecutionResultExecutedHandler>();
|
||||
.AddNotificationHandlersFrom<ActivityExecutionResultExecutedHandler>()
|
||||
.AddTransient<IServerFeatureProvider, WorkflowTestingServerFeatureProvider>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" });
|
||||
}
|
||||
|
|
@ -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()
|
||||
;
|
||||
}
|
||||
|
||||
|
|
|
|||
57
src/server/Elsa.Server.Api/Endpoints/Features/List.cs
Normal file
57
src/server/Elsa.Server.Api/Endpoints/Features/List.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in a new issue