Incremental work on saving and loading workflow definitions

This commit is contained in:
Sipke Schoorstra 2020-11-17 20:12:18 +01:00
parent acf884c075
commit c193b28ba1
37 changed files with 474 additions and 249 deletions

View file

@ -9,7 +9,9 @@ indent_size=4
# ReSharper properties
resharper_csharp_max_line_length=240
resharper_keep_user_linebreaks=true
resharper_place_field_attribute_on_same_line=if_owner_is_single_line
resharper_space_within_single_line_array_initializer_braces=true
resharper_wrap_before_linq_expression=true
# Microsoft .NET properties
csharp_new_line_before_members_in_object_initializers=false

View file

@ -5,10 +5,6 @@ namespace Elsa.Client.Models
[DataContract]
public class ConnectionDefinition
{
public ConnectionDefinition()
{
}
public ConnectionDefinition(string sourceActivityId, string targetActivityId, string outcome)
{
SourceActivityId = sourceActivityId;
@ -16,8 +12,8 @@ namespace Elsa.Client.Models
Outcome = outcome;
}
[DataMember(Order = 1)] public string? SourceActivityId { get; set; }
[DataMember(Order = 2)] public string? TargetActivityId { get; set; }
[DataMember(Order = 3)] public string? Outcome { get; set; }
[DataMember(Order = 1)] public string SourceActivityId { get; set; }
[DataMember(Order = 2)] public string TargetActivityId { get; set; }
[DataMember(Order = 3)] public string Outcome { get; set; }
}
}

View file

@ -1,9 +1,11 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace Elsa.Client.Models
{
[DataContract]
public class List<T>
{
public ICollection<T> Items { get; set; } = new System.Collections.Generic.List<T>();
[DataMember(Order = 1)] public ICollection<T> Items { get; set; } = new System.Collections.Generic.List<T>();
}
}

View file

@ -1,9 +1,12 @@
namespace Elsa.Client.Models
using System.Runtime.Serialization;
namespace Elsa.Client.Models
{
[DataContract]
public class PagedList<T> : List<T>
{
public int? Page { get; set; }
public int? PageSize { get; set; }
public int TotalCount { get; set; }
[DataMember(Order = 1)] public int? Page { get; set; }
[DataMember(Order = 2)] public int? PageSize { get; set; }
[DataMember(Order = 3)] public int TotalCount { get; set; }
}
}

View file

@ -7,11 +7,14 @@ namespace Elsa.Client.Services
{
public interface IWorkflowDefinitionsApi
{
[Get("/v1/workflow-definitions/{workflowDefinitionId}")]
Task<WorkflowDefinition> GetAsync(string workflowDefinitionId, VersionOptions? versionOptions = default, CancellationToken cancellationToken = default);
[Get("/v1/workflow-definitions/{workflowDefinitionId}/{versionOptions}")]
Task<WorkflowDefinition> GetByDefinitionAndVersionAsync(string workflowDefinitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default);
[Get("/v1/workflow-definitions/{workflowDefinitionVersionId}")]
Task<WorkflowDefinition> GetByVersionIdAsync(string workflowDefinitionVersionId, CancellationToken cancellationToken = default);
[Get("/v1/workflow-definitions")]
Task<WorkflowDefinition> ListAsync(string workflowDefinitionId, VersionOptions? versionOptions = default, CancellationToken cancellationToken = default);
Task<PagedList<WorkflowDefinition>> ListAsync(int? page = default, int? pageSize = default, VersionOptions? versionOptions = default, CancellationToken cancellationToken = default);
[Post("/v1/workflow-definitions")]
Task<WorkflowDefinition> PostAsync([Body(BodySerializationMethod.Serialized)] PostWorkflowDefinitionRequest request, CancellationToken cancellationToken = default);

View file

@ -0,0 +1,8 @@
namespace ElsaDashboard.Application.Server
{
public enum BlazorRuntimeModel
{
Server,
Browser
}
}

View file

@ -36,7 +36,7 @@
<a class="dismiss">🗙</a>
</div>
@if (Program.UseBlazorServer)
@if (Program.RuntimeModel == BlazorRuntimeModel.Server)
{
<script src="_framework/blazor.server.js"></script>
}

View file

@ -6,9 +6,8 @@ namespace ElsaDashboard.Application.Server
{
public class Program
{
public static bool UseBlazorServer = true;
public static bool UseBlazorWebAssembly => !UseBlazorServer;
public static RenderMode RenderMode => UseBlazorServer ? RenderMode.ServerPrerendered: RenderMode.WebAssemblyPrerendered;
public static BlazorRuntimeModel RuntimeModel => BlazorRuntimeModel.Browser;
public static RenderMode RenderMode => RuntimeModel == BlazorRuntimeModel.Server ? RenderMode.ServerPrerendered : RenderMode.WebAssemblyPrerendered;
public static void Main(string[] args)
{
@ -17,9 +16,6 @@ namespace ElsaDashboard.Application.Server
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}
}

View file

@ -25,7 +25,7 @@ namespace ElsaDashboard.Application.Server
services.AddElsaDashboardUI();
services.AddElsaDashboardBackend(options => options.ServerUrl = new Uri("https://localhost:11000"));
if (Program.UseBlazorServer)
if (Program.RuntimeModel == BlazorRuntimeModel.Server)
services.AddServerSideBlazor(options =>
{
options.DetailedErrors = !Environment.IsProduction();
@ -40,7 +40,7 @@ namespace ElsaDashboard.Application.Server
{
app.UseDeveloperExceptionPage();
if(Program.UseBlazorWebAssembly)
if (Program.RuntimeModel == BlazorRuntimeModel.Browser)
app.UseWebAssemblyDebugging();
}
else
@ -50,7 +50,7 @@ namespace ElsaDashboard.Application.Server
app.UseHsts();
}
if(Program.UseBlazorWebAssembly)
if (Program.RuntimeModel == BlazorRuntimeModel.Browser)
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
@ -58,7 +58,7 @@ namespace ElsaDashboard.Application.Server
app.UseElsaGrpcServices();
app.UseEndpoints(endpoints =>
{
if(Program.UseBlazorServer)
if (Program.RuntimeModel == BlazorRuntimeModel.Server)
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");

View file

@ -266,7 +266,14 @@
</div>
<main class="flex-1 relative z-0 overflow-y-auto focus:outline-none" tabindex="0" x-data="" x-init="$el.focus()">
<!-- Page title & actions -->
<div class="border-b border-gray-200 px-4 py-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8">
<div class="flex-1 min-w-0">
<h1 class="text-lg font-medium leading-6 text-gray-900 sm:truncate">
Home
</h1>
</div>
</div>
</main>
</div>

View file

@ -1,3 +1,10 @@
<module href="./src/partials/layout.html">
<!-- Page title & actions -->
<div class="border-b border-gray-200 px-4 py-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8">
<div class="flex-1 min-w-0">
<h1 class="text-lg font-medium leading-6 text-gray-900 sm:truncate">
Home
</h1>
</div>
</div>
</module>

View file

@ -1,16 +1,19 @@
namespace ElsaDashboard.Application.Models
using Microsoft.AspNetCore.Components.Routing;
namespace ElsaDashboard.Application.Models
{
public class MenuItem
{
public MenuItem(string text, string url, string icon)
public MenuItem(string text, string url, string icon, NavLinkMatch match = NavLinkMatch.Prefix)
{
Text = text;
Url = url;
Icon = icon;
}
public string Text { get; set; }
public string Icon { get; set; }
public string Url { get; set; }
public string Text { get; init; }
public string Icon { get; init; }
public string Url { get; init; }
public NavLinkMatch Match { get; init; } = NavLinkMatch.Prefix;
}
}

View file

@ -1,4 +1,5 @@
@page "/designer"
@page "/workflows/designer"
@page "/workflows/{workflowDefinitionVersionId}/designer"
<div class="flex-0 border-b border-gray-200 px-4 py-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8">
<div class="flex-1 min-w-0">
<h1 class="text-lg font-medium leading-6 text-gray-900 sm:truncate">
@ -13,4 +14,4 @@
</span>
</div>
</div>
<WorkflowDesigner />
<WorkflowDesigner Model="WorkflowModel" />

View file

@ -1,6 +1,56 @@
namespace ElsaDashboard.Application.Pages
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Elsa.Client.Models;
using ElsaDashboard.Application.Models;
using ElsaDashboard.Shared.Rpc;
using Microsoft.AspNetCore.Components;
namespace ElsaDashboard.Application.Pages
{
partial class Designer
{
[Parameter]public string? WorkflowDefinitionVersionId { get; set; }
[Inject] private IWorkflowDefinitionService WorkflowDefinitionService { get; set; } = default!;
[Inject] private IActivityService ActivityService { get; set; } = default!;
private IDictionary<string, ActivityDescriptor> ActivityDescriptors { get; set; } = default!;
private WorkflowModel WorkflowModel { get; set; } = WorkflowModel.Blank();
protected override async Task OnInitializedAsync()
{
ActivityDescriptors = (await ActivityService.GetActivitiesAsync()).ToDictionary(x => x.Type);
if (WorkflowDefinitionVersionId != null)
{
var workflowDefinition = await WorkflowDefinitionService.GetByVersionIdAsync(WorkflowDefinitionVersionId);
WorkflowModel = CreateWorkflowModel(workflowDefinition);
}
else
{
WorkflowModel = WorkflowModel.Blank();
}
}
private WorkflowModel CreateWorkflowModel(WorkflowDefinition workflowDefinition)
{
return new WorkflowModel
{
Name = workflowDefinition.Name,
Activities = workflowDefinition.Activities.Select(CreateActivityModel).ToImmutableList(),
Connections = workflowDefinition.Connections.Select(CreateConnectionModel).ToImmutableList()
};
}
private ConnectionModel CreateConnectionModel(ConnectionDefinition connectionDefinition)
{
return new(connectionDefinition.SourceActivityId, connectionDefinition.TargetActivityId, connectionDefinition.Outcome);
}
private ActivityModel CreateActivityModel(ActivityDefinition activityDefinition)
{
var descriptor = ActivityDescriptors[activityDefinition.Type];
return new ActivityModel(activityDefinition.ActivityId, activityDefinition.Type, descriptor.Outcomes);
}
}
}

View file

@ -1,5 +1,10 @@
@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.
<!-- Page title & actions -->
<div class="border-b border-gray-200 px-4 py-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8">
<div class="flex-1 min-w-0">
<h1 class="text-lg font-medium leading-6 text-gray-900 sm:truncate">
Home
</h1>
</div>
</div>

View file

@ -8,7 +8,7 @@
</div>
<div class="mt-4 flex sm:mt-0 sm:ml-4">
<span class="order-0 sm:order-1 sm:ml-3 shadow-sm rounded-md">
<a href="/designer" type="button" class="inline-flex items-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-blue-500 hover:bg-blue-400 focus:outline-none focus:shadow-outline-blue focus:border-blue-700 active:bg-blue-700 transition duration-150 ease-in-out">
<a href="workflows/designer" type="button" class="inline-flex items-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-blue-500 hover:bg-blue-400 focus:outline-none focus:shadow-outline-blue focus:border-blue-700 active:bg-blue-700 transition duration-150 ease-in-out">
Create
</a>
</span>
@ -108,12 +108,15 @@
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-100">
@foreach (var workflowDefinition in WorkflowDefinitions.Items)
{
var displayName = workflowDefinition.DisplayName ?? workflowDefinition.Name ?? "Unnamed";
<tr>
<td class="px-6 py-3 whitespace-no-wrap text-sm leading-5 font-medium text-gray-900">
<div class="flex items-center space-x-3 lg:pl-2">
<div class="flex-shrink-0 w-2.5 h-2.5 rounded-full bg-pink-600"></div>
<a href="#" class="truncate hover:text-gray-600">
<span>Document Approval</span>
<a href="@($"workflows/{workflowDefinition.WorkflowDefinitionVersionId}/designer")" class="truncate hover:text-gray-600">
<span>@displayName</span>
</a>
</div>
</td>
@ -151,7 +154,7 @@
class="z-10 mx-3 origin-top-right absolute right-7 top-0 w-48 mt-1 rounded-md shadow-lg">
<div class="rounded-md bg-white shadow-xs" role="menu" aria-orientation="vertical" aria-labelledby="project-options-menu-0">
<div class="py-1">
<a href="#" class="group flex items-center px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" role="menuitem">
<a href="@($"workflows/{workflowDefinition.WorkflowDefinitionVersionId}/designer")" class="group flex items-center px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" role="menuitem">
<!-- Heroicon name: pencil-alt -->
<svg class="mr-3 h-5 w-5 text-gray-400 group-hover:text-gray-500 group-focus:text-gray-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z"/>
@ -190,8 +193,7 @@
</div>
</td>
</tr>
<!-- More project rows... -->
}
</tbody>
</table>
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">

View file

@ -0,0 +1,18 @@
using System.Threading.Tasks;
using Elsa.Client.Models;
using ElsaDashboard.Shared.Rpc;
using Microsoft.AspNetCore.Components;
namespace ElsaDashboard.Application.Pages
{
partial class Workflows
{
[Inject] private IWorkflowDefinitionService WorkflowDefinitionService { get; set; } = default!;
private PagedList<WorkflowDefinition> WorkflowDefinitions { get; set; } = new();
protected override async Task OnInitializedAsync()
{
WorkflowDefinitions = await WorkflowDefinitionService.ListAsync();
}
}
}

View file

@ -0,0 +1,37 @@
using System;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace ElsaDashboard.Application.Services
{
public class BackgroundWorker
{
private Channel<Func<ValueTask>> _channel;
public BackgroundWorker()
{
_channel = Channel.CreateBounded<Func<ValueTask>>(10);
}
public async Task ScheduleTask(Func<ValueTask> task, CancellationToken cancellationToken = default) => await _channel.Writer.WriteAsync(task, cancellationToken);
public async Task ScheduleTask(Action task, CancellationToken cancellationToken = default) =>
await _channel.Writer.WriteAsync(() =>
{
task();
return ValueTask.CompletedTask;
}, cancellationToken);
public async Task StartAsync(CancellationToken cancellationToken = default)
{
while (await _channel.Reader.WaitToReadAsync(cancellationToken))
{
while (_channel.Reader.TryRead(out var task))
{
await task();
}
}
}
}
}

View file

@ -29,13 +29,13 @@
<div class="p-6 space-y-6 overflow-y-auto">
@foreach (var grouping in ActivityGroupings)
@foreach (var grouping in ActivityGroupings.OrderBy(x => x.Key))
{
<div>
<h2>@grouping.Key</h2>
<ul class="grid grid-cols-1 gap-4 mt-3" x-max="1">
@foreach (var activity in grouping)
@foreach (var activity in grouping.OrderBy(x => x.DisplayName))
{
<li @onclick="(() => OnActivityClick(activity))" class="relative col-span-1 flex shadow-sm rounded-md select-none cursor-pointer">
<div class="flex-shrink-0 flex items-center justify-center w-16 bg-blue-600 text-white text-sm leading-5 font-medium rounded-l-md">

View file

@ -28,7 +28,7 @@ namespace ElsaDashboard.Application.Shared
protected override async Task OnInitializedAsync()
{
var response = (await ActivityService.GetActivitiesAsync());
Activities = response.Activities.ToList();
Activities = response.ToList();
}
private void OnActivityTypeFilterClick(ActivityTraitFilter activityTraitFilter)

View file

@ -27,34 +27,6 @@
</div>
</form>
</div>
<div class="flex items-center">
<!-- Profile dropdown -->
<div x-on:click.away="open = false" class="ml-3 relative" x-data="{ open: false }">
<div>
<button x-on:click="open = !open" class="max-w-xs flex items-center text-sm rounded-full focus:outline-none focus:shadow-outline" id="user-menu" aria-label="User menu" aria-haspopup="true" x-bind:aria-expanded="open">
<img class="h-8 w-8 rounded-full" src="https://images.unsplash.com/photo-1502685104226-ee32379fefbe?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=facearea&amp;facepad=2&amp;w=256&amp;h=256&amp;q=80" alt="">
</button>
</div>
<div x-show="open" x-description="Profile dropdown panel, show/hide based on dropdown state." x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg" style="display: none;">
<div class="rounded-md bg-white shadow-xs" role="menu" aria-orientation="vertical" aria-labelledby="user-menu">
<div class="py-1">
<a href="#" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" role="menuitem">View profile</a>
<a href="#" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" role="menuitem">Settings</a>
<a href="#" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" role="menuitem">Notifications</a>
</div>
<div class="border-t border-gray-100"></div>
<div class="py-1">
<a href="#" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" role="menuitem">Get desktop app</a>
<a href="#" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" role="menuitem">Support</a>
</div>
<div class="border-t border-gray-100"></div>
<div class="py-1">
<a href="#" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" role="menuitem">Logout</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<main class="flex flex-col flex-1 relative z-0 overflow-y-auto focus:outline-none" tabindex="0" x-data="" x-init="$el.focus()">

View file

@ -17,28 +17,15 @@
<div class="mt-5 flex-1 h-0 overflow-y-auto">
<nav class="px-2">
<div class="space-y-1">
<a href="#" class="group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md text-gray-900 bg-gray-100 hover:text-gray-900 hover:bg-gray-100 focus:bg-gray-200 focus:outline-none transition ease-in-out duration-150">
@foreach (var menuItem in MenuItems)
{
const string activeClass = "text-gray-900 bg-gray-100 hover:text-gray-900 hover:bg-gray-100 focus:bg-gray-200";
<NavLink Match="@menuItem.Match" href="@menuItem.Url" class="group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-50 focus:bg-gray-50 focus:outline-none transition ease-in-out duration-150" ActiveClass="@activeClass">
<svg class="mr-3 h-6 w-6 text-gray-500 group-hover:text-gray-500 group-focus:text-gray-600 transition ease-in-out duration-150" x-description="Heroicon name: home" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="@menuItem.Icon"></path>
</svg>
Home
</a>
<a href="#" class="group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-50 focus:bg-gray-50 focus:outline-none transition ease-in-out duration-150">
<svg class="mr-3 h-6 w-6 text-gray-400 group-hover:text-gray-500 group-focus:text-gray-600 transition ease-in-out duration-150" x-description="Heroicon name: view-list" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
</svg>
My tasks
</a>
<a href="#" class="group flex items-center px-2 py-2 text-base leading-5 font-medium rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-50 focus:bg-gray-50 focus:outline-none transition ease-in-out duration-150">
<svg class="mr-3 h-6 w-6 text-gray-400 group-hover:text-gray-500 group-focus:text-gray-600 transition ease-in-out duration-150" x-description="Heroicon name: clock" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Recent
</a>
@menuItem.Text
</NavLink> }
</div>
</nav>
</div>
@ -72,27 +59,16 @@
<nav class="px-3 mt-6">
<div class="space-y-1">
<a href="#" class="group flex items-center px-2 py-2 text-sm leading-5 font-medium rounded-md text-gray-900 bg-gray-200 focus:outline-none focus:bg-gray-50 transition ease-in-out duration-150">
<svg class="mr-3 h-6 w-6 text-gray-500 group-hover:text-gray-500 group-focus:text-gray-600 transition ease-in-out duration-150" x-description="Heroicon name: home" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
@foreach (var menuItem in MenuItems)
{
const string activeClass = "text-gray-900 bg-gray-200 focus:bg-gray-50";
<NavLink Match="@menuItem.Match" href="@menuItem.Url" class="group flex items-center px-2 py-2 text-sm leading-5 font-medium rounded-md text-gray-700 hover:text-gray-900 hover:bg-gray-50 focus:outline-none focus:bg-gray-50 transition ease-in-out duration-150" ActiveClass="@activeClass">
<svg class="mr-3 h-6 w-6 text-gray-400 group-hover:text-gray-500 group-focus:text-gray-600 transition ease-in-out duration-150" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="@menuItem.Icon"></path>
</svg>
Home
</a>
<a href="#" class="group flex items-center px-2 py-2 text-sm leading-5 font-medium rounded-md text-gray-700 hover:text-gray-900 hover:bg-gray-50 focus:outline-none focus:bg-gray-50 transition ease-in-out duration-150">
<svg class="mr-3 h-6 w-6 text-gray-400 group-hover:text-gray-500 group-focus:text-gray-600 transition ease-in-out duration-150" x-description="Heroicon name: view-list" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"></path>
</svg>
My tasks
</a>
<a href="#" class="group flex items-center px-2 py-2 text-sm leading-5 font-medium rounded-md text-gray-700 hover:text-gray-900 hover:bg-gray-50 focus:outline-none focus:bg-gray-50 transition ease-in-out duration-150">
<svg class="mr-3 h-6 w-6 text-gray-400 group-hover:text-gray-500 group-focus:text-gray-600 transition ease-in-out duration-150" x-description="Heroicon name: clock" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Recent
</a>
@menuItem.Text
</NavLink>
}
</div>
</nav>
</div>

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using ElsaDashboard.Application.Models;
using Microsoft.AspNetCore.Components.Routing;
namespace ElsaDashboard.Application.Shared
{
@ -9,8 +10,9 @@ namespace ElsaDashboard.Application.Shared
{
MenuItems = new[]
{
new MenuItem("Dashboard", "/", "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"),
new MenuItem("Workflows", "/workflows", "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"),
new MenuItem("Home", "", "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6", NavLinkMatch.All),
new MenuItem("Workflows", "workflows", "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z", NavLinkMatch.Prefix),
new MenuItem("Composite Activities", "composite-activities", "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z", NavLinkMatch.Prefix),
};
}

View file

@ -3,12 +3,14 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.Formats.Asn1;
using System.Linq;
using System.Threading.Channels;
using System.Threading.Tasks;
using Elsa.Client.Models;
using ElsaDashboard.Application.Extensions;
using ElsaDashboard.Application.Models;
using ElsaDashboard.Application.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
@ -18,17 +20,36 @@ namespace ElsaDashboard.Application.Shared
{
private static Action<ConnectionModel> _connectionCreatedAction = default!;
[Parameter] public WorkflowModel Model { private get; set; } = WorkflowModel.Demo();
[Parameter] public WorkflowModel Model { private get; set; } = WorkflowModel.Blank();
[Inject] private IJSRuntime JS { get; set; } = default!;
[Inject] private IFlyoutPanelService FlyoutPanelService { get; set; } = default!;
private IJSObjectReference _designerModule = default!;
private bool _connectionsChanged = true;
private EventCallbackFactory EventCallbackFactory { get; } = new();
private BackgroundWorker BackgroundWorker { get; } = new();
[JSInvokableAttribute("InvokeConnectionCreated")]
public static void InvokeConnectionCreated(ConnectionModel connection) => _connectionCreatedAction(connection);
protected override void OnInitialized() => _connectionCreatedAction = OnConnectionCreated;
int _currentCount = 0;
void IncrementCount()
{
_currentCount++;
}
protected override async Task OnInitializedAsync()
{
_connectionCreatedAction = OnConnectionCreated;
InvokeAsync(() => BackgroundWorker.StartAsync());
}
protected override void OnParametersSet()
{
ConnectionsHasChanged();
}
public async ValueTask DisposeAsync()
{
@ -38,14 +59,24 @@ namespace ElsaDashboard.Application.Shared
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
if (_designerModule == null!)
_designerModule = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/ElsaDashboard.Application/workflowDesigner.js");
}
await RepaintConnections();
}
private async Task UpdateModelAsync(WorkflowModel model)
{
Model = model;
ConnectionsHasChanged();
await BackgroundWorker.ScheduleTask(SaveWorkflowAsync);
}
private async ValueTask SaveWorkflowAsync()
{
}
private IEnumerable<ActivityModel> GetRootActivities() => Model.GetChildActivities(null);
private async ValueTask RepaintConnections()
@ -197,6 +228,7 @@ namespace ElsaDashboard.Application.Shared
Model = model;
await FlyoutPanelService.HideAsync();
ConnectionsHasChanged();
await BackgroundWorker.ScheduleTask(() => Console.WriteLine("TEST"));
}
private void ConnectionsHasChanged()

View file

@ -20,6 +20,7 @@ namespace ElsaDashboard.Backend.Extensions
services.AddElsaClient(configure);
services.AddCodeFirstGrpc(options => options.ResponseCompressionLevel = CompressionLevel.Optimal);
services.AddScoped<IActivityService, ActivityService>();
services.AddScoped<IWorkflowDefinitionService, WorkflowDefinitionService>();
return services;
}

View file

@ -1,5 +1,7 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading.Tasks;
using Elsa.Client;
using Elsa.Client.Models;
using ElsaDashboard.Shared.Rpc;
using ProtoBuf.Grpc;
@ -14,10 +16,10 @@ namespace ElsaDashboard.Backend.Rpc
_elsaClient = elsaClient;
}
public async Task<GetActivitiesResponse> GetActivitiesAsync(CallContext callContext)
public async Task<IEnumerable<ActivityDescriptor>> GetActivitiesAsync(CallContext callContext)
{
var result = await _elsaClient.Activities.ListAsync(callContext.CancellationToken);
return new GetActivitiesResponse(result.Items);
return result.Items;
}
}
}

View file

@ -1,7 +0,0 @@
namespace ElsaDashboard.Backend.Rpc
{
public class WorkflowDefinitionManager
{
}
}

View file

@ -0,0 +1,23 @@
using System.Threading.Tasks;
using Elsa.Client;
using Elsa.Client.Models;
using ElsaDashboard.Shared.Rpc;
using ProtoBuf.Grpc;
namespace ElsaDashboard.Backend.Rpc
{
public class WorkflowDefinitionService : IWorkflowDefinitionService
{
private readonly IElsaClient _elsaClient;
public WorkflowDefinitionService(IElsaClient elsaClient)
{
_elsaClient = elsaClient;
}
public async Task<PagedList<WorkflowDefinition>> ListAsync(ListWorkflowDefinitionsRequest request, CallContext context) =>
await _elsaClient.WorkflowDefinitions.ListAsync(request.Page, request.PageSize, request.VersionOptions, context.CancellationToken);
public Task<WorkflowDefinition> GetByVersionIdAsync(string workflowDefinitionVersionId, CallContext context = default) => _elsaClient.WorkflowDefinitions.GetByVersionIdAsync(workflowDefinitionVersionId, context.CancellationToken);
}
}

View file

@ -12,21 +12,6 @@ namespace ElsaDashboard.Shared.Rpc
public interface IActivityService
{
[ProtoBehavior]
Task<GetActivitiesResponse> GetActivitiesAsync(CallContext context = default);
}
[ProtoContract]
public sealed class GetActivitiesResponse
{
public GetActivitiesResponse()
{
}
public GetActivitiesResponse(IEnumerable<ActivityDescriptor> activities)
{
Activities = activities;
}
[ProtoMember(1)] public IEnumerable<ActivityDescriptor> Activities { get; set; } = new System.Collections.Generic.List<ActivityDescriptor>();
Task<IEnumerable<ActivityDescriptor>> GetActivitiesAsync(CallContext context = default);
}
}

View file

@ -1,12 +1,38 @@
using System.ServiceModel;
using System.Threading.Tasks;
using Elsa.Client.Models;
using ProtoBuf;
using ProtoBuf.Grpc;
namespace ElsaDashboard.Shared.Rpc
{
[ProtoContract]
[ServiceContract]
public interface IWorkflowDefinitionService
{
//ValueTask<ICollection<Test>> GetAsync(string workflowDefinitionId, VersionOptions version);
Task<PagedList<WorkflowDefinition>> ListAsync(ListWorkflowDefinitionsRequest request, CallContext context = default);
Task<WorkflowDefinition> GetByVersionIdAsync(string workflowDefinitionVersionId, CallContext context = default);
}
public record Test(int a, bool b);
[ProtoContract]
public record ListWorkflowDefinitionsRequest
{
public ListWorkflowDefinitionsRequest(int? page = default, int? pageSize = default, VersionOptions? versionOptions = default)
{
Page = page;
PageSize = pageSize;
VersionOptions = versionOptions;
}
[ProtoMember(1)] public int? Page { get; init; }
[ProtoMember(2)] public int? PageSize { get; init; }
[ProtoMember(3)] public VersionOptions? VersionOptions { get; init; }
}
public static class WorkflowDefinitionServiceExtensions
{
public static Task<PagedList<WorkflowDefinition>> ListAsync(this IWorkflowDefinitionService service, int? page = default, int? pageSize = default, VersionOptions? versionOptions = default)
{
return service.ListAsync(new ListWorkflowDefinitionsRequest(page, pageSize, versionOptions));
}
}
}

View file

@ -17,10 +17,6 @@ namespace ElsaDashboard.Shared.Surrogates
SerializerSettings = new JsonSerializerSettings().ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
}
public ActivityPropertyDescriptorSurrogate()
{
}
public ActivityPropertyDescriptorSurrogate(ActivityPropertyDescriptor value)
{
Name = value.Name;

View file

@ -12,7 +12,8 @@ namespace ElsaDashboard.Shared.Surrogates
{
private static readonly IDictionary<Type, Type> SurrogateMapping = new Dictionary<Type, Type>
{
[typeof(ActivityPropertyDescriptor)] = typeof(ActivityPropertyDescriptorSurrogate)
[typeof(ActivityPropertyDescriptor)] = typeof(ActivityPropertyDescriptorSurrogate),
[typeof(VersionOptions)] = typeof(VersionOptionsSurrogate)
};
/// <summary>

View file

@ -0,0 +1,27 @@
using Elsa.Client.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NodaTime;
using NodaTime.Serialization.JsonNet;
using ProtoBuf;
namespace ElsaDashboard.Shared.Surrogates
{
[ProtoContract(IgnoreListHandling = true)]
public class VersionOptionsSurrogate
{
public VersionOptionsSurrogate()
{
}
public VersionOptionsSurrogate(VersionOptions value)
{
Value = value.ToString();
}
[ProtoMember(1)] public string Value { get; } = default!;
public static implicit operator VersionOptions(VersionOptionsSurrogate surrogate) => VersionOptions.FromString(surrogate.Value);
public static implicit operator VersionOptionsSurrogate(VersionOptions source) => new(source);
}
}

View file

@ -1,4 +1,6 @@
using System.Threading.Tasks;
using AutoMapper;
using Elsa.Extensions;
using Elsa.Services;
using Microsoft.Extensions.DependencyInjection;
@ -13,6 +15,7 @@ namespace Elsa.Samples.HelloWorldConsole
.AddElsa()
.AddConsoleActivities()
.AddWorkflow<HelloWorld>()
.AddAutoMapperProfiles<Program>()
.BuildServiceProvider();
// Run startup actions (not needed when registering Elsa with a Host).

View file

@ -14,14 +14,14 @@ namespace Elsa.Server.Api.Endpoints.WorkflowDefinitions
{
[ApiController]
[ApiVersion("1")]
[Route("v{apiVersion:apiVersion}/workflow-definitions/{workflowDefinitionId}")]
[Route("v{apiVersion:apiVersion}/workflow-definitions/{workflowDefinitionId}/{versionOptions}")]
[Produces("application/json")]
public class Get : Controller
public class GetByDefinitionAndVersion : Controller
{
private readonly IWorkflowDefinitionManager _workflowDefinitionManager;
private readonly IContentSerializer _serializer;
public Get(IWorkflowDefinitionManager workflowDefinitionManager, IContentSerializer serializer)
public GetByDefinitionAndVersion(IWorkflowDefinitionManager workflowDefinitionManager, IContentSerializer serializer)
{
_workflowDefinitionManager = workflowDefinitionManager;
_serializer = serializer;
@ -33,13 +33,13 @@ namespace Elsa.Server.Api.Endpoints.WorkflowDefinitions
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SwaggerOperation(
Summary = "Returns a single workflow definition.",
Description = "Returns a single workflow definition using the specified workflow definition ID and optional version options. When no version options are specified, the latest version is returned.",
Description = "Returns a single workflow definition using the specified workflow definition ID and version options.",
OperationId = "WorkflowDefinitions.Get",
Tags = new[] { "WorkflowDefinitions" })
]
public async Task<IActionResult> Handle(string workflowDefinitionId, [FromQuery]VersionOptions? versionOptions = default, CancellationToken cancellationToken = default)
public async Task<IActionResult> Handle(string workflowDefinitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
{
var workflowDefinition = await _workflowDefinitionManager.GetAsync(workflowDefinitionId, versionOptions ?? VersionOptions.Latest, cancellationToken);
var workflowDefinition = await _workflowDefinitionManager.GetAsync(workflowDefinitionId, versionOptions, cancellationToken);
return workflowDefinition == null ? (IActionResult) NotFound() : Json(workflowDefinition, _serializer.GetSettings());
}
}

View file

@ -0,0 +1,46 @@
using System.Threading;
using System.Threading.Tasks;
using Elsa.Extensions;
using Elsa.Models;
using Elsa.Serialization;
using Elsa.Server.Api.Swagger;
using Elsa.Services;
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/{workflowDefinitionVersionId}")]
[Produces("application/json")]
public class GetByVersionId : Controller
{
private readonly IWorkflowDefinitionManager _workflowDefinitionManager;
private readonly IContentSerializer _serializer;
public GetByVersionId(IWorkflowDefinitionManager workflowDefinitionManager, IContentSerializer serializer)
{
_workflowDefinitionManager = workflowDefinitionManager;
_serializer = serializer;
}
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(WorkflowDefinition))]
[SwaggerResponseExample(StatusCodes.Status200OK, typeof(WorkflowDefinitionExample))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SwaggerOperation(
Summary = "Returns a single workflow definition.",
Description = "Returns a single workflow definition using the specified workflow definition version ID.",
OperationId = "WorkflowDefinitions.Get",
Tags = new[] { "WorkflowDefinitions" })
]
public async Task<IActionResult> Handle(string workflowDefinitionVersionId, CancellationToken cancellationToken = default)
{
var workflowDefinition = await _workflowDefinitionManager.GetByVersionIdAsync(workflowDefinitionVersionId, cancellationToken);
return workflowDefinition == null ? (IActionResult) NotFound() : Json(workflowDefinition, _serializer.GetSettings());
}
}
}

View file

@ -66,7 +66,7 @@ namespace Elsa.Server.Api.Endpoints.WorkflowDefinitions
else
await _workflowPublisher.SaveDraftAsync(workflowDefinition, cancellationToken);
return CreatedAtAction("Handle", "Get", new {workflowDefinitionId = workflowDefinition.WorkflowDefinitionId, apiVersion = apiVersion.ToString()}, workflowDefinition);
return CreatedAtAction("Handle", "GetByVersionId", new {workflowDefinitionVersionId = workflowDefinition.WorkflowDefinitionVersionId, apiVersion = apiVersion.ToString()}, workflowDefinition);
}
}
}