Incremental work on dashboard + API

This commit is contained in:
Sipke Schoorstra 2020-12-10 22:08:53 +01:00
parent b4bf08e15c
commit c9199bb335
31 changed files with 455 additions and 164 deletions

View file

@ -1,13 +1,15 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using Elsa.Client.Comparers;
using NodaTime;
namespace Elsa.Client.Models
{
[DataContract]
public class WorkflowInstance
{
private HashSet<BlockingActivity> _blockingActivities = new HashSet<BlockingActivity>(BlockingActivityEqualityComparer.Instance);
private HashSet<BlockingActivity> _blockingActivities = new(BlockingActivityEqualityComparer.Instance);
public WorkflowInstance()
{
Variables = new Variables();
@ -17,27 +19,30 @@ namespace Elsa.Client.Models
PostScheduledActivities = new Stack<ScheduledActivity>();
}
public int Id { get; set; }
public string WorkflowInstanceId { get; set; } = default!;
public string WorkflowDefinitionId { get; set; } = default!;
public int Version { get; set; }
public WorkflowStatus Status { get; set; }
public string? CorrelationId { get; set; }
public string? ContextId { get; set; }
public Instant CreatedAt { get; set; }
public Variables Variables { get; set; }
public object? Output { get; set; }
public ICollection<ActivityInstance> Activities { get; set; }
[DataMember(Order = 1)] public int Id { get; set; }
[DataMember(Order = 2)] public string WorkflowInstanceId { get; set; } = default!;
[DataMember(Order = 3)] public string WorkflowDefinitionId { get; set; } = default!;
[DataMember(Order = 4)] public int Version { get; set; }
[DataMember(Order = 5)] public WorkflowStatus Status { get; set; }
[DataMember(Order = 6)] public string? CorrelationId { get; set; }
[DataMember(Order = 7)] public string? ContextId { get; set; }
[DataMember(Order = 8)] public Instant CreatedAt { get; set; }
[DataMember(Order = 9)] public Instant? LastExecutedAt { get; set; }
[DataMember(Order = 10)] public Instant? LastBurstAt { get; set; }
[DataMember(Order = 11)] public Instant? CompletedAt { get; set; }
[DataMember(Order = 12)] public Variables Variables { get; set; }
[DataMember(Order = 13)] public object? Output { get; set; }
[DataMember(Order = 14)] public ICollection<ActivityInstance> Activities { get; set; }
public HashSet<BlockingActivity> BlockingActivities
[DataMember(Order = 15)] public HashSet<BlockingActivity> BlockingActivities
{
get => _blockingActivities;
set => _blockingActivities = new HashSet<BlockingActivity>(value, BlockingActivityEqualityComparer.Instance);
}
public ICollection<ExecutionLogEntry> ExecutionLog { get; set; }
public WorkflowFault? Fault { get; set; }
public Stack<ScheduledActivity> ScheduledActivities { get; set; }
public Stack<ScheduledActivity> PostScheduledActivities { get; set; }
[DataMember(Order = 16)] public ICollection<ExecutionLogEntry> ExecutionLog { get; set; }
[DataMember(Order = 17)] public WorkflowFault? Fault { get; set; }
[DataMember(Order = 18)] public Stack<ScheduledActivity> ScheduledActivities { get; set; }
[DataMember(Order = 19)] public Stack<ScheduledActivity> PostScheduledActivities { get; set; }
}
}

View file

@ -2,7 +2,7 @@
{
public interface ICompositeActivity<in TBuilder> where TBuilder: ICompositeActivityBuilder
{
void Build(TBuilder compositeActivity);
void Build(TBuilder workflow);
}
public interface ICompositeActivity : ICompositeActivity<ICompositeActivityBuilder>

View file

@ -14,6 +14,7 @@ namespace Elsa.Builders
bool DeleteCompletedInstances { get; }
bool IsEnabled { get; }
IWorkflowBuilder WithWorkflowDefinitionId(string value);
IWorkflowBuilder WithDisplayName(string value);
IWorkflowBuilder WithDescription(string value);
IWorkflowBuilder WithVersion(int value);
IWorkflowBuilder AsSingleton();

View file

@ -27,6 +27,9 @@ namespace Elsa.Models
public string? CorrelationId { get; set; }
public string? ContextId { get; set; }
public Instant CreatedAt { get; set; }
public Instant? LastExecutedAt { get; set; }
public Instant? LastBurstAt { get; set; }
public Instant? CompletedAt { get; set; }
public Variables Variables { get; set; }
public object? Output { get; set; }
public ICollection<ActivityInstance> Activities { get; set; }

View file

@ -45,6 +45,12 @@ namespace Elsa.Builders
return this;
}
public IWorkflowBuilder WithDisplayName(string value)
{
DisplayName = value;
return this;
}
public IWorkflowBuilder WithDescription(string? value)
{
Description = value;
@ -123,7 +129,12 @@ namespace Elsa.Builders
public IWorkflowBlueprint Build(IWorkflow workflow, string activityIdPrefix)
{
WithId(workflow.GetType().Name);
var workflowTypeName = workflow.GetType().Name;
Name ??= workflowTypeName;
DisplayName ??= workflowTypeName;
WithId(workflowTypeName);
workflow.Build(this);
return BuildBlueprint(activityIdPrefix);
}
@ -139,7 +150,7 @@ namespace Elsa.Builders
public IWorkflowBlueprint BuildBlueprint(string activityIdPrefix)
{
var compositeRoot = Build(activityIdPrefix);
return new WorkflowBlueprint(
ActivityId,
Version,

View file

@ -17,6 +17,7 @@
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="stylesheet" href="/tailwind.css">
<link href="_content/Blazored.Modal/blazored-modal.css" rel="stylesheet" />
<link href="ElsaDashboard.Application.Server.styles.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/@@ryangjchandler/spruce@2.0.1/dist/spruce.umd.js"></script>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.js"></script>
@ -45,5 +46,6 @@ else
<script src="_framework/blazor.webassembly.js"></script>
}
<script src="_content/ElsaDashboard.Application/store.js"></script>
<script src="_content/Blazored.Modal/blazored.modal.js"></script>
</body>
</html>

View file

@ -1,10 +1,12 @@
<Router AppAssembly="@typeof(MenuItem).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
<CascadingBlazoredModal>
<Router AppAssembly="@typeof(MenuItem).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingBlazoredModal>

View file

@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazored.Modal" Version="5.1.0" />
<PackageReference Include="Humanizer.Core" Version="2.8.26" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="5.0.0" />
</ItemGroup>

View file

@ -1,4 +1,5 @@
using ElsaDashboard.Application.Services;
using Blazored.Modal;
using ElsaDashboard.Application.Services;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
@ -7,7 +8,9 @@ namespace Microsoft.Extensions.DependencyInjection
{
public static IServiceCollection AddElsaDashboardUI(this IServiceCollection services)
{
return services.AddScoped<IFlyoutPanelService, FlyoutPanelService>();
return services
.AddBlazoredModal()
.AddScoped<IFlyoutPanelService, FlyoutPanelService>();
}
}
}

View file

@ -0,0 +1 @@
<TailwindModal></TailwindModal>

View file

@ -60,7 +60,10 @@
<tbody class="bg-white divide-y divide-gray-100">
@foreach (var workflowInstance in WorkflowInstances.Items)
{
var displayName = workflowInstance.Id;
var workflowBlueprint = WorkflowBlueprints[(workflowInstance.WorkflowDefinitionId, workflowInstance.Version)];
var displayName = workflowBlueprint.DisplayName;
var statusColor = GetStatusColor(workflowInstance.Status);
var viewUrl = $"workflow-instances/{workflowInstance.WorkflowInstanceId}/viewer";
<tr>
<td class="px-6 py-3 whitespace-no-wrap text-sm leading-5 font-medium text-gray-900">
<a href="@($"workflow-instances/{workflowInstance.WorkflowInstanceId}/viewer")" class="truncate hover:text-gray-600">
@ -69,140 +72,48 @@
</td>
<td class="px-6 py-3 whitespace-no-wrap text-sm leading-5 font-medium text-gray-900 text-left">
<a href="@($"workflow-registry/{workflowInstance.WorkflowDefinitionId}/viewer")" class="truncate hover:text-gray-600">
Hello World
@displayName
</a>
</td>
<td class="hidden md:table-cell px-6 py-3 whitespace-no-wrap text-sm leading-5 text-gray-500 text-right">
1
@workflowInstance.Version
</td>
<td class="hidden md:table-cell px-6 py-3 whitespace-no-wrap text-sm leading-5 text-gray-500 text-right">
<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>
<span>Completed</span>
<div class="flex-shrink-0 w-2.5 h-2.5 rounded-full bg-@statusColor-600"></div>
<span>@workflowInstance.Status</span>
</div>
</td>
<td class="hidden md:table-cell px-6 py-3 whitespace-no-wrap text-sm leading-5 text-gray-500 text-left">
Today
@workflowInstance.CreatedAt
</td>
<td class="hidden md:table-cell px-6 py-3 whitespace-no-wrap text-sm leading-5 text-gray-500 text-left">
Today
@(workflowInstance.CompletedAt != null ? workflowInstance.CompletedAt.ToString() : "-")
</td>
<td class="pr-6">
<div x-data="{ open: false }" x-on:keydown.escape="open = false" x-on:click.away="open = false" class="relative flex justify-end items-center">
<button x-on:click="open = !open" id="project-options-menu-0" aria-has-popup="true" type="button" class="w-8 h-8 inline-flex items-center justify-center text-gray-400 rounded-full bg-transparent hover:text-gray-500 focus:outline-none focus:text-gray-500 focus:bg-gray-100 transition ease-in-out duration-150">
<!-- Heroicon name: dots-vertical -->
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"/>
<ContextMenu>
<ContextMenuItem Url="@viewUrl">
<!-- 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"/>
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"/>
</svg>
</button>
<div x-show="open"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
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="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="@($"workflow-instances/{workflowInstance.WorkflowInstanceId}/viewer")" 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"/>
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd"/>
</svg>
Edit
</a>
<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">
<!-- Heroicon name: duplicate -->
<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="M7 9a2 2 0 012-2h6a2 2 0 012 2v6a2 2 0 01-2 2H9a2 2 0 01-2-2V9z"/>
<path d="M5 3a2 2 0 00-2 2v6a2 2 0 002 2V5h8a2 2 0 00-2-2H5z"/>
</svg>
Duplicate
</a>
<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">
<!-- Heroicon name: user-add -->
<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="M8 9a3 3 0 100-6 3 3 0 000 6zM8 11a6 6 0 016 6H2a6 6 0 016-6zM16 7a1 1 0 10-2 0v1h-1a1 1 0 100 2h1v1a1 1 0 102 0v-1h1a1 1 0 100-2h-1V7z"/>
</svg>
Share
</a>
</div>
<div class="border-t border-gray-100"></div>
<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">
<!-- Heroicon name: trash -->
<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 fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
Delete
</a>
</div>
</div>
</div>
</div>
View
</ContextMenuItem>
<ContextMenuDivider/>
<ContextMenuItem OnClick="@(() => OnDeleteWorkflowInstanceClick(workflowInstance))">
<!-- Heroicon name: trash -->
<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 fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>
Delete
</ContextMenuItem>
</ContextMenu>
</td>
</tr>
}
</tbody>
</table>
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
<a href="#" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm leading-5 font-medium rounded-md text-gray-700 bg-white hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
Previous
</a>
<a href="#" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm leading-5 font-medium rounded-md text-gray-700 bg-white hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
Next
</a>
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm leading-5 text-gray-700">
Showing
<span class="font-medium">1</span>
to
<span class="font-medium">10</span>
of
<span class="font-medium">97</span>
results
</p>
</div>
<div>
<nav class="relative z-0 inline-flex shadow-sm">
<a href="#" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-500 hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150" aria-label="Previous">
<svg class="h-5 w-5" x-description="Heroicon name: chevron-left" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</a>
<a href="#" class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
1
</a>
<a href="#" class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
2
</a>
<a href="#" class="hidden md:inline-flex -ml-px relative items-center px-4 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
3
</a>
<span class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700">
...
</span>
<a href="#" class="hidden md:inline-flex -ml-px relative items-center px-4 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
8
</a>
<a href="#" class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
9
</a>
<a href="#" class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
10
</a>
<a href="#" class="-ml-px relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-500 hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150" aria-label="Next">
<svg class="h-5 w-5" x-description="Heroicon name: chevron-right" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
</a>
</nav>
</div>
</div>
</div>
<Pager Page="@WorkflowInstances.Page" PageSize="@WorkflowInstances.PageSize" TotalCount="@WorkflowInstances.TotalCount"/>
</div>
</div>

View file

@ -1,5 +1,11 @@
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Blazored.Modal;
using Blazored.Modal.Services;
using Elsa.Client.Models;
using ElsaDashboard.Application.Shared;
using ElsaDashboard.Shared.Rpc;
using Microsoft.AspNetCore.Components;
@ -8,11 +14,45 @@ namespace ElsaDashboard.Application.Pages.WorkflowInstances
partial class List
{
[Inject] private IWorkflowInstanceService WorkflowInstanceService { get; set; } = default!;
[Inject] private IWorkflowRegistryService WorkflowRegistryService { get; set; } = default!;
[Inject] private IModalService ModalService { get; set; } = default!;
private PagedList<WorkflowInstance> WorkflowInstances { get; set; } = new();
private IDictionary<(string, int), WorkflowBlueprint> WorkflowBlueprints { get; set; } = new Dictionary<(string, int), WorkflowBlueprint>();
protected override async Task OnInitializedAsync()
{
WorkflowInstances = await WorkflowInstanceService.ListAsync();
var workflowBlueprintsTask = WorkflowRegistryService.ListAsync();
var workflowInstancesTask = WorkflowInstanceService.ListAsync();
await Task.WhenAll(workflowBlueprintsTask, workflowInstancesTask);
WorkflowBlueprints = workflowBlueprintsTask.Result.Items.ToDictionary(x => (x.Id, x.Version));
WorkflowInstances = workflowInstancesTask.Result;
}
private async Task OnDeleteWorkflowInstanceClick(WorkflowInstance workflowInstance)
{
var options = new ModalOptions { UseCustomLayout = true };
var parameters = new ModalParameters();
parameters.Add("Title", "Are you sure?");
var result = await ModalService.Show<DeleteDialog>("Are you sure?").Result;
if (result.Cancelled)
return;
// Delete.
}
private static string GetStatusColor(WorkflowStatus status) =>
status switch
{
WorkflowStatus.Idle => "gray",
WorkflowStatus.Running => "pink",
WorkflowStatus.Suspended => "blue",
WorkflowStatus.Finished => "green",
WorkflowStatus.Faulted => "red",
WorkflowStatus.Cancelled => "yellow",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null)
};
}
}

View file

@ -0,0 +1,21 @@
<div x-data="{ open: false }" x-on:keydown.escape="open = false" x-on:click.away="open = false" class="relative flex justify-end items-center">
<button x-on:click="open = !open" id="project-options-menu-0" aria-has-popup="true" type="button" class="w-8 h-8 inline-flex items-center justify-center text-gray-400 rounded-full bg-transparent hover:text-gray-500 focus:outline-none focus:text-gray-500 focus:bg-gray-100 transition ease-in-out duration-150">
<!-- Heroicon name: dots-vertical -->
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"/>
</svg>
</button>
<div x-show="open"
x-transition:enter="transition ease-out duration-100"
x-transition:enter-start="transform opacity-0 scale-95"
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="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">
@ChildContent
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Components;
namespace ElsaDashboard.Application.Shared
{
partial class ContextMenu
{
[Parameter]public RenderFragment ChildContent { get; set; }
}
}

View file

@ -0,0 +1 @@
<div class="border-t border-gray-100"></div>

View file

@ -0,0 +1,42 @@
<a @onclick:preventDefault @onclick="@OnClick" 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">
@if (ChildContent != null)
{
@ChildContent
}
else
{
<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="@Icon"/>
</svg>
@Text
}
</a>
@code
{
[Inject]
private NavigationManager NavigationManager { get; set; }
[Parameter]
public string Text { get; set; } = default!;
[Parameter]
public string Icon { get; set; } = default!;
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter]
public string? Url { get; set; }
[Parameter]
public EventCallback OnClick { get; set; }
protected override void OnParametersSet()
{
if (Url != null)
OnClick = new EventCallbackFactory().Create(this, Navigate);
}
private void Navigate() => NavigationManager.NavigateTo(Url!);
}

View file

@ -0,0 +1,68 @@
@if (Page != null)
{
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
@if (Page > 0)
{
<a href="#" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm leading-5 font-medium rounded-md text-gray-700 bg-white hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
Previous
</a>
}
@if (Page < PageCount)
{
<a href="#" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm leading-5 font-medium rounded-md text-gray-700 bg-white hover:text-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
Next
</a>
}
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm leading-5 text-gray-700">
Showing
<span class="font-medium">@From</span>
to
<span class="font-medium">@To</span>
of
<span class="font-medium">@TotalCount</span>
results
</p>
</div>
<div>
<nav class="relative z-0 inline-flex shadow-sm">
@if (Page > 0)
{
<a href="#" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-500 hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150" aria-label="Previous">
<svg class="h-5 w-5" x-description="Heroicon name: chevron-left" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
</a>
}
@for (var i = 1; i <= PageCount; i++)
{
var isCurrent = Page + 1 == i;
if (isCurrent)
{
<span class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700">
@i
</span>
}
else
{
<a href="#" class="-ml-px relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150">
@i
</a>
}
}
@if (Page < PageCount)
{
<a href="#" class="-ml-px relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm leading-5 font-medium text-gray-500 hover:text-gray-400 focus:z-10 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150" aria-label="Next">
<svg class="h-5 w-5" x-description="Heroicon name: chevron-right" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
</a>
}
</nav>
</div>
</div>
</div>
}

View file

@ -0,0 +1,16 @@
using System;
using Microsoft.AspNetCore.Components;
namespace ElsaDashboard.Application.Shared
{
partial class Pager
{
[Parameter] public int? Page { get; set; }
[Parameter] public int? PageSize { get; set; }
[Parameter] public int TotalCount { get; set; }
private int From => Page!.Value * PageSize!.Value + 1;
private int To => Math.Min(From + PageSize!.Value, TotalCount);
private int PageCount => TotalCount / PageSize!.Value;
}
}

View file

@ -0,0 +1,62 @@
<!-- This example requires Tailwind CSS v2.0+ -->
<div class="fixed z-10 inset-0 overflow-y-auto">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<!-- This element is to trick the browser into centering the modal contents. -->
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6" role="dialog" aria-modal="true" aria-labelledby="modal-headline">
<div class="hidden sm:block absolute top-0 right-0 pt-4 pr-4">
<button type="button" class="bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<span class="sr-only">Close</span>
<!-- Heroicon name: x -->
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="sm:flex sm:items-start">
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<!-- Heroicon name: exclamation -->
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
Deactivate account
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500">
Are you sure you want to deactivate your account? All of your data will be permanently removed from our servers forever. This action cannot be undone.
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
Deactivate
</button>
<button type="button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm">
Cancel
</button>
</div>
</div>
</div>
</div>
@code {
[CascadingParameter]
BlazoredModalInstance BlazoredModal { get; set; }
[Parameter]
public string Message { get; set; }
async Task Close() => await BlazoredModal.Close(ModalResult.Ok(true));
async Task Cancel() => await BlazoredModal.Cancel();
}

View file

@ -1,5 +1,7 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Blazored.Modal
@using Blazored.Modal.Services
@using ElsaDashboard.Application
@using ElsaDashboard.Application.Models
@using ElsaDashboard.Application.Shared

View file

@ -13,7 +13,9 @@
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.33.1" />
<PackageReference Include="Grpc.AspNetCore.Web" Version="2.33.1" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.0" />
<PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.0.136" />
<PackageReference Include="protobuf-net.NodaTime" Version="3.0.73" />
</ItemGroup>
</Project>

View file

@ -17,9 +17,10 @@ namespace ElsaDashboard.Backend.Extensions
public static IServiceCollection AddElsaDashboardBackend(this IServiceCollection services, Action<ElsaClientOptions>? configure = default)
{
RuntimeTypeModel.Default.AddElsaGrpcSurrogates();
RuntimeTypeModel.Default.AddNodaTime();
services.AddCors();
services.AddElsaClient(configure);
services.AddCodeFirstGrpc(options => options.ResponseCompressionLevel = CompressionLevel.Optimal);
services.AddCodeFirstGrpc(options => options.ResponseCompressionLevel = CompressionLevel.Fastest);
services.AddScoped<IActivityService, ActivityService>();
services.AddScoped<IWorkflowDefinitionService, WorkflowDefinitionService>();
services.AddScoped<IWorkflowRegistryService, WorkflowRegistryService>();

View file

@ -10,10 +10,7 @@ namespace ElsaDashboard.Backend.Rpc
{
private readonly IElsaClient _elsaClient;
public WorkflowInstanceService(IElsaClient elsaClient) => _elsaClient = elsaClient;
public Task<PagedList<WorkflowInstance>> ListAsync(int page = 0, int pageSize = 50, CallContext context = default) =>
_elsaClient.WorkflowInstanceses.ListAsync(page, pageSize, context.CancellationToken);
public Task<PagedList<WorkflowInstance>> ListAsync(int page = 0, int pageSize = 50, CallContext context = default) => _elsaClient.WorkflowInstanceses.ListAsync(page, pageSize, context.CancellationToken);
public Task<WorkflowInstance?> GetById(string id, CallContext context = default) => _elsaClient.WorkflowInstanceses.GetByIdAsync(id, context.CancellationToken);
}
}

View file

@ -13,7 +13,8 @@ namespace ElsaDashboard.Shared.Surrogates
private static readonly IDictionary<Type, Type> SurrogateMapping = new Dictionary<Type, Type>
{
[typeof(ActivityPropertyInfo)] = typeof(ActivityPropertyDescriptorSurrogate),
[typeof(VersionOptions)] = typeof(VersionOptionsSurrogate)
[typeof(VersionOptions)] = typeof(VersionOptionsSurrogate),
[typeof(WorkflowInstance)] = typeof(WorkflowInstanceSurrogate)
};
/// <summary>
@ -21,9 +22,9 @@ namespace ElsaDashboard.Shared.Surrogates
/// </summary>
public static RuntimeTypeModel AddElsaGrpcSurrogates(this RuntimeTypeModel runtimeTypeModel)
{
foreach (var map in SurrogateMapping)
foreach (var map in SurrogateMapping)
runtimeTypeModel.Add(map.Key, false).SetSurrogate(map.Value);
return runtimeTypeModel;
}
}

View file

@ -0,0 +1,35 @@
using Elsa.Client.Models;
using Newtonsoft.Json;
using NodaTime;
using NodaTime.Serialization.JsonNet;
using ProtoBuf;
namespace ElsaDashboard.Shared.Surrogates
{
[ProtoContract(IgnoreListHandling = true)]
public class WorkflowInstanceSurrogate
{
private static readonly JsonSerializerSettings SerializerSettings;
static WorkflowInstanceSurrogate()
{
SerializerSettings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto
}.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
}
public WorkflowInstanceSurrogate(WorkflowInstance value)
{
Value = Serialize(value);
}
[ProtoMember(1)] public string Value { get; }
public static implicit operator WorkflowInstance(WorkflowInstanceSurrogate surrogate) => Deserialize(surrogate.Value)!;
public static implicit operator WorkflowInstanceSurrogate(WorkflowInstance source) => new(source);
private static WorkflowInstance Deserialize(string json) => JsonConvert.DeserializeObject<WorkflowInstance>(json, SerializerSettings)!;
private static string Serialize(WorkflowInstance value) => JsonConvert.SerializeObject(value, SerializerSettings)!;
}
}

View file

@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="Grpc.Net.Client.Web" Version="2.33.1" />
<PackageReference Include="protobuf-net.NodaTime" Version="3.0.73" />
</ItemGroup>
<ItemGroup>

View file

@ -10,6 +10,7 @@ namespace ElsaDashboard.WebAssembly.Extensions
public static IServiceCollection AddElsaDashboardBackend(this IServiceCollection services)
{
RuntimeTypeModel.Default.AddElsaGrpcSurrogates();
RuntimeTypeModel.Default.AddNodaTime();
return services
.AddGrpcClient<IActivityService>()
.AddGrpcClient<IWorkflowDefinitionService>();

View file

@ -18,9 +18,9 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<!-- <ItemGroup>-->
<!-- <FrameworkReference Include="Microsoft.AspNetCore.App" />-->
<!-- </ItemGroup>-->
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />

View file

@ -1,4 +1,5 @@
using AutoMapper;
using Elsa.Models;
using Elsa.Server.Api.Endpoints.WorkflowRegistry;
using Elsa.Services.Models;

View file

@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using Elsa.Comparers;
using Elsa.Models;
using Newtonsoft.Json.Linq;
using NodaTime;
namespace Elsa.Server.Api.Models
{
[DataContract]
public class WorkflowInstanceModel
{
private HashSet<BlockingActivity> _blockingActivities = new(BlockingActivityEqualityComparer.Instance);
public WorkflowInstanceModel()
{
Variables = new Variables();
Activities = new List<ActivityInstance>();
ExecutionLog = new List<ExecutionLogEntry>();
ScheduledActivities = new Stack<ScheduledActivity>();
PostScheduledActivities = new Stack<ScheduledActivity>();
}
[DataMember(Order = 1)] public int Id { get; set; }
[DataMember(Order = 2)] public string WorkflowInstanceId { get; set; } = default!;
[DataMember(Order = 3)] public string WorkflowDefinitionId { get; set; } = default!;
[DataMember(Order = 4)] public int Version { get; set; }
[DataMember(Order = 5)] public WorkflowStatus Status { get; set; }
[DataMember(Order = 6)] public string? CorrelationId { get; set; }
[DataMember(Order = 7)] public string? ContextId { get; set; }
[DataMember(Order = 8)] public Instant CreatedAt { get; set; }
[DataMember(Order = 9)] public Instant? LastExecutedAt { get; set; }
[DataMember(Order = 10)] public Instant? LastBurstAt { get; set; }
[DataMember(Order = 11)] public Instant? CompletedAt { get; set; }
[DataMember(Order = 12)] public Variables Variables { get; set; }
[DataMember(Order = 13)] public JObject? Output { get; set; }
[DataMember(Order = 14)] public ICollection<ActivityInstance> Activities { get; set; }
[DataMember(Order = 15)] public HashSet<BlockingActivity> BlockingActivities
{
get => _blockingActivities;
set => _blockingActivities = new HashSet<BlockingActivity>(value, BlockingActivityEqualityComparer.Instance);
}
[DataMember(Order = 16)] public ICollection<ExecutionLogEntry> ExecutionLog { get; set; }
[DataMember(Order = 17)] public WorkflowFault? Fault { get; set; }
[DataMember(Order = 18)] public Stack<ScheduledActivity> ScheduledActivities { get; set; }
[DataMember(Order = 19)] public Stack<ScheduledActivity> PostScheduledActivities { get; set; }
}
}

View file

@ -9,6 +9,7 @@ namespace Elsa.Server.Host.Workflows
public void Build(IWorkflowBuilder workflow)
{
workflow
.WithDisplayName("Hello World!")
.HttpRequestReceived("/hello-world")
.WriteHttpResponse(HttpStatusCode.OK, "Hello World!", "text/plain");
}